From 0dc105405c868c0abb93fe9681cd497b7f19ec00 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 29 Mar 2017 13:37:36 -0600 Subject: [PATCH 01/17] Added new dockerfile for plugin development --- .dockerignore | 15 +++++++++++++++ .gitignore | 10 +--------- Dockerfile | 14 +++++++++----- Dockerfile.onbuild | 13 +++++++++++++ scripts/deploy.sh | 8 ++++++-- 5 files changed, 44 insertions(+), 16 deletions(-) create mode 100644 Dockerfile.onbuild diff --git a/.dockerignore b/.dockerignore index 14b1fb97f..e413ac5ea 100644 --- a/.dockerignore +++ b/.dockerignore @@ -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 diff --git a/.gitignore b/.gitignore index d91635c94..62a765fff 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/Dockerfile b/Dockerfile index f8cb8a45d..4e2b77520 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,16 +5,20 @@ 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 && \ + 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"] diff --git a/Dockerfile.onbuild b/Dockerfile.onbuild new file mode 100644 index 000000000..2476c6d1a --- /dev/null +++ b/Dockerfile.onbuild @@ -0,0 +1,13 @@ +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 && \ + NODE_ENV=production yarn build && \ + NODE_ENV=production yarn install --production && \ + yarn cache clean \ No newline at end of file diff --git a/scripts/deploy.sh b/scripts/deploy.sh index fd681fc7f..efb182dc9 100644 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -28,6 +28,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,16 +36,19 @@ 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" ] From 126387becc69cc9814800561490d8ad26ba2d113 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 30 Mar 2017 11:37:18 -0600 Subject: [PATCH 02/17] Adjusted docker build script --- circle.yml | 4 ++-- scripts/{deploy.sh => docker.sh} | 27 ++++++++++++++++++--------- 2 files changed, 20 insertions(+), 11 deletions(-) rename scripts/{deploy.sh => docker.sh} (78%) diff --git a/circle.yml b/circle.yml index 2357ecf4c..c97f30cee 100644 --- a/circle.yml +++ b/circle.yml @@ -47,9 +47,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 diff --git a/scripts/deploy.sh b/scripts/docker.sh similarity index 78% rename from scripts/deploy.sh rename to scripts/docker.sh index efb182dc9..4aef5956f 100644 --- a/scripts/deploy.sh +++ b/scripts/docker.sh @@ -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 @@ -50,10 +48,21 @@ deploy_latest() { 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" -eq "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 \ No newline at end of file From da4eceafe9c18c4d43946672250b9e2bd10cd693 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 30 Mar 2017 14:19:42 -0600 Subject: [PATCH 03/17] Fix for deploy script --- scripts/docker.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) mode change 100644 => 100755 scripts/docker.sh diff --git a/scripts/docker.sh b/scripts/docker.sh old mode 100644 new mode 100755 index 4aef5956f..3aae8faf1 --- a/scripts/docker.sh +++ b/scripts/docker.sh @@ -48,7 +48,7 @@ deploy_latest() { docker build -t coralproject/talk:latest -f Dockerfile . docker build -t coralproject/talk:latest-onbuild -f Dockerfile.onbuild . -if [ "$1" -eq "deploy" ] +if [ "$1" = "deploy" ] then if [[ -n "$DOCKER_EMAIL" && -n "$DOCKER_USER" && -n "$DOCKER_PASS" ]] From e5da8354d65fb74867bfac794ef96d8d4f35cfc3 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 31 Mar 2017 09:34:38 -0600 Subject: [PATCH 04/17] Added frozen lockfile support --- Dockerfile | 2 +- Dockerfile.onbuild | 2 +- circle.yml | 3 ++- client/coral-admin/README.md | 23 ----------------------- 4 files changed, 4 insertions(+), 26 deletions(-) delete mode 100644 client/coral-admin/README.md diff --git a/Dockerfile b/Dockerfile index 4e2b77520..7cef46451 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ EXPOSE 5000 COPY . /usr/src/app # Install app dependencies and build static assets. -RUN yarn install && \ +RUN yarn install --frozen-lockfile && \ yarn build && \ yarn install --production && \ yarn cache clean diff --git a/Dockerfile.onbuild b/Dockerfile.onbuild index 2476c6d1a..a34887f3a 100644 --- a/Dockerfile.onbuild +++ b/Dockerfile.onbuild @@ -7,7 +7,7 @@ ONBUILD COPY . /usr/src/app # 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 && \ +ONBUILD RUN NODE_ENV=development yarn install --frozen-lockfile && \ NODE_ENV=production yarn build && \ NODE_ENV=production yarn install --production && \ yarn cache clean \ No newline at end of file diff --git a/circle.yml b/circle.yml index c97f30cee..013ef28b8 100644 --- a/circle.yml +++ b/circle.yml @@ -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 diff --git a/client/coral-admin/README.md b/client/coral-admin/README.md deleted file mode 100644 index 7bc9de36e..000000000 --- a/client/coral-admin/README.md +++ /dev/null @@ -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 From 01086735669ba922df6775b3a58739aab6371831 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 31 Mar 2017 15:54:39 -0600 Subject: [PATCH 05/17] Added remote plugin reconciliation --- bin/cli | 1 + bin/cli-plugins | 193 ++++++++++++++++++++++++ graph/context.js | 2 +- graph/loaders/index.js | 2 +- graph/mutators/index.js | 2 +- graph/resolvers/index.js | 2 +- graph/schema.js | 2 +- graph/typeDefs.js | 2 +- package.json | 3 + plugins.js | 68 ++++++++- services/plugins.js | 3 + yarn.lock | 311 ++++++++++----------------------------- 12 files changed, 343 insertions(+), 248 deletions(-) create mode 100755 bin/cli-plugins create mode 100644 services/plugins.js diff --git a/bin/cli b/bin/cli index c0690e455..7e27fa85b 100755 --- a/bin/cli +++ b/bin/cli @@ -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); /** diff --git a/bin/cli-plugins b/bin/cli-plugins new file mode 100755 index 000000000..581631573 --- /dev/null +++ b/bin/cli-plugins @@ -0,0 +1,193 @@ +#!/usr/bin/env node + +/** + * Module dependencies. + */ + +const program = require('./commander'); + +// Make things colorful! +require('colors'); + +const fs = require('fs'); +const path = require('path'); +const spawn = require('cross-spawn'); +const resolve = require('resolve'); +const {plugins, isInternal} = require('../plugins'); + +function existsInNodeModules(name) { + try { + resolve.sync(name, {basedir: process.cwd()}); + + return true; + } catch (e) { + return false; + } +} + +const EXTERNAL = /^\w[a-z\-0-9\.]+$/; // Match "react", "path", "fs", "lodash.random", etc. + +async function reconcileRemotePlugins(dryRun) { + const linkable = []; + const fetchable = []; + + console.log('\n[1/3] 🔍 Reconciling plugins...\n'.yellow); + console.log(' +local (l) packages in your project\n +external (e) referenced packages in node_modules but not in current project\n +missing (m) referenced packages but not found\n +symlinked (sl) symlinked external packages\n'); + for (let i in plugins) { + let section = plugins[i]; + + for (let name in section) { + let version = section[name]; + + 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)) { + let stat = fs.lstatSync(path.join(process.cwd(), 'plugins', name)); + if (stat.isSymbolicLink()) { + console.log(` sl ${name.cyan}`); + } else { + console.log(` l ${name.cyan}`); + } + + continue; + } + + if (!existsInNodeModules(dep)) { + console.warn(` m ${name.cyan}`); + fetchable.push({name, version}); + } else { + console.log(` e ${name.cyan}`); + linkable.push({name, version}); + } + } + } + + console.log('\n[2/3] 🚚 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()); + } + + fetchable.forEach((plugin) => { + linkable.push(plugin); + }); + } + + // TODO fetch the plugins using yarn + + console.log('\n[3/3] 🔗 Linking plugins...\n'.yellow); + + for (let i in linkable) { + let {name} = linkable[i]; + + let src = path.join(process.cwd(), 'node_modules', name); + let dst = path.join(process.cwd(), 'plugins', name); + + console.log(`$ link ${src.cyan} -> ${dst.cyan}`); + + if (!dryRun) { + + // Create the symlink for the plugin directory. + fs.symlinkSync(src, dst, 'dir'); + } + } + + return {linkable, fetchable}; +} + +// 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(options) { + let startTime = new Date(); + + // We don't need to do anything if we skip everything.... + if (options.skipLocal && options.skipRemote) { + return; + } + + // Traverse local plugins and install dependencies if enabled. + if (!options.skipLocal) { + + } + + // Locate any external plugins and install them. + if (!options.skipRemote) { + let results = []; + try { + results = await reconcileRemotePlugins(options.dryRun); + } catch (e) { + throw e; + } + + let status; + if (options.dryRun) { + status = '[dry-run] success'.green; + } else { + status = 'success'.green; + } + + let message; + if (results.linkable.length === 0 && results.fetchable.length === 0) { + message = 'Already up-to-date.'; + } else if (results.linkable.length === 0) { + message = `Fetched ${results.fetchable.length} new plugins.`; + } else if (results.fetchable.length === 0) { + message = `Linked ${results.linkable.length} new plugins.`; + } else { + message = `Fetched ${results.fetchable.length} new plugins, linked ${results.linkable.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('reconcile') + .description('reconciles local plugin dependencies and downloads external plugins') + .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(); +} diff --git a/graph/context.js b/graph/context.js index 432ab2081..f3ed42ad8 100644 --- a/graph/context.js +++ b/graph/context.js @@ -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'); /** diff --git a/graph/loaders/index.js b/graph/loaders/index.js index 40e723918..26727940f 100644 --- a/graph/loaders/index.js +++ b/graph/loaders/index.js @@ -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 = [ diff --git a/graph/mutators/index.js b/graph/mutators/index.js index e3df4aefb..9975bedde 100644 --- a/graph/mutators/index.js +++ b/graph/mutators/index.js @@ -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 = [ diff --git a/graph/resolvers/index.js b/graph/resolvers/index.js index 7e165f2db..361cfa48f 100644 --- a/graph/resolvers/index.js +++ b/graph/resolvers/index.js @@ -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 = { diff --git a/graph/schema.js b/graph/schema.js index c927d8c32..53360e701 100644 --- a/graph/schema.js +++ b/graph/schema.js @@ -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'); diff --git a/graph/typeDefs.js b/graph/typeDefs.js index 5b07d0f2d..9b2c8b7aa 100644 --- a/graph/typeDefs.js +++ b/graph/typeDefs.js @@ -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 diff --git a/package.json b/package.json index 6fdf4560a..17dabb747 100644 --- a/package.json +++ b/package.json @@ -52,8 +52,10 @@ "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", @@ -88,6 +90,7 @@ "react-apollo": "^0.10.0", "react-recaptcha": "^2.2.6", "redis": "^2.7.1", + "resolve": "^1.3.2", "uuid": "^2.0.3" }, "devDependencies": { diff --git a/plugins.js b/plugins.js index 7713e0b8f..116e9093c 100644 --- a/plugins.js +++ b/plugins.js @@ -1,5 +1,6 @@ const fs = require('fs'); const path = require('path'); +const debug = require('debug')('talk:plugins'); let plugins = {}; @@ -16,13 +17,65 @@ 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); + } + + // The plugin is not available. + return undefined; +} + /** * 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}`); + this.plugins = Object.keys(plugin_names).map((plugin_name) => { + let plugin = {}; + + // Get the version/folder that was specified. + plugin.version = plugin_names[plugin_name]; + + // Get the path for the plugin. + plugin.path = pluginPath(plugin_name); + + if (typeof plugin.path === 'undefined') { + throw new Error(`plugin '${plugin_name}' is not local or is not symlinked from your node_modules directory, plugin reconsiliation may be required`); + } + + try { + plugin.module = require(plugin.path); + } catch (e) { + throw new Error(`plugin '${plugin_name}' could not be required from '${plugin.path}': ${e.message}`); + } + + if (isInternal(plugin_name)) { + debug(`loading internal plugin '${plugin_name}' from '${plugin.path}'`); + } else { + debug(`loading external plugin '${plugin_name}' from '${plugin.path}'`); + } // Ensure we have a default plugin name, but allow the name to be // overrided by the plugin. @@ -38,8 +91,8 @@ 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]: module[hook]})); } } @@ -78,4 +131,9 @@ class PluginManager { } } -module.exports = new PluginManager(plugins); +module.exports = { + plugins, + PluginManager, + isInternal, + pluginPath +}; diff --git a/services/plugins.js b/services/plugins.js new file mode 100644 index 000000000..ced4bd75a --- /dev/null +++ b/services/plugins.js @@ -0,0 +1,3 @@ +const {plugins, PluginManager} = require('../plugins'); + +module.exports = new PluginManager(plugins); diff --git a/yarn.lock b/yarn.lock index 81ddf18b4..49a633adb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -397,19 +397,7 @@ babel-eslint@^7.2.1: babel-types "^6.23.0" babylon "^6.16.1" -babel-generator@^6.18.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.22.0.tgz#d642bf4961911a8adc7c692b0c9297f325cda805" - dependencies: - babel-messages "^6.22.0" - babel-runtime "^6.22.0" - babel-types "^6.22.0" - detect-indent "^4.0.0" - jsesc "^1.3.0" - lodash "^4.2.0" - source-map "^0.5.0" - -babel-generator@^6.24.0: +babel-generator@^6.18.0, babel-generator@^6.24.0: version "6.24.0" resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.24.0.tgz#eba270a8cc4ce6e09a61be43465d7c62c1f87c56" dependencies: @@ -482,17 +470,7 @@ babel-helper-explode-class@^6.22.0: babel-traverse "^6.22.0" babel-types "^6.22.0" -babel-helper-function-name@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.22.0.tgz#51f1bdc4bb89b15f57a9b249f33d742816dcbefc" - dependencies: - babel-helper-get-function-arity "^6.22.0" - babel-runtime "^6.22.0" - babel-template "^6.22.0" - babel-traverse "^6.22.0" - babel-types "^6.22.0" - -babel-helper-function-name@^6.23.0: +babel-helper-function-name@^6.22.0, babel-helper-function-name@^6.23.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.23.0.tgz#25742d67175c8903dbe4b6cb9d9e1fcb8dcf23a6" dependencies: @@ -576,13 +554,7 @@ babel-loader@^6.4.1: mkdirp "^0.5.1" object-assign "^4.0.1" -babel-messages@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.22.0.tgz#36066a214f1217e4ed4164867669ecb39e3ea575" - dependencies: - babel-runtime "^6.22.0" - -babel-messages@^6.23.0: +babel-messages@^6.22.0, babel-messages@^6.23.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" dependencies: @@ -686,16 +658,7 @@ babel-plugin-transform-class-constructor-call@^6.22.0: babel-runtime "^6.22.0" babel-template "^6.22.0" -babel-plugin-transform-class-properties@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.22.0.tgz#aa78f8134495c7de06c097118ba061844e1dc1d8" - dependencies: - babel-helper-function-name "^6.22.0" - babel-plugin-syntax-class-properties "^6.8.0" - babel-runtime "^6.22.0" - babel-template "^6.22.0" - -babel-plugin-transform-class-properties@^6.23.0: +babel-plugin-transform-class-properties@^6.22.0, babel-plugin-transform-class-properties@^6.23.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.23.0.tgz#187b747ee404399013563c993db038f34754ac3b" dependencies: @@ -925,14 +888,7 @@ babel-plugin-transform-object-assign@^6.8.0: dependencies: babel-runtime "^6.22.0" -babel-plugin-transform-object-rest-spread@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.22.0.tgz#1d419b55e68d2e4f64a5ff3373bd67d73c8e83bc" - dependencies: - babel-plugin-syntax-object-rest-spread "^6.8.0" - babel-runtime "^6.22.0" - -babel-plugin-transform-object-rest-spread@^6.23.0: +babel-plugin-transform-object-rest-spread@^6.22.0, babel-plugin-transform-object-rest-spread@^6.23.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.23.0.tgz#875d6bc9be761c58a2ae3feee5dc4895d8c7f921" dependencies: @@ -1057,17 +1013,7 @@ babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.2.0, babel-runtim core-js "^2.4.0" regenerator-runtime "^0.10.0" -babel-template@^6.16.0, babel-template@^6.22.0, babel-template@^6.3.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.22.0.tgz#403d110905a4626b317a2a1fcb8f3b73204b2edb" - dependencies: - babel-runtime "^6.22.0" - babel-traverse "^6.22.0" - babel-types "^6.22.0" - babylon "^6.11.0" - lodash "^4.2.0" - -babel-template@^6.23.0: +babel-template@^6.16.0, babel-template@^6.22.0, babel-template@^6.23.0, babel-template@^6.3.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.23.0.tgz#04d4f270adbb3aa704a8143ae26faa529238e638" dependencies: @@ -1077,21 +1023,7 @@ babel-template@^6.23.0: babylon "^6.11.0" lodash "^4.2.0" -babel-traverse@^6.18.0, babel-traverse@^6.22.0: - version "6.22.1" - resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.22.1.tgz#3b95cd6b7427d6f1f757704908f2fc9748a5f59f" - dependencies: - babel-code-frame "^6.22.0" - babel-messages "^6.22.0" - babel-runtime "^6.22.0" - babel-types "^6.22.0" - babylon "^6.15.0" - debug "^2.2.0" - globals "^9.0.0" - invariant "^2.2.0" - lodash "^4.2.0" - -babel-traverse@^6.23.0, babel-traverse@^6.23.1: +babel-traverse@^6.18.0, babel-traverse@^6.22.0, babel-traverse@^6.23.0, babel-traverse@^6.23.1: version "6.23.1" resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.23.1.tgz#d3cb59010ecd06a97d81310065f966b699e14f48" dependencies: @@ -1105,16 +1037,7 @@ babel-traverse@^6.23.0, babel-traverse@^6.23.1: invariant "^2.2.0" lodash "^4.2.0" -babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.22.0.tgz#2a447e8d0ea25d2512409e4175479fd78cc8b1db" - dependencies: - babel-runtime "^6.22.0" - esutils "^2.0.2" - lodash "^4.2.0" - to-fast-properties "^1.0.1" - -babel-types@^6.23.0: +babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.22.0, babel-types@^6.23.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.23.0.tgz#bb17179d7538bad38cd0c9e115d340f77e7e9acf" dependencies: @@ -1123,11 +1046,11 @@ babel-types@^6.23.0: lodash "^4.2.0" to-fast-properties "^1.0.1" -babylon@^6.11.0, babylon@^6.13.0, babylon@^6.15.0: +babylon@^6.11.0: version "6.15.0" resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.15.0.tgz#ba65cfa1a80e1759b0e89fb562e27dccae70348e" -babylon@^6.16.1: +babylon@^6.13.0, babylon@^6.15.0, babylon@^6.16.1: version "6.16.1" resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.16.1.tgz#30c5a22f481978a9e7f8cdfdf496b11d94b404d3" @@ -1221,22 +1144,7 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: version "4.11.6" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.6.tgz#53344adb14617a13f6e8dd2ce28905d1c0ba3215" -body-parser@^1.12.2: - version "1.16.0" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.16.0.tgz#924a5e472c6229fb9d69b85a20d5f2532dec788b" - dependencies: - bytes "2.4.0" - content-type "~1.0.2" - debug "2.6.0" - depd "~1.1.0" - http-errors "~1.5.1" - iconv-lite "0.4.15" - on-finished "~2.3.0" - qs "6.2.1" - raw-body "~2.2.0" - type-is "~1.6.14" - -body-parser@^1.17.1: +body-parser@^1.12.2, body-parser@^1.17.1: version "1.17.1" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.17.1.tgz#75b3bc98ddd6e7e0d8ffe750dfaca5c66993fa47" dependencies: @@ -1723,7 +1631,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" @@ -1965,6 +1873,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" @@ -2186,9 +2102,9 @@ date-now@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" -debug@*, debug@2, debug@2.6.0, debug@^2.1.1, debug@^2.2.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.0.tgz#bc596bcabe7617f11d9fa15361eded5608b8499b" +debug@*, debug@2, debug@2.6.3, debug@^2.1.1, debug@^2.2.0, debug@^2.6.3: + version "2.6.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.3.tgz#0f7eb8c30965ec08c72accfa0130c8b79984141d" dependencies: ms "0.7.2" @@ -2210,12 +2126,6 @@ debug@2.6.1: dependencies: ms "0.7.2" -debug@2.6.3, debug@^2.6.3: - version "2.6.3" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.3.tgz#0f7eb8c30965ec08c72accfa0130c8b79984141d" - dependencies: - ms "0.7.2" - debug@^0.7.2, debug@~0.7.4: version "0.7.4" resolved "https://registry.yarnpkg.com/debug/-/debug-0.7.4.tgz#06e1ea8082c2cb14e39806e22e2f6f757f92af39" @@ -2797,10 +2707,6 @@ esutils@^2.0.0, esutils@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" -etag@~1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.7.0.tgz#03d30b5f67dd6e632d2945d30d6652731a34d5d8" - etag@~1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.0.tgz#6f631aef336d6c46362b51764044ce216be3c051" @@ -2871,38 +2777,7 @@ express-session@^1.15.1: uid-safe "~2.1.3" utils-merge "1.0.0" -express@^4.12.2: - version "4.14.1" - resolved "https://registry.yarnpkg.com/express/-/express-4.14.1.tgz#646c237f766f148c2120aff073817b9e4d7e0d33" - dependencies: - accepts "~1.3.3" - array-flatten "1.1.1" - content-disposition "0.5.2" - content-type "~1.0.2" - cookie "0.3.1" - cookie-signature "1.0.6" - debug "~2.2.0" - depd "~1.1.0" - encodeurl "~1.0.1" - escape-html "~1.0.3" - etag "~1.7.0" - finalhandler "0.5.1" - fresh "0.3.0" - merge-descriptors "1.0.1" - methods "~1.1.2" - on-finished "~2.3.0" - parseurl "~1.3.1" - path-to-regexp "0.1.7" - proxy-addr "~1.1.3" - qs "6.2.0" - range-parser "~1.2.0" - send "0.14.2" - serve-static "~1.11.2" - type-is "~1.6.14" - utils-merge "1.0.0" - vary "~1.1.0" - -express@^4.15.2: +express@^4.12.2, express@^4.15.2: version "4.15.2" resolved "https://registry.yarnpkg.com/express/-/express-4.15.2.tgz#af107fc148504457f2dca9a6f2571d7129b97b35" dependencies: @@ -3038,16 +2913,6 @@ fill-range@^2.1.0: repeat-element "^1.1.2" repeat-string "^1.5.2" -finalhandler@0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-0.5.1.tgz#2c400d8d4530935bc232549c5fa385ec07de6fcd" - dependencies: - debug "~2.2.0" - escape-html "~1.0.3" - on-finished "~2.3.0" - statuses "~1.3.1" - unpipe "~1.0.0" - finalhandler@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.0.tgz#b5691c2c0912092f18ac23e9416bde5cd7dc6755" @@ -3167,10 +3032,6 @@ frameguard@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/frameguard/-/frameguard-3.0.0.tgz#7bcad469ee7b96e91d12ceb3959c78235a9272e9" -fresh@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.3.0.tgz#651f838e22424e7566de161d8358caa199f83d4f" - fresh@0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.0.tgz#f474ca5e6a9246d6fd8e0953cfa9b9c805afa78e" @@ -3756,7 +3617,7 @@ htmlparser2@~3.8.1: entities "1.0" readable-stream "1.1" -http-errors@~1.5.0, http-errors@~1.5.1: +http-errors@~1.5.0: version "1.5.1" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.5.1.tgz#788c0d2c1de2c81b9e6e8c01843b6b97eb920750" dependencies: @@ -4269,19 +4130,7 @@ istanbul-lib-hook@^1.0.0: dependencies: append-transform "^0.4.0" -istanbul-lib-instrument@^1.3.0: - version "1.4.2" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.4.2.tgz#0e2fdfac93c1dabf2e31578637dc78a19089f43e" - dependencies: - babel-generator "^6.18.0" - babel-template "^6.16.0" - babel-traverse "^6.18.0" - babel-types "^6.18.0" - babylon "^6.13.0" - istanbul-lib-coverage "^1.0.0" - semver "^5.3.0" - -istanbul-lib-instrument@^1.6.2: +istanbul-lib-instrument@^1.3.0, istanbul-lib-instrument@^1.6.2: version "1.6.2" resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.6.2.tgz#dac644f358f51efd6113536d7070959a0111f73b" dependencies: @@ -4912,6 +4761,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" @@ -6280,18 +6136,18 @@ postcss@5.1.2: source-map "^0.5.6" supports-color "^3.1.2" -postcss@^5.0.0, postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.19, postcss@^5.0.2, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.6, postcss@^5.0.8, postcss@^5.1.2, postcss@^5.2.11, postcss@^5.2.4, postcss@^5.2.5: - version "5.2.11" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.11.tgz#ff29bcd6d2efb98bfe08a022055ec599bbe7b761" +postcss@^5.0.0, postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.19, postcss@^5.0.2, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.8, postcss@^5.1.2, postcss@^5.2.15, postcss@^5.2.4, postcss@^5.2.5: + version "5.2.16" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.16.tgz#732b3100000f9ff8379a48a53839ed097376ad57" dependencies: chalk "^1.1.3" js-base64 "^2.1.9" source-map "^0.5.6" supports-color "^3.2.3" -postcss@^5.2.15: - version "5.2.16" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.16.tgz#732b3100000f9ff8379a48a53839ed097376ad57" +postcss@^5.0.6, postcss@^5.2.11: + version "5.2.11" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.11.tgz#ff29bcd6d2efb98bfe08a022055ec599bbe7b761" dependencies: chalk "^1.1.3" js-base64 "^2.1.9" @@ -6412,6 +6268,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" @@ -6543,15 +6403,7 @@ q@^1.1.2: version "1.4.1" resolved "https://registry.yarnpkg.com/q/-/q-1.4.1.tgz#55705bcd93c5f3673530c2c2cbc0c2b3addc286e" -qs@6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.0.tgz#3b7848c03c2dece69a9522b0fae8c4126d745f3b" - -qs@6.2.1, qs@^6.1.0, qs@^6.2.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.1.tgz#ce03c5ff0935bc1d9d69a9f14cbd18e568d67625" - -qs@6.4.0: +qs@6.4.0, qs@^6.1.0, qs@^6.2.0: version "6.4.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" @@ -6828,11 +6680,7 @@ redis-commands@^1.2.0: version "1.3.1" resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.3.1.tgz#81d826f45fa9c8b2011f4cd7a0fe597d241d442b" -redis-parser@^2.0.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-2.4.0.tgz#018ea743077aae944d0b798b2fd12587320bf3c9" - -redis-parser@^2.5.0: +redis-parser@^2.0.0, redis-parser@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-2.5.0.tgz#79fc2b1d4a6e4d2870b35368433639271fca2617" @@ -6840,15 +6688,7 @@ redis@^0.12.1: version "0.12.1" resolved "https://registry.yarnpkg.com/redis/-/redis-0.12.1.tgz#64df76ad0fc8acebaebd2a0645e8a48fac49185e" -redis@^2.1.0, redis@~2.6.0-2: - version "2.6.5" - resolved "https://registry.yarnpkg.com/redis/-/redis-2.6.5.tgz#87c1eff4a489f94b70871f3d08b6988f23a95687" - dependencies: - double-ended-queue "^2.1.0-0" - redis-commands "^1.2.0" - redis-parser "^2.0.0" - -redis@^2.7.1: +redis@^2.1.0, redis@^2.7.1: version "2.7.1" resolved "https://registry.yarnpkg.com/redis/-/redis-2.7.1.tgz#7d56f7875b98b20410b71539f1d878ed58ebf46a" dependencies: @@ -6856,6 +6696,14 @@ redis@^2.7.1: redis-commands "^1.2.0" redis-parser "^2.5.0" +redis@~2.6.0-2: + version "2.6.5" + resolved "https://registry.yarnpkg.com/redis/-/redis-2.6.5.tgz#87c1eff4a489f94b70871f3d08b6988f23a95687" + dependencies: + double-ended-queue "^2.1.0-0" + redis-commands "^1.2.0" + redis-parser "^2.0.0" + reds@^0.2.5: version "0.2.5" resolved "https://registry.yarnpkg.com/reds/-/reds-0.2.5.tgz#38a767f7663cd749036848697d82c74fd29bc01f" @@ -7056,9 +6904,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" @@ -7172,24 +7022,6 @@ semver@~5.0.1: version "5.0.3" resolved "https://registry.yarnpkg.com/semver/-/semver-5.0.3.tgz#77466de589cd5d3c95f138aa78bc569a3cb5d27a" -send@0.14.2: - version "0.14.2" - resolved "https://registry.yarnpkg.com/send/-/send-0.14.2.tgz#39b0438b3f510be5dc6f667a11f71689368cdeef" - dependencies: - debug "~2.2.0" - depd "~1.1.0" - destroy "~1.0.4" - encodeurl "~1.0.1" - escape-html "~1.0.3" - etag "~1.7.0" - fresh "0.3.0" - http-errors "~1.5.1" - mime "1.3.4" - ms "0.7.2" - on-finished "~2.3.0" - range-parser "~1.2.0" - statuses "~1.3.1" - send@0.15.1: version "0.15.1" resolved "https://registry.yarnpkg.com/send/-/send-0.15.1.tgz#8a02354c26e6f5cca700065f5f0cdeba90ec7b5f" @@ -7217,15 +7049,6 @@ serve-static@1.12.1: parseurl "~1.3.1" send "0.15.1" -serve-static@~1.11.2: - version "1.11.2" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.11.2.tgz#2cf9889bd4435a320cc36895c9aa57bd662e6ac7" - dependencies: - encodeurl "~1.0.1" - escape-html "~1.0.3" - parseurl "~1.3.1" - send "0.14.2" - set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -7252,6 +7075,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" @@ -8128,7 +7961,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: @@ -8230,6 +8063,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" From c4edab9bbdc653a2744be41477a4b48012572439 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 31 Mar 2017 16:09:56 -0600 Subject: [PATCH 06/17] cleaned up cli --- bin/cli-plugins | 31 +++++++++++++++++++++++-------- package.json | 1 + yarn.lock | 10 ++++++++++ 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/bin/cli-plugins b/bin/cli-plugins index 581631573..673f6c058 100755 --- a/bin/cli-plugins +++ b/bin/cli-plugins @@ -8,7 +8,9 @@ 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'); @@ -17,7 +19,7 @@ const {plugins, isInternal} = require('../plugins'); function existsInNodeModules(name) { try { - resolve.sync(name, {basedir: process.cwd()}); + resolve.sync(name, {basedir: dir}); return true; } catch (e) { @@ -27,11 +29,11 @@ function existsInNodeModules(name) { const EXTERNAL = /^\w[a-z\-0-9\.]+$/; // Match "react", "path", "fs", "lodash.random", etc. -async function reconcileRemotePlugins(dryRun) { +function reconcilePackages() { const linkable = []; const fetchable = []; - console.log('\n[1/3] 🔍 Reconciling plugins...\n'.yellow); + console.log(); console.log(' +local (l) packages in your project\n +external (e) referenced packages in node_modules but not in current project\n +missing (m) referenced packages but not found\n +symlinked (sl) symlinked external packages\n'); for (let i in plugins) { let section = plugins[i]; @@ -50,7 +52,7 @@ async function reconcileRemotePlugins(dryRun) { } if (isInternal(dep)) { - let stat = fs.lstatSync(path.join(process.cwd(), 'plugins', name)); + let stat = fs.lstatSync(path.join(dir, 'plugins', name)); if (stat.isSymbolicLink()) { console.log(` sl ${name.cyan}`); } else { @@ -69,8 +71,16 @@ async function reconcileRemotePlugins(dryRun) { } } } + console.log(); - console.log('\n[2/3] 🚚 Fetching plugins...\n'.yellow); + return {linkable, fetchable}; +} + +async function reconcileRemotePlugins(dryRun) { + console.log(`\n[1/3] ${emoji.get('mag')} Reconciling plugins...`.yellow); + const {linkable, fetchable} = reconcilePackages(); + + console.log(`[2/3] ${emoji.get('truck')} Fetching plugins...\n`.yellow); if (fetchable.length > 0) { @@ -101,13 +111,13 @@ async function reconcileRemotePlugins(dryRun) { // TODO fetch the plugins using yarn - console.log('\n[3/3] 🔗 Linking plugins...\n'.yellow); + console.log(`\n[3/3] ${emoji.get('link')} Linking plugins...\n`.yellow); for (let i in linkable) { let {name} = linkable[i]; - let src = path.join(process.cwd(), 'node_modules', name); - let dst = path.join(process.cwd(), 'plugins', name); + let src = path.join(dir, 'node_modules', name); + let dst = path.join(dir, 'plugins', name); console.log(`$ link ${src.cyan} -> ${dst.cyan}`); @@ -177,6 +187,11 @@ async function reconcilePluginDeps(options) { // 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') diff --git a/package.json b/package.json index 17dabb747..cca869dac 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,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", diff --git a/yarn.lock b/yarn.lock index 49a633adb..82af883c0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5156,6 +5156,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" @@ -7345,6 +7351,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" From f1239b4b19f37576403773cc6f7b2348ae641fbd Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 31 Mar 2017 16:37:24 -0600 Subject: [PATCH 07/17] Added support for local plugin dep install --- .eslintignore | 2 ++ Dockerfile | 1 + Dockerfile.onbuild | 1 + bin/cli-plugins | 74 +++++++++++++++++++++++++++++++++------------- 4 files changed, 58 insertions(+), 20 deletions(-) diff --git a/.eslintignore b/.eslintignore index 83564d3ff..6ec4c0c88 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,5 @@ dist client/lib **/*.html +plugins/ +plugins/**/node_modules \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 7cef46451..5bc38013a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,6 +14,7 @@ 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 diff --git a/Dockerfile.onbuild b/Dockerfile.onbuild index a34887f3a..34c321448 100644 --- a/Dockerfile.onbuild +++ b/Dockerfile.onbuild @@ -8,6 +8,7 @@ ONBUILD COPY . /usr/src/app # 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 \ No newline at end of file diff --git a/bin/cli-plugins b/bin/cli-plugins index 673f6c058..319c046b3 100755 --- a/bin/cli-plugins +++ b/bin/cli-plugins @@ -29,12 +29,13 @@ function existsInNodeModules(name) { const EXTERNAL = /^\w[a-z\-0-9\.]+$/; // Match "react", "path", "fs", "lodash.random", etc. -function reconcilePackages() { +function reconcilePackages(log = console.log) { const linkable = []; const fetchable = []; + const local = []; - console.log(); - console.log(' +local (l) packages in your project\n +external (e) referenced packages in node_modules but not in current project\n +missing (m) referenced packages but not found\n +symlinked (sl) symlinked external packages\n'); + log(); + log(' +local (l) packages in your project\n +external (e) referenced packages in node_modules but not in current project\n +missing (m) referenced packages but not found\n +symlinked (sl) symlinked external packages\n'); for (let i in plugins) { let section = plugins[i]; @@ -54,33 +55,34 @@ function reconcilePackages() { if (isInternal(dep)) { let stat = fs.lstatSync(path.join(dir, 'plugins', name)); if (stat.isSymbolicLink()) { - console.log(` sl ${name.cyan}`); + log(` sl ${name.cyan}`); } else { - console.log(` l ${name.cyan}`); + log(` l ${name.cyan}`); + local.push({name, version}); } continue; } if (!existsInNodeModules(dep)) { - console.warn(` m ${name.cyan}`); + log(` m ${name.cyan}`); fetchable.push({name, version}); } else { - console.log(` e ${name.cyan}`); + log(` e ${name.cyan}`); linkable.push({name, version}); } } } - console.log(); + log(); - return {linkable, fetchable}; + return {local, linkable, fetchable}; } -async function reconcileRemotePlugins(dryRun) { - console.log(`\n[1/3] ${emoji.get('mag')} Reconciling plugins...`.yellow); +async function reconcileRemotePlugins({skipLocal, dryRun}) { + console.log(`\n[${skipLocal ? '1/3' : '2/4'}] ${emoji.get('mag')} Reconciling plugins...`.yellow); const {linkable, fetchable} = reconcilePackages(); - console.log(`[2/3] ${emoji.get('truck')} Fetching plugins...\n`.yellow); + console.log(`[${skipLocal ? '2/3' : '3/4'}] ${emoji.get('truck')} Fetching plugins...\n`.yellow); if (fetchable.length > 0) { @@ -111,7 +113,7 @@ async function reconcileRemotePlugins(dryRun) { // TODO fetch the plugins using yarn - console.log(`\n[3/3] ${emoji.get('link')} Linking plugins...\n`.yellow); + console.log(`\n[${skipLocal ? '3/3' : '4/4'}] ${emoji.get('link')} Linking plugins...\n`.yellow); for (let i in linkable) { let {name} = linkable[i]; @@ -131,33 +133,65 @@ async function reconcileRemotePlugins(dryRun) { return {linkable, fetchable}; } +async function reconcileLocalPlugins({skipRemote, dryRun}) { + console.log(`\n[${skipRemote ? '1/1' : '1/4'}] ${emoji.get('pick')} Installing local plugin dependencies...\n`.yellow); + const {local} = reconcilePackages(() => {}); + + 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(options) { +async function reconcilePluginDeps({skipLocal, skipRemote, dryRun}) { let startTime = new Date(); // We don't need to do anything if we skip everything.... - if (options.skipLocal && options.skipRemote) { + if (skipLocal && skipRemote) { return; } // Traverse local plugins and install dependencies if enabled. - if (!options.skipLocal) { - + if (!skipLocal) { + await reconcileLocalPlugins({skipRemote, dryRun}); } // Locate any external plugins and install them. - if (!options.skipRemote) { + if (!skipRemote) { let results = []; try { - results = await reconcileRemotePlugins(options.dryRun); + results = await reconcileRemotePlugins({skipLocal, skipRemote, dryRun}); } catch (e) { throw e; } let status; - if (options.dryRun) { + if (dryRun) { status = '[dry-run] success'.green; } else { status = 'success'.green; From c221dbb4a00b8bc38049003837e8a51dd6d2424f Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 31 Mar 2017 16:45:52 -0600 Subject: [PATCH 08/17] Added credits --- bin/cli-plugins | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bin/cli-plugins b/bin/cli-plugins index 319c046b3..ecffd6f0b 100755 --- a/bin/cli-plugins +++ b/bin/cli-plugins @@ -4,6 +4,9 @@ * Module dependencies. */ +// Interface heavily inspired by the yarn package manager: +// https://yarnpkg.com/ + const program = require('./commander'); // Make things colorful! From d4c7dac945aa602e5526e6b6a98eaa55a9982b7c Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 3 Apr 2017 08:53:40 -0600 Subject: [PATCH 09/17] Adjusted plugins.json format, added docs --- PLUGINS.md | 24 +++++++++++++++++++++++- plugins.js | 38 ++++++++++++++++++++++++-------------- 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/PLUGINS.md b/PLUGINS.md index a77b0b8b2..e303883db 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -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,28 @@ 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._ + ## Server Plugins ### Specification diff --git a/plugins.js b/plugins.js index 116e9093c..945f6e299 100644 --- a/plugins.js +++ b/plugins.js @@ -51,36 +51,46 @@ function pluginPath(name) { * Stores a reference to a section for a section of Plugins. */ class PluginSection { - constructor(plugin_names) { - this.plugins = Object.keys(plugin_names).map((plugin_name) => { + constructor(plugins) { + this.plugins = plugins.map((p) => { let plugin = {}; - // Get the version/folder that was specified. - plugin.version = plugin_names[plugin_name]; + // 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; + } 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); + plugin.path = pluginPath(plugin.name); if (typeof plugin.path === 'undefined') { - throw new Error(`plugin '${plugin_name}' is not local or is not symlinked from your node_modules directory, plugin reconsiliation may be required`); + throw new Error(`plugin '${plugin.name}' is not local or is not symlinked from your node_modules directory, plugin reconsiliation may be required`); } try { plugin.module = require(plugin.path); } catch (e) { - throw new Error(`plugin '${plugin_name}' could not be required from '${plugin.path}': ${e.message}`); + throw new Error(`plugin '${plugin.name}' could not be required from '${plugin.path}': ${e.message}`); } - if (isInternal(plugin_name)) { - debug(`loading internal plugin '${plugin_name}' from '${plugin.path}'`); + if (isInternal(plugin.name)) { + debug(`loading internal plugin '${plugin.name}' from '${plugin.path}'`); } else { - debug(`loading external plugin '${plugin_name}' from '${plugin.path}'`); + debug(`loading external plugin '${plugin.name}' from '${plugin.path}'`); } - // Ensure we have a default plugin name, but allow the name to be - // overrided by the plugin. - plugin.name = plugin.name || plugin_name; - return plugin; }); } From 881f79a5719f8c7d68fbfd5208744fd32e2169c3 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 3 Apr 2017 08:59:28 -0600 Subject: [PATCH 10/17] Adjusted error handling --- plugins.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins.js b/plugins.js index 945f6e299..6b506f5aa 100644 --- a/plugins.js +++ b/plugins.js @@ -82,7 +82,13 @@ class PluginSection { try { plugin.module = require(plugin.path); } catch (e) { - throw new Error(`plugin '${plugin.name}' could not be required from '${plugin.path}': ${e.message}`); + 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)) { From 9c806cb6e4bef3d68df85c785c5304f036fbdb9a Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 3 Apr 2017 11:58:54 -0600 Subject: [PATCH 11/17] Added support for plugin upgrades --- bin/cli-plugins | 150 ++++++++++++++++++++++++++++++++---------------- package.json | 1 + plugins.js | 66 ++++++++++++--------- 3 files changed, 141 insertions(+), 76 deletions(-) diff --git a/bin/cli-plugins b/bin/cli-plugins index ecffd6f0b..94ce74904 100755 --- a/bin/cli-plugins +++ b/bin/cli-plugins @@ -17,8 +17,9 @@ 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, isInternal} = require('../plugins'); +const {plugins, itteratePlugins, isInternal} = require('../plugins'); function existsInNodeModules(name) { try { @@ -30,20 +31,48 @@ function existsInNodeModules(name) { } } +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(log = console.log) { - const linkable = []; +function reconcilePackages({quiet = false, upgradeRemote = false}) { const fetchable = []; const local = []; + const upgradable = []; - log(); - log(' +local (l) packages in your project\n +external (e) referenced packages in node_modules but not in current project\n +missing (m) referenced packages but not found\n +symlinked (sl) symlinked external packages\n'); + 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 = plugins[i]; + let section = itteratePlugins(plugins[i]); - for (let name in section) { - let version = section[name]; + for (let j in section) { + let {name, version} = section[j]; let namespaced = name.charAt(0) === '@'; let dep = name.split('/') @@ -56,36 +85,59 @@ function reconcilePackages(log = console.log) { } if (isInternal(dep)) { - let stat = fs.lstatSync(path.join(dir, 'plugins', name)); - if (stat.isSymbolicLink()) { - log(` sl ${name.cyan}`); - } else { - log(` l ${name.cyan}`); - local.push({name, version}); + if (!quiet) { + console.log(` l ${name}`); } + local.push({name, version}); continue; } if (!existsInNodeModules(dep)) { - log(` m ${name.cyan}`); + 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 { - log(` e ${name.cyan}`); - linkable.push({name, version}); + if (!quiet) { + console.log(` e ${name}`); + } + + if (upgradeRemote) { + upgradable.push({name, version}); + } } } } - log(); - return {local, linkable, fetchable}; + if (!quiet) { + console.log(); + } + + return {local, fetchable, upgradable}; } -async function reconcileRemotePlugins({skipLocal, dryRun}) { - console.log(`\n[${skipLocal ? '1/3' : '2/4'}] ${emoji.get('mag')} Reconciling plugins...`.yellow); - const {linkable, fetchable} = reconcilePackages(); +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/3' : '3/4'}] ${emoji.get('truck')} Fetching plugins...\n`.yellow); + console.log(`[${skipLocal ? '2/2' : '3/3'}] ${emoji.get('truck')} Fetching plugins...\n`.yellow); if (fetchable.length > 0) { @@ -108,37 +160,36 @@ async function reconcileRemotePlugins({skipLocal, dryRun}) { console.log(output.stdout.toString()); } - - fetchable.forEach((plugin) => { - linkable.push(plugin); - }); } - // TODO fetch the plugins using yarn - - console.log(`\n[${skipLocal ? '3/3' : '4/4'}] ${emoji.get('link')} Linking plugins...\n`.yellow); - - for (let i in linkable) { - let {name} = linkable[i]; - - let src = path.join(dir, 'node_modules', name); - let dst = path.join(dir, 'plugins', name); - - console.log(`$ link ${src.cyan} -> ${dst.cyan}`); + if (upgradable.length > 0) { + console.log(`$ yarn upgrade ${upgradable.map(({name, version}) => `${name}@${version}`.cyan)}`); if (!dryRun) { - // Create the symlink for the plugin directory. - fs.symlinkSync(src, dst, 'dir'); + 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 {linkable, fetchable}; + return {upgradable, fetchable}; } async function reconcileLocalPlugins({skipRemote, dryRun}) { - console.log(`\n[${skipRemote ? '1/1' : '1/4'}] ${emoji.get('pick')} Installing local plugin dependencies...\n`.yellow); - const {local} = reconcilePackages(() => {}); + 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]; @@ -171,7 +222,7 @@ async function reconcileLocalPlugins({skipRemote, dryRun}) { // 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}) { +async function reconcilePluginDeps({skipLocal, skipRemote, dryRun, upgradeRemote}) { let startTime = new Date(); // We don't need to do anything if we skip everything.... @@ -188,7 +239,7 @@ async function reconcilePluginDeps({skipLocal, skipRemote, dryRun}) { if (!skipRemote) { let results = []; try { - results = await reconcileRemotePlugins({skipLocal, skipRemote, dryRun}); + results = await reconcileRemotePlugins({skipLocal, skipRemote, dryRun, upgradeRemote}); } catch (e) { throw e; } @@ -201,14 +252,14 @@ async function reconcilePluginDeps({skipLocal, skipRemote, dryRun}) { } let message; - if (results.linkable.length === 0 && results.fetchable.length === 0) { + if (results.upgradable.length === 0 && results.fetchable.length === 0) { message = 'Already up-to-date.'; - } else if (results.linkable.length === 0) { + } else if (results.upgradable.length === 0) { message = `Fetched ${results.fetchable.length} new plugins.`; } else if (results.fetchable.length === 0) { - message = `Linked ${results.linkable.length} new plugins.`; + message = `Upgraded ${results.upgradable.length} new plugins.`; } else { - message = `Fetched ${results.fetchable.length} new plugins, linked ${results.linkable.length} plugins.`; + message = `Fetched ${results.fetchable.length} new plugins, upgraded ${results.upgradable.length} plugins.`; } console.log(`\n${status} ${message}`); @@ -232,6 +283,7 @@ program 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') diff --git a/package.json b/package.json index 6c0affbba..f29351b3e 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "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" }, diff --git a/plugins.js b/plugins.js index 6b506f5aa..4fae45d5a 100644 --- a/plugins.js +++ b/plugins.js @@ -1,5 +1,6 @@ const fs = require('fs'); const path = require('path'); +const resolve = require('resolve'); const debug = require('debug')('talk:plugins'); let plugins = {}; @@ -43,8 +44,40 @@ function pluginPath(name) { return path.join(__dirname, 'plugins', name); } - // The plugin is not available. - return undefined; + 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; + }); } /** @@ -52,31 +85,9 @@ function pluginPath(name) { */ class PluginSection { constructor(plugins) { - this.plugins = 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; - } 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); - + this.plugins = itteratePlugins(plugins).map((plugin) => { if (typeof plugin.path === 'undefined') { - throw new Error(`plugin '${plugin.name}' is not local or is not symlinked from your node_modules directory, plugin reconsiliation may be required`); + throw new Error(`plugin '${plugin.name}' is not local and is not resolvable, plugin reconsiliation may be required`); } try { @@ -151,5 +162,6 @@ module.exports = { plugins, PluginManager, isInternal, - pluginPath + pluginPath, + itteratePlugins }; From b5280b122114eb63e007aaf642b4bb96109286c2 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 3 Apr 2017 13:02:32 -0600 Subject: [PATCH 12/17] Added support for plugin relative imports --- PLUGINS.md | 14 ++++++++++++++ package.json | 1 + plugins.js | 3 +++ yarn.lock | 20 ++++++-------------- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/PLUGINS.md b/PLUGINS.md index e303883db..e31a7d0e5 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -44,6 +44,20 @@ 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 additioal 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 diff --git a/package.json b/package.json index f29351b3e..5dcb93bcc 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ }, "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", diff --git a/plugins.js b/plugins.js index 4fae45d5a..c53ed1886 100644 --- a/plugins.js +++ b/plugins.js @@ -3,6 +3,9 @@ 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 = {}; // Try to parse the plugins.json file, logging out an error if the plugins.json diff --git a/yarn.lock b/yarn.lock index 0093a66b7..a0420ab91 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -6512,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: @@ -6586,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: From 368838df98760fda624be01699456221936ba369 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 3 Apr 2017 15:16:05 -0600 Subject: [PATCH 13/17] Spelling --- PLUGINS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PLUGINS.md b/PLUGINS.md index e31a7d0e5..eca4e911e 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -53,7 +53,7 @@ project root. An example could be: const cache = require('services/cache'); ``` -You may also include additioal external depenancies in your local packages by +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. From febe264320030c8e6beee72d03dbae2d8812488b Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 3 Apr 2017 15:31:51 -0600 Subject: [PATCH 14/17] Added route support --- plugins.js | 5 ++++- routes/index.js | 10 ++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/plugins.js b/plugins.js index c53ed1886..3060008d5 100644 --- a/plugins.js +++ b/plugins.js @@ -122,7 +122,10 @@ class PluginSection { hook(hook) { return this.plugins .filter(({module}) => hook in module) - .map((plugin) => ({plugin, [hook]: module[hook]})); + .map((plugin) => ({ + plugin, + [hook]: plugin.module[hook] + })); } } diff --git a/routes/index.js b/routes/index.js index 58c8f13d7..f6745b1c7 100644 --- a/routes/index.js +++ b/routes/index.js @@ -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', 'routes').forEach(({plugin, routes}) => { + debug(`added plugin '${plugin.name}'`); + + // Pass the root router to the plugin to mount it's routes. + routes(router); +}); + module.exports = router; From c5098a3e91407866f5409ce94ed1a3791c186aee Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 3 Apr 2017 15:37:31 -0600 Subject: [PATCH 15/17] Added docs --- PLUGINS.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/PLUGINS.md b/PLUGINS.md index eca4e911e..cca319abd 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -206,6 +206,33 @@ 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'}]}); + }); + } +} +``` + ### Full Example Contents of `plugins.json`: From 8d3524dd611751a812ea1ac508364c42f92ca2ee Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 3 Apr 2017 16:06:38 -0600 Subject: [PATCH 16/17] Added support for auth based plugins --- PLUGINS.md | 52 ++++++++++++++++++++++++++++++++ app.js | 2 +- routes/api/auth/index.js | 50 +------------------------------ services/passport.js | 64 +++++++++++++++++++++++++++++++++++++++- 4 files changed, 117 insertions(+), 51 deletions(-) diff --git a/PLUGINS.md b/PLUGINS.md index cca319abd..819066217 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -233,6 +233,58 @@ module.exports = { } ``` +#### Field: `auth` + +```js +const FacebookStrategy = require('passport-facebook').Strategy; +const UsersService = require('services/users'); +const {ValidateUserLogin, HandleAuthPopupCallback} = require('services/passport'); + +module.exports = { + auth(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); + })); + }, + routes(router) { + 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`: diff --git a/app.js b/app.js index 5b704bea0..3ae03ec72 100644 --- a/app.js +++ b/app.js @@ -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); diff --git a/routes/api/auth/index.js b/routes/api/auth/index.js index fc3b87e51..12a3e8bbe 100644 --- a/routes/api/auth/index.js +++ b/routes/api/auth/index.js @@ -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 */ diff --git a/services/passport.js b/services/passport.js index f1994392f..2d6604c3b 100644 --- a/services/passport.js +++ b/services/passport.js @@ -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', 'auth').forEach(({plugin, auth}) => { + debug(`added plugin '${plugin.name}'`); + + // Pass the passport.js instance to the plugin to allow it to inject it's + // functionality. + auth(passport); +}); + +module.exports = { + passport, + ValidateUserLogin, + HandleFailedAttempt, + HandleAuthCallback, + HandleAuthPopupCallback +}; From c1dac11a2bb28e4b65ed02595fcb23dbeff4b87a Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 3 Apr 2017 16:11:34 -0600 Subject: [PATCH 17/17] Updated docs, hook names --- PLUGINS.md | 9 ++++++--- routes/index.js | 6 +++--- services/passport.js | 6 +++--- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/PLUGINS.md b/PLUGINS.md index 819066217..e5cf8d760 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -233,7 +233,7 @@ module.exports = { } ``` -#### Field: `auth` +#### Field: `passport` ```js const FacebookStrategy = require('passport-facebook').Strategy; @@ -241,7 +241,7 @@ const UsersService = require('services/users'); const {ValidateUserLogin, HandleAuthPopupCallback} = require('services/passport'); module.exports = { - auth(passport) { + passport(passport) { passport.use(new FacebookStrategy({ clientID: process.env.TALK_FACEBOOK_APP_ID, clientSecret: process.env.TALK_FACEBOOK_APP_SECRET, @@ -260,7 +260,10 @@ module.exports = { return ValidateUserLogin(profile, user, done); })); }, - routes(router) { + 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'); /** diff --git a/routes/index.js b/routes/index.js index f6745b1c7..9e38097f9 100644 --- a/routes/index.js +++ b/routes/index.js @@ -25,11 +25,11 @@ if (process.env.NODE_ENV !== 'production') { } // Inject server route plugins. -plugins.get('server', 'routes').forEach(({plugin, routes}) => { - debug(`added plugin '${plugin.name}'`); +plugins.get('server', 'router').forEach((plugin) => { + debug(`added plugin '${plugin.plugin.name}'`); // Pass the root router to the plugin to mount it's routes. - routes(router); + plugin.router(router); }); module.exports = router; diff --git a/services/passport.js b/services/passport.js index 2d6604c3b..09ded01e8 100644 --- a/services/passport.js +++ b/services/passport.js @@ -377,12 +377,12 @@ if (process.env.TALK_FACEBOOK_APP_ID && process.env.TALK_FACEBOOK_APP_SECRET && } // Inject server route plugins. -plugins.get('server', 'auth').forEach(({plugin, auth}) => { - debug(`added plugin '${plugin.name}'`); +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. - auth(passport); + plugin.passport(passport); }); module.exports = {