diff --git a/plugins.js b/plugins.js index f0c31407f..dfe3d3312 100644 --- a/plugins.js +++ b/plugins.js @@ -10,6 +10,7 @@ const PLUGINS_JSON = process.env.TALK_PLUGINS_JSON; // Add the current path to the module root. amp.addPath(__dirname); +let pluginsPath; let plugins = {}; // Try to parse the plugins.json file, logging out an error if the plugins.json @@ -22,14 +23,16 @@ try { if (PLUGINS_JSON && PLUGINS_JSON.length > 0) { debug('Now using TALK_PLUGINS_JSON environment variable for plugins'); - plugins = require(envPlugins); + pluginsPath = envPlugins; } else if (fs.existsSync(customPlugins)) { debug(`Now using ${customPlugins} for plugins`); - plugins = JSON.parse(fs.readFileSync(customPlugins, 'utf8')); + pluginsPath = customPlugins; } else { debug(`Now using ${defaultPlugins} for plugins`); - plugins = JSON.parse(fs.readFileSync(defaultPlugins, 'utf8')); + pluginsPath = defaultPlugins; } + + plugins = require(pluginsPath); } catch (err) { if (err.code === 'ENOENT') { console.error('plugins.json and plugins.default.json not found, plugins will not be active'); @@ -78,7 +81,12 @@ function isInternal(name) { */ function pluginPath(name) { if (isInternal(name)) { - return path.join(__dirname, 'plugins', name); + try { + return resolve.sync(name, {moduleDirectory: 'plugins', basedir: process.cwd()}); + } catch (e) { + console.warn(e); + return undefined; + } } try { @@ -88,15 +96,8 @@ function pluginPath(name) { } } -/** - * Itterates over the plugins and gets the plugin path's, version, and name. - * - * @param {Array} plugins - * @returns {Array} - */ -function itteratePlugins(plugins) { - return plugins.map((p) => { - let plugin = {}; +class Plugin { + constructor(entry) { // This checks to see if the structure for this entry is an object: // @@ -106,23 +107,47 @@ function itteratePlugins(plugins) { // // "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}`; + if (typeof entry === 'object') { + this.name = Object.keys(entry).find((name) => name !== null); + this.version = entry[this.name]; + } else if (typeof entry === 'string') { + this.name = entry; + this.version = `file:./plugins/${this.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}`); + 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 entry}`); } // Get the path for the plugin. - plugin.path = pluginPath(plugin.name); + this.path = pluginPath(this.name); + } - return plugin; - }); + require() { + if (typeof this.path === 'undefined') { + throw new Error(`plugin '${this.name}' is not local and is not resolvable, plugin reconsiliation may be required`); + } + + try { + this.module = require(this.path); + } catch (e) { + if (e && e.code && e.code === 'MODULE_NOT_FOUND' && isInternal(this.name)) { + console.error(new Error(`plugin '${this.name}' could not be loaded due to missing dependencies, plugin reconsiliation may be required`)); + throw e; + } + + console.error(new Error(`plugin '${this.name}' could not be required from '${this.path}': ${e.message}`)); + throw e; + } + } } +/** + * Itterates over the plugins and gets the plugin path's, version, and name. + * + * @param {Array} plugins + * @returns {Array} + */ +const itteratePlugins = (plugins) => plugins.map((p) => new Plugin(p)); + // Add each plugin folder to the allowed import path so that they can import our // internal dependancies. Object.keys(plugins).forEach((type) => itteratePlugins(plugins[type]).forEach((plugin) => { @@ -139,22 +164,20 @@ Object.keys(plugins).forEach((type) => itteratePlugins(plugins[type]).forEach((p */ class PluginSection { constructor(plugins) { - this.plugins = itteratePlugins(plugins).map((plugin) => { - if (typeof plugin.path === 'undefined') { - throw new Error(`plugin '${plugin.name}' is not local and is not resolvable, plugin reconsiliation may be required`); - } + this.required = false; + this.plugins = itteratePlugins(plugins); + } - try { - plugin.module = require(plugin.path); - } catch (e) { - if (e && e.code && e.code === 'MODULE_NOT_FOUND' && isInternal(plugin.name)) { - console.error(new Error(`plugin '${plugin.name}' could not be loaded due to missing dependencies, plugin reconsiliation may be required`)); - throw e; - } + require() { + if (this.required) { + return; + } + + this.required = true; + this.plugins.forEach((plugin) => { - console.error(new Error(`plugin '${plugin.name}' could not be required from '${plugin.path}': ${e.message}`)); - throw e; - } + // Load the plugin. + plugin.require(); if (isInternal(plugin.name)) { debug(`loading internal plugin '${plugin.name}' from '${plugin.path}'`); @@ -171,6 +194,10 @@ class PluginSection { * available. */ hook(hook) { + + // Load the plugin source if we haven't already. + this.require(); + return this.plugins .filter(({module}) => hook in module) .filter((plugin) => { @@ -226,6 +253,7 @@ class PluginManager { module.exports = { plugins, + pluginsPath, PluginManager, isInternal, pluginPath, diff --git a/webpack.config.js b/webpack.config.js index ce1e0bd29..130ced27b 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -3,6 +3,7 @@ const fs = require('fs'); const CompressionPlugin = require('compression-webpack-plugin'); const autoprefixer = require('autoprefixer'); const precss = require('precss'); +const _ = require('lodash'); const Copy = require('copy-webpack-plugin'); const LicenseWebpackPlugin = require('license-webpack-plugin'); const webpack = require('webpack'); @@ -10,23 +11,11 @@ const webpack = require('webpack'); // Possibly load the config from the .env file (if there is one). require('dotenv').config(); -let pluginsConfigPath; +const {plugins, pluginsPath, PluginManager} = require('./plugins'); +const manager = new PluginManager(plugins); +const targetPlugins = manager.section('targets').plugins; -let envPlugins = path.join(__dirname, 'plugins.env.js'); -let customPlugins = path.join(__dirname, 'plugins.json'); -let defaultPlugins = path.join(__dirname, 'plugins.default.json'); - -if (process.env.TALK_PLUGINS_JSON && process.env.TALK_PLUGINS_JSON.length > 0) { - pluginsConfigPath = envPlugins; -} else if (fs.existsSync(customPlugins)) { - pluginsConfigPath = customPlugins; -} else { - pluginsConfigPath = defaultPlugins; -} - -console.log(`Using ${pluginsConfigPath} as the plugin configuration path`); - -// Edit the build targets and embeds below. +console.log(`Using ${pluginsPath} as the plugin configuration path`); const buildTargets = [ 'coral-admin', @@ -37,47 +26,23 @@ const buildEmbeds = [ 'stream' ]; +//============================================================================== +// Base Webpack Config +//============================================================================== + const config = { devtool: 'cheap-module-source-map', - entry: Object.assign({}, { - 'embed': [ - 'babel-polyfill', - path.join(__dirname, 'client/coral-embed/src/index') - ] - }, buildTargets.reduce((entry, target) => { - - // Add the entry for the bundle. - entry[`${target}/bundle`] = [ - 'babel-polyfill', - path.join(__dirname, 'client/', target, '/src/index') - ]; - - return entry; - }, {}), buildEmbeds.reduce((entry, embed) => { - - // Add the entry for the bundle. - entry[`embed/${embed}/bundle`] = [ - 'babel-polyfill', - path.join(__dirname, 'client/', `coral-embed-${embed}`, '/src/index') - ]; - - return entry; - }, {})), output: { path: path.join(__dirname, 'dist'), publicPath: '/client/', - filename: '[name].js', - - // NOTE: this causes all exports to override the global.Coral, so no more - // than one bundle.js can be included on a page. - library: 'Coral' + filename: '[name].js' }, module: { rules: [ { loader: 'plugins-loader', test: /\.(json|js)$/, - include: pluginsConfigPath + include: pluginsPath }, { loader: 'babel-loader', @@ -127,7 +92,8 @@ const config = { plugins: [ new LicenseWebpackPlugin({ pattern: /^(MIT|ISC|BSD.*)$/, - addUrl: true + addUrl: true, + suppressErrors: true }), new Copy([ ...buildEmbeds.map((embed) => ({ @@ -156,7 +122,7 @@ const config = { alias: { 'plugin-api': path.resolve(__dirname, 'plugin-api/'), plugins: path.resolve(__dirname, 'plugins/'), - pluginsConfig: pluginsConfigPath + pluginsConfig: pluginsPath }, modules: [ path.resolve(__dirname, 'plugins'), @@ -168,6 +134,10 @@ const config = { } }; +//============================================================================== +// Production configuration overrides +//============================================================================== + if (process.env.NODE_ENV === 'production') { config.plugins.push(new CompressionPlugin({ asset: '[path].gz[query]', @@ -178,4 +148,87 @@ if (process.env.NODE_ENV === 'production') { })); } -module.exports = config; +//============================================================================== +// Entries +//============================================================================== + +// Applies the base configuration to the following entries. +const applyConfig = (entries, root = {}) => _.merge({}, config, { + entry: entries.reduce((entry, {name, path}) => { + entry[name] = [ + 'babel-polyfill', + path + ]; + + return entry; + }, {}) +}, root); + +module.exports = [ + + // Coral Embed + applyConfig([ + + // Load in the root embed. + { + name: 'embed', + path: path.join(__dirname, 'client/coral-embed/src/index') + } + + ], { + output: { + library: 'Coral' + } + }), + + // All framework targets/embeds/plugins. + applyConfig([ + + // // Load in all the targets. + ...buildTargets.map((target) => ({ + name: `${target}/bundle`, + path: path.join(__dirname, 'client/', target, '/src/index') + })), + + // Load in all the embeds. + ...buildEmbeds.map((embed) => ({ + name: `embed/${embed}/bundle`, + path: path.join(__dirname, 'client/', `coral-embed-${embed}`, '/src/index') + })), + + // Load in all the plugin entries. + ...targetPlugins.reduce((entries, plugin) => { + + // Introspect the path to find a targets folder. + let folder = path.dirname(plugin.path); + let files = fs.readdirSync(folder); + + // While the folder does not contain the targets folder... + while (!files.includes('targets')) { + + // Try to go up a folder. + folder = path.normalize(path.join(folder, '..')); + + // And as long as we haven't gone too high + if (!(folder.includes(path.join(__dirname, 'node_modules')) || !folder.includes(path.join(__dirname, 'plugins')))) { + throw new Error(`target plugin ${plugin.name} does not have a 'targets' folder`); + } + + files = fs.readdirSync(folder); + } + + // List all targets available in that folder. + folder = path.join(folder, 'targets'); + + let targets = fs.readdirSync(folder); + if (targets.length === 0) { + throw new Error(`target plugin ${plugin.name} has no targets in it's target folder ${folder}`); + } + + return entries.concat(targets.map((target) => ({ + name: `plugin/${plugin.name}/${target}/bundle`, + path: path.join(folder, target, 'index') + }))); + }, []) + ]) +];