diff --git a/.eslintignore b/.eslintignore
index d506a20c2..fc0d50a25 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -3,4 +3,5 @@ client/lib
**/*.html
plugins/*
!plugins/coral-plugin-facebook-auth
-node_modules
\ No newline at end of file
+!plugins/coral-plugin-respect
+node_modules
diff --git a/.gitignore b/.gitignore
index 6b7b187d9..8982613ac 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,4 +16,5 @@ coverage/
plugins.json
plugins/*
-!plugins/coral-plugin-facebook-auth
\ No newline at end of file
+!plugins/coral-plugin-facebook-auth
+!plugins/coral-plugin-respect
diff --git a/.nodemon.json b/.nodemon.json
index 4c48c707e..7f7fd3d59 100644
--- a/.nodemon.json
+++ b/.nodemon.json
@@ -1,5 +1,5 @@
{
"verbose": true,
- "ignore": ["test/*", "client/*", "dist/*"],
+ "ignore": ["test/*", "client/*", "dist/*", "plugins/*/client"],
"ext": "js,json,graphql"
}
diff --git a/client/coral-docs/src/index.js b/client/coral-docs/src/index.js
index 5d8c49d9d..e706203a8 100644
--- a/client/coral-docs/src/index.js
+++ b/client/coral-docs/src/index.js
@@ -1,8 +1,8 @@
import React from 'react';
-import ReactDOM from 'react-dom';
+import {render} from 'react-dom';
import {GraphQLDocs} from 'graphql-docs';
import fetcher from './services/fetcher';
// Render the application into the DOM
-ReactDOM.render(, document.querySelector('#root'));
+render(, document.querySelector('#root'));
diff --git a/client/coral-embed-stream/src/Comment.js b/client/coral-embed-stream/src/Comment.js
index d970a5f35..45f2ef084 100644
--- a/client/coral-embed-stream/src/Comment.js
+++ b/client/coral-embed-stream/src/Comment.js
@@ -19,6 +19,7 @@ import FlagComment from 'coral-plugin-flags/FlagComment';
import LikeButton from 'coral-plugin-likes/LikeButton';
import {BestButton, IfUserCanModifyBest, BEST_TAG, commentIsBest, BestIndicator} from 'coral-plugin-best/BestButton';
import LoadMore from 'coral-embed-stream/src/LoadMore';
+import {Slot} from 'coral-framework';
import styles from './Comment.css';
@@ -157,6 +158,7 @@ class Comment extends React.Component {
?
: null }
+
@@ -187,6 +189,7 @@ class Comment extends React.Component {
removeBest={removeBestTag} />
+
@@ -241,7 +244,8 @@ class Comment extends React.Component {
showSignInDialog={showSignInDialog}
reactKey={reply.id}
key={reply.id}
- comment={reply} />;
+ comment={reply}
+ />;
})
}
{
diff --git a/client/coral-embed-stream/src/Embed.js b/client/coral-embed-stream/src/Embed.js
index 016536708..d9b5c8310 100644
--- a/client/coral-embed-stream/src/Embed.js
+++ b/client/coral-embed-stream/src/Embed.js
@@ -313,5 +313,5 @@ export default compose(
addCommentTag,
removeCommentTag,
deleteAction,
- queryStream
+ queryStream,
)(Embed);
diff --git a/client/coral-embed-stream/src/Stream.js b/client/coral-embed-stream/src/Stream.js
index 34145ed4d..66c19e0ab 100644
--- a/client/coral-embed-stream/src/Stream.js
+++ b/client/coral-embed-stream/src/Stream.js
@@ -41,7 +41,8 @@ class Stream extends React.Component {
deleteAction,
showSignInDialog,
addCommentTag,
- removeCommentTag
+ removeCommentTag,
+ pluginProps
} = this.props;
return (
@@ -67,7 +68,9 @@ class Stream extends React.Component {
showSignInDialog={showSignInDialog}
key={comment.id}
reactKey={comment.id}
- comment={comment} />
+ comment={comment}
+ pluginProps={pluginProps}
+ />
)
}
diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css
index 0af50b2e2..2ef617593 100644
--- a/client/coral-embed-stream/style/default.css
+++ b/client/coral-embed-stream/style/default.css
@@ -255,13 +255,13 @@ hr {
.commentActionsRight, .replyActionsRight {
display: flex;
justify-content: flex-end;
- width: 50%;
+ width: 30%;
}
.commentActionsLeft, .replyActionsLeft {
display: flex;
justify-content: flex-start;
float: left;
- width: 50%;
+ width: 70%;
}
.comment__action-container .material-icons {
diff --git a/client/coral-framework/actions/asset.js b/client/coral-framework/actions/asset.js
index fa927e58a..50efa3dcb 100644
--- a/client/coral-framework/actions/asset.js
+++ b/client/coral-framework/actions/asset.js
@@ -3,7 +3,7 @@ import coralApi from '../helpers/response';
import {addNotification} from '../actions/notification';
import {pym} from 'coral-framework';
-import I18n from '../../coral-framework/modules/i18n/i18n';
+import I18n from 'coral-framework/modules/i18n/i18n';
import translations from './../translations';
const lang = new I18n(translations);
diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js
index 01174cf05..11e7bd996 100644
--- a/client/coral-framework/actions/auth.js
+++ b/client/coral-framework/actions/auth.js
@@ -1,3 +1,5 @@
+import {gql} from 'react-apollo';
+import client from 'coral-framework/services/client';
import I18n from '../../coral-framework/modules/i18n/i18n';
import translations from './../translations';
const lang = new I18n(translations);
@@ -5,6 +7,20 @@ import * as actions from '../constants/auth';
import coralApi, {base} from '../helpers/response';
import {pym} from 'coral-framework';
+const ME_QUERY = gql`
+ query Me {
+ me {
+ status
+ }
+ }
+`;
+
+function fetchMe() {
+ return client.query({
+ fetchPolicy: 'network-only',
+ query: ME_QUERY});
+}
+
// Dialog Actions
export const showSignInDialog = (offset = 0) => ({type: actions.SHOW_SIGNIN_DIALOG, offset});
export const hideSignInDialog = () => ({type: actions.HIDE_SIGNIN_DIALOG});
@@ -52,6 +68,7 @@ export const fetchSignIn = (formData) => (dispatch) => {
const isAdmin = !!user && !!user.roles.filter(i => i === 'ADMIN').length;
dispatch(signInSuccess(user, isAdmin));
dispatch(hideSignInDialog());
+ fetchMe();
})
.catch(error => {
if (error.metadata) {
@@ -104,6 +121,7 @@ export const facebookCallback = (err, data) => dispatch => {
dispatch(signInFacebookSuccess(user));
dispatch(hideSignInDialog());
dispatch(showCreateUsernameDialog());
+ fetchMe();
} catch (err) {
dispatch(signInFacebookFailure(err));
return;
@@ -151,7 +169,10 @@ const logOutFailure = () => ({type: actions.LOGOUT_FAILURE});
export const logout = () => dispatch => {
dispatch(logOutRequest());
return coralApi('/auth', {method: 'DELETE'})
- .then(() => dispatch(logOutSuccess()))
+ .then(() => {
+ dispatch(logOutSuccess());
+ fetchMe();
+ })
.catch(error => dispatch(logOutFailure(error)));
};
diff --git a/client/coral-framework/actions/index.js b/client/coral-framework/actions/index.js
new file mode 100644
index 000000000..65f7b87d3
--- /dev/null
+++ b/client/coral-framework/actions/index.js
@@ -0,0 +1,9 @@
+import * as authActions from './auth';
+import * as assetActions from './asset';
+import * as notificationActions from './notification';
+
+export default {
+ authActions,
+ assetActions,
+ notificationActions,
+};
diff --git a/client/coral-framework/components/Slot.js b/client/coral-framework/components/Slot.js
new file mode 100644
index 000000000..3d1d61328
--- /dev/null
+++ b/client/coral-framework/components/Slot.js
@@ -0,0 +1,19 @@
+import React, {Component} from 'react';
+import {getSlotElements} from 'coral-framework/helpers/plugins';
+
+class Slot extends Component {
+ render() {
+ const {fill, ...rest} = this.props;
+ return (
+
+ {getSlotElements(fill, rest)}
+
+ );
+ }
+}
+
+Slot.propTypes = {
+ fill: React.PropTypes.string
+};
+
+export default Slot;
diff --git a/client/coral-framework/helpers/plugins.js b/client/coral-framework/helpers/plugins.js
new file mode 100644
index 000000000..e0d345db4
--- /dev/null
+++ b/client/coral-framework/helpers/plugins.js
@@ -0,0 +1,21 @@
+import React from 'react';
+import merge from 'lodash/merge';
+import flatten from 'lodash/flatten';
+import plugins from 'pluginsConfig';
+
+export const pluginReducers = merge(
+ ...plugins
+ .filter(o => o.module.reducer)
+ .map(o => ({...o.module.reducer}))
+);
+
+/**
+ * Returns React Elements for given slot.
+ */
+export function getSlotElements(slot, props = {}) {
+ const components = flatten(plugins
+ .filter(o => o.module.slots[slot])
+ .map(o => o.module.slots[slot]));
+ return components
+ .map((component, i) => React.createElement(component, {...props, key: i}));
+}
diff --git a/client/coral-framework/index.js b/client/coral-framework/index.js
index 2900466ee..96750bffd 100644
--- a/client/coral-framework/index.js
+++ b/client/coral-framework/index.js
@@ -1,15 +1,16 @@
import store from './services/store';
import pym from './services/PymConnection';
import I18n from './modules/i18n/i18n';
-import * as authActions from './actions/auth';
-import * as assetActions from './actions/asset';
-import * as notificationActions from './actions/notification';
+import actions from './actions';
+import Slot from './components/Slot';
-export {
+// TODO (bc): Deprecate old actions. Spreading actions is now needed.
+
+export default {
pym,
+ Slot,
I18n,
store,
- authActions,
- assetActions,
- notificationActions
+ actions,
+ ...actions
};
diff --git a/client/coral-framework/loaders/plugins-loader.js b/client/coral-framework/loaders/plugins-loader.js
new file mode 100644
index 000000000..c3534df32
--- /dev/null
+++ b/client/coral-framework/loaders/plugins-loader.js
@@ -0,0 +1,33 @@
+/**
+ * Executes `source` to retrieve plugins configuration
+ * and loads the `index.js` of specified plugins.
+ *
+ * Outputs a module that looks like the following:
+ *
+ * module.exports = [{plugin: string, module: object}, ...]
+ *
+ */
+const {stripIndent} = require('common-tags');
+
+function getPluginList(config) {
+ if (config && config.client) {
+ return config.client.map(x => typeof x === 'string' ? x : Object.keys(x)[0]);
+ }
+
+ return [];
+}
+
+module.exports = function(source) {
+ this.cacheable();
+ const config = this.exec(source, this.resourcePath);
+ const plugins = getPluginList(config).map((plugin) => `{
+ module: require('${plugin}/client'),
+ plugin: '${plugin}'
+ }`);
+
+ return stripIndent`
+ module.exports = [
+ ${plugins.join(',')}
+ ];
+ `;
+};
diff --git a/client/coral-framework/reducers/index.js b/client/coral-framework/reducers/index.js
index 9e98c901a..fae7e60d5 100644
--- a/client/coral-framework/reducers/index.js
+++ b/client/coral-framework/reducers/index.js
@@ -1,9 +1,11 @@
import auth from './auth';
import user from './user';
import asset from './asset';
+import {pluginReducers} from '../helpers/plugins';
export default {
auth,
user,
asset,
+ ...pluginReducers
};
diff --git a/client/coral-framework/services/client.js b/client/coral-framework/services/client.js
index b4a7a38df..949d37fee 100644
--- a/client/coral-framework/services/client.js
+++ b/client/coral-framework/services/client.js
@@ -6,9 +6,11 @@ export const client = new ApolloClient({
queryTransformer: addTypename,
dataIdFromObject: (result) => {
if (result.id && result.__typename) { // eslint-disable-line no-underscore-dangle
- return result.__typename + result.id; // eslint-disable-line no-underscore-dangle
+ return `${result.__typename}_${result.id}`; // eslint-disable-line no-underscore-dangle
}
return null;
},
networkInterface: getNetworkInterface()
});
+
+export default client;
diff --git a/client/coral-framework/utils/index.js b/client/coral-framework/utils/index.js
new file mode 100644
index 000000000..6d589cf23
--- /dev/null
+++ b/client/coral-framework/utils/index.js
@@ -0,0 +1,7 @@
+/**
+* getActionSummary
+* retrieves the action summary based on the type and the comment
+*/
+
+export const getActionSummary = (type, comment) =>
+ comment.action_summaries.filter(a => a.__typename === type)[0];
diff --git a/graph/helpers/response.js b/graph/helpers/response.js
new file mode 100644
index 000000000..ebdbc02ec
--- /dev/null
+++ b/graph/helpers/response.js
@@ -0,0 +1,31 @@
+const errors = require('../../errors');
+const {Error: {ValidationError}} = require('mongoose');
+
+/**
+ * Wraps up a promise to return an object with the resolution of the promise
+ * keyed at `key` or an error caught at `errors`.
+ */
+
+const wrapResponse = (key) => (promise) => {
+ return promise.then((value) => {
+ let res = {};
+ if (key) {
+ res[key] = value;
+ }
+ return res;
+ }).catch((err) => {
+ if (err instanceof errors.APIError) {
+ return {
+ errors: [err]
+ };
+ } else if (err instanceof ValidationError) {
+
+ // TODO: wrap this with one of our internal errors.
+ throw err;
+ }
+
+ throw err;
+ });
+};
+
+module.exports = wrapResponse;
diff --git a/graph/hooks.js b/graph/hooks.js
index 3fe1306cf..499eb60b1 100644
--- a/graph/hooks.js
+++ b/graph/hooks.js
@@ -1,4 +1,7 @@
-const {forEachField} = require('graphql-tools');
+const {
+ GraphQLObjectType,
+ GraphQLInterfaceType
+} = require('graphql');
const debug = require('debug')('talk:graph:schema');
/**
@@ -22,6 +25,33 @@ const defaultResolveFn = (source, args, context, {fieldName}) => {
}
};
+// This function is pretty much copied verbatim from the graphql-tools repo:
+// https://github.com/apollographql/graphql-tools/blob/b12973c86e00be209d04af0184780998056051c4/src/schemaGenerator.ts#L180-L194
+// With the small alteration that we look for the `resolveType` function on the
+// schema so we can wrap post hooks around it to provide additional resolve
+// points.
+const forEachField = (schema, fn) => {
+ const typeMap = schema.getTypeMap();
+ Object.keys(typeMap).forEach((typeName) => {
+ const type = typeMap[typeName];
+
+ if (type instanceof GraphQLObjectType || type instanceof GraphQLInterfaceType) {
+
+ // Here we capture the change to extract the resolve type. We pass this
+ // with the `isResolveType = true` to introduce the specific beheviour.
+ if ('resolveType' in type) {
+ fn(type, typeName, '__resolveType', true);
+ }
+
+ const fields = type.getFields();
+ Object.keys(fields).forEach((fieldName) => {
+ const field = fields[fieldName];
+ fn(field, typeName, fieldName);
+ });
+ }
+ });
+};
+
/**
* Decorates the schema with pre and post hooks as provided by the Plugin
* Manager.
@@ -29,7 +59,7 @@ const defaultResolveFn = (source, args, context, {fieldName}) => {
* @param {Array} hooks hooks to apply to the schema
* @return {void}
*/
-const decorateWithHooks = (schema, hooks) => forEachField(schema, (field, typeName, fieldName) => {
+const decorateWithHooks = (schema, hooks) => forEachField(schema, (field, typeName, fieldName, isResolveType = false) => {
// Pull out the pre/post hooks from the available hooks.
const {
@@ -85,6 +115,48 @@ const decorateWithHooks = (schema, hooks) => forEachField(schema, (field, typeNa
return;
}
+ // If this is a resolve type, we need to do some specific things to handle
+ // this type of field.
+ if (isResolveType) {
+
+ // Warn if we have any pre hooks.
+ if (pre.length !== 0) {
+ throw new Error(`invalid pre hooks were found for ${typeName}.${fieldName}, only post hooks are supported on the __resolveType hook`);
+ }
+
+ // This only needs to do something if post hooks are defined.
+ if (post.length === 0) {
+ return;
+ }
+
+ // Cache the original resolverType function.
+ let resolveType = field.resolveType;
+
+ // Return the function to handle the resolveType hooks.
+ field.resolveType = (obj, context, info) => {
+ let type = resolveType(obj, context, info);
+
+ // Only if a previous resolver was unable to resolve the field type do we
+ // progress to the hooks (in order!) to resolve the field name until we
+ // have resolved it.
+ if (typeof type !== 'undefined' && type != null) {
+ return type;
+ }
+
+ // We will walk through the post hooks until we find the right one. This
+ // follows what redux does to combine existing reducers.
+ for (let i = 0; i < post.length; i++) {
+ let resolveType = post[i];
+ type = resolveType(obj, context, info);
+ if (typeof type !== 'undefined' && type != null) {
+ return type;
+ }
+ }
+ };
+
+ return;
+ }
+
// Cache the original resolve function, this emulates the beheviour found in
// graphql-tools: https://github.com/apollographql/graphql-tools/blob/6e9cc124b10d673448386041e6c3d058bc205a02/src/schemaGenerator.ts#L423-L425
let resolve = field.resolve;
@@ -102,7 +174,7 @@ const decorateWithHooks = (schema, hooks) => forEachField(schema, (field, typeNa
await Promise.all(pre.map((pre) => pre(obj, args, context, info)));
// Resolve the field.
- let result = resolve(obj, args, context, info);
+ let result = await resolve(obj, args, context, info);
// Insure all post hooks after we've resolved the field with the result
// passed in as the fifth argument.
diff --git a/graph/resolvers/root_mutation.js b/graph/resolvers/root_mutation.js
index a474407d2..3d5ea9ad9 100644
--- a/graph/resolvers/root_mutation.js
+++ b/graph/resolvers/root_mutation.js
@@ -1,33 +1,6 @@
-const {Error: {ValidationError}} = require('mongoose');
-const errors = require('../../errors');
+const wrapResponse = require('../helpers/response');
const CommentsService = require('../../services/comments');
-/**
- * Wraps up a promise to return an object with the resolution of the promise
- * keyed at `key` or an error caught at `errors`.
- */
-const wrapResponse = (key) => (promise) => {
- return promise.then((value) => {
- let res = {};
- if (key) {
- res[key] = value;
- }
- return res;
- }).catch((err) => {
- if (err instanceof errors.APIError) {
- return {
- errors: [err]
- };
- } else if (err instanceof ValidationError) {
-
- // TODO: wrap this with one of our internal errors.
- throw err;
- }
-
- throw err;
- });
-};
-
const RootMutation = {
createComment(_, {asset_id, parent_id, body}, {mutators: {Comment}}) {
return wrapResponse('comment')(Comment.create({asset_id, parent_id, body}));
diff --git a/package.json b/package.json
index 3ea18dfcd..f4e2eb725 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
"postinstall": "./bin/cli plugins reconcile --skip-remote",
"start": "./bin/cli serve --jobs",
"dev-start": "nodemon --config .nodemon.json --exec \"./bin/cli -c .env serve --jobs\"",
- "build": "NODE_ENV=production webpack --progress -p --config webpack.config.js --bail",
+ "build": "NODE_ENV=production webpack -p --config webpack.config.js --bail",
"build-watch": "NODE_ENV=development webpack --progress --config webpack.config.js --watch",
"lint": "eslint bin/* .",
"lint-fix": "eslint bin/* . --fix",
@@ -74,6 +74,7 @@
"graphql-tools": "^0.9.0",
"helmet": "^3.5.0",
"inquirer": "^3.0.6",
+ "joi": "^10.4.1",
"jsonwebtoken": "^7.3.0",
"kue": "^0.11.5",
"linkify-it": "^2.0.3",
@@ -90,7 +91,7 @@
"parse-duration": "^0.1.1",
"passport": "^0.3.2",
"passport-local": "^1.0.0",
- "react-apollo": "^0.10.0",
+ "react-apollo": "^1.0.0",
"react-recaptcha": "^2.2.6",
"redis": "^2.7.1",
"resolve": "^1.3.2",
@@ -99,8 +100,9 @@
"uuid": "^2.0.3"
},
"devDependencies": {
- "apollo-client": "^0.8.3",
+ "apollo-client": "^1.0.0",
"autoprefixer": "^6.5.2",
+ "babel-cli": "^6.24.0",
"babel-core": "^6.24.0",
"babel-eslint": "^7.2.1",
"babel-jest": "^19.0.0",
@@ -114,10 +116,12 @@
"babel-plugin-transform-react-jsx": "^6.23.0",
"babel-polyfill": "^6.23.0",
"babel-preset-es2015": "^6.24.0",
+ "babel-preset-react": "^6.23.0",
"babel-preset-stage-0": "^6.16.0",
"chai": "^3.5.0",
"chai-as-promised": "^6.0.0",
"chai-http": "^3.0.0",
+ "common-tags": "^1.4.0",
"copy-webpack-plugin": "^4.0.0",
"css-loader": "^0.27.3",
"dialog-polyfill": "^0.4.4",
diff --git a/plugins.default.json b/plugins.default.json
index 40702cde9..b3443af48 100644
--- a/plugins.default.json
+++ b/plugins.default.json
@@ -1,5 +1,9 @@
{
"server": [
+ "coral-plugin-respect",
"coral-plugin-facebook-auth"
+ ],
+ "client": [
+ "coral-plugin-respect"
]
}
diff --git a/plugins.env.js b/plugins.env.js
new file mode 100644
index 000000000..7be520cc0
--- /dev/null
+++ b/plugins.env.js
@@ -0,0 +1 @@
+module.exports = JSON.parse(process.env.TALK_PLUGINS_JSON);
diff --git a/plugins.js b/plugins.js
index 43ac04478..02cdb58e1 100644
--- a/plugins.js
+++ b/plugins.js
@@ -2,9 +2,11 @@ const fs = require('fs');
const path = require('path');
const resolve = require('resolve');
const debug = require('debug')('talk:plugins');
+const Joi = require('joi');
+const amp = require('app-module-path');
-// Add support for require rewriting.
-require('app-module-path').addPath(__dirname);
+// Add the current path to the module root.
+amp.addPath(__dirname);
let plugins = {};
@@ -12,13 +14,13 @@ let plugins = {};
// file isn't loaded, but continuing. Else, like a parsing error, throw it and
// crash the program.
try {
- let defaultPlugins = path.join(__dirname, 'plugins.default.json');
+ let envPlugins = path.join(__dirname, 'plugins.env.js');
let customPlugins = path.join(__dirname, 'plugins.json');
- let envPluginJSON = process.env.TALK_PLUGINS_JSON;
+ let defaultPlugins = path.join(__dirname, 'plugins.default.json');
- if (envPluginJSON && envPluginJSON.length > 0) {
+ if (process.env.TALK_PLUGINS_JSON && process.env.TALK_PLUGINS_JSON.length > 0) {
debug('Now using TALK_PLUGINS_JSON environment variable for plugins');
- plugins = JSON.parse(envPluginJSON);
+ plugins = require(envPlugins);
} else if (fs.existsSync(customPlugins)) {
debug(`Now using ${customPlugins} for plugins`);
plugins = JSON.parse(fs.readFileSync(customPlugins, 'utf8'));
@@ -34,6 +36,23 @@ try {
}
}
+/**
+ * All the hooks from plugins must match the schema defined here.
+ */
+const hookSchemas = {
+ passport: Joi.func().arity(1),
+ router: Joi.func().arity(1),
+ context: Joi.object().pattern(/\w/, Joi.func().maxArity(1)),
+ hooks: Joi.object().pattern(/\w/, Joi.object().pattern(/(?:__resolveType|\w+)/, Joi.object({
+ pre: Joi.func(),
+ post: Joi.func()
+ }))),
+ loaders: Joi.object().pattern(/\w/, Joi.object().pattern(/\w/, Joi.func())),
+ mutators: Joi.object().pattern(/\w/, Joi.object().pattern(/\w/, Joi.func())),
+ resolvers: Joi.object().pattern(/\w/, Joi.object().pattern(/(?:__resolveType|\w+)/, Joi.func())),
+ typeDefs: Joi.string()
+};
+
/**
* isInternal checks to see if a given plugin is internal, and returns true
* if it is.
@@ -67,6 +86,12 @@ function pluginPath(name) {
}
}
+/**
+ * Itterates over the plugins and gets the plugin path's, version, and name.
+ *
+ * @param {Array