diff --git a/architect.js b/architect.js index 090b564..ca23a77 100644 --- a/architect.js +++ b/architect.js @@ -1,604 +1,415 @@ ( // Module boilerplate to support node.js and AMD. - (typeof module !== "undefined" && function (m) { module.exports = m(require('events')); }) || - (typeof define === "function" && function (m) { define(["events"], m); }) -)(function (events) { -"use strict"; -var EventEmitter = events.EventEmitter; - -var exports = {}; - -var DEBUG = typeof location != "undefined" && location.href.match(/debug=[123]/) ? true : false; - -// Only define Node-style usage using sync I/O if in node. -if (typeof module === "object") (function () { - var dirname = require('path').dirname; - var resolve = require('path').resolve; - var existsSync = require('fs').existsSync || require('path').existsSync; - var realpathSync = require('fs').realpathSync; - var exists = require('fs').exists || require('path').exists; - var realpath = require('fs').realpath; - var packagePathCache = {}; - var basePath; - - exports.loadConfig = loadConfig; - exports.resolveConfig = resolveConfig; - - // This is assumed to be used at startup and uses sync I/O as well as can - // throw exceptions. It loads and parses a config file. - function loadConfig(configPath, callback) { - var config = require(configPath); - var base = dirname(configPath); - - return resolveConfig(config, base, callback); - } - - function resolveConfig(config, base, callback) { - if(typeof base === 'function') { - // probably being called from loadAdditionalConfig, use saved base - callback = base; - base = basePath; - } else { - basePath = base; - } + (typeof module !== "undefined" && function(m) { module.exports = m(require('events')); }) || + (typeof define === "function" && function(m) { define(["events"], m); }) +)(function(events) { + "use strict"; + var EventEmitter = events.EventEmitter; + + var exports = {}; + + var DEBUG = typeof location != "undefined" && location.href.match(/debug=[123]/) ? true : false; + + // Only define Node-style usage using sync I/O if in node. + if (typeof module === "object")(function() { + const fs = require("fs"); + const path = require("path"); + + function findPackagePath(packagePath, paths) { + paths = paths.reduce((paths, basePath) => { + while (basePath != "/") { + paths.push(path.resolve(basePath, "node_modules", packagePath, "package.json")); + paths.push(path.resolve(basePath, packagePath, "package.json")); + paths.push(path.resolve(basePath, "node_modules", packagePath + ".js")); + paths.push(path.resolve(basePath, packagePath + ".js")); + basePath = path.resolve(basePath, ".."); + } - if (!callback) - return resolveConfigSync(config, base); - else - resolveConfigAsync(config, base, callback); - } + return paths; + }, []); - function resolveConfigSync(config, base) { - config.forEach(function (plugin, index) { - // Shortcut where string is used for plugin without any options. - if (typeof plugin === "string") { - plugin = config[index] = { packagePath: plugin }; + for (let packagePath of paths) { + if (fs.existsSync(packagePath)) + return packagePath; } - // The plugin is a package on the disk. We need to load it. - if (plugin.hasOwnProperty("packagePath") && !plugin.hasOwnProperty("setup")) { - var defaults = resolveModuleSync(base, plugin.packagePath); - Object.keys(defaults).forEach(function (key) { - if (!plugin.hasOwnProperty(key)) { - plugin[key] = defaults[key]; - } - }); - plugin.packagePath = defaults.packagePath; - plugin.setup = require(plugin.packagePath); - } - }); - return config; - } + } - function resolveConfigAsync(config, base, callback) { - function resolveNext(i) { - if (i >= config.length) { - return callback(null, config); - } + exports.resolveConfig = resolveConfig; - var plugin = config[i]; + function resolveConfigAsync(config, base, callback) { + loadPlugin(config, base) + .then(config => callback(null, config)) + .catch(err => callback(err)); + } - // Shortcut where string is used for plugin without any options. - if (typeof plugin === "string") { - plugin = config[i] = { packagePath: plugin }; - } - // The plugin is a package on the disk. We need to load it. - if (plugin.hasOwnProperty("packagePath") && !plugin.hasOwnProperty("setup")) { - resolveModule(base, plugin.packagePath, function(err, defaults) { - if (err) return callback(err); - - Object.keys(defaults).forEach(function (key) { - if (!plugin.hasOwnProperty(key)) { - plugin[key] = defaults[key]; - } - }); - plugin.packagePath = defaults.packagePath; - try { - plugin.setup = require(plugin.packagePath); - } catch(e) { - return callback(e); - } + function normalize(plugin) { + if (typeof plugin === "string") + return { packagePath: plugin }; + return plugin; + } - return resolveNext(++i); - }); - return; - } + async function resolveConfig(config, base, callback) { + config = config.map(normalize); - return resolveNext(++i); - } + if (typeof base == "function") + return resolveConfig(config, null, base); - resolveNext(0); - } + if (callback) + return resolveConfigAsync(config, base, callback); - // Loads a module, getting metadata from either it's package.json or export - // object. - function resolveModuleSync(base, modulePath) { - var packagePath; - try { - packagePath = resolvePackageSync(base, modulePath + "/package.json"); - } - catch (err) { - if (err.code !== "ENOENT") throw err; - } - var metadata = packagePath && require(packagePath).plugin || {}; - if (packagePath) { - modulePath = dirname(packagePath); - } else { - modulePath = resolvePackageSync(base, modulePath); + return loadPlugin(config, base); } - var module = require(modulePath); - metadata.provides = metadata.provides || module.provides || []; - metadata.consumes = metadata.consumes || module.consumes || []; - metadata.packagePath = modulePath; - return metadata; - } - // Loads a module, getting metadata from either it's package.json or export - // object. - function resolveModule(base, modulePath, callback) { - resolvePackage(base, modulePath + "/package.json", function(err, packagePath) { - //if (err && err.code !== "ENOENT") return callback(err); - - var metadata = {}; - if (!err) { - try { - metadata = packagePath && require(packagePath).plugin || {}; - } catch(e) { - return callback(e); - } - } + async function loadPlugin(config, base) { + for (let plugin of config) { + if (plugin.hasOwnProperty("setup")) + continue; - (function(next) { - if (err) { - //@todo Fabian what is a better way? - resolvePackage(base, modulePath + ".js", next); - } - else if (packagePath) { - next(null, dirname(packagePath)); - } - else { - resolvePackage(base, modulePath, next); - } - })(function(err, modulePath) { - if (err) return callback(err); - - var module; - try { - module = require(modulePath); - } catch(e) { - return callback(e); - } + let packagePath = findPackagePath(plugin.packagePath, [].concat(base)); - metadata.provides = metadata.provides || module.provides || []; - metadata.consumes = metadata.consumes || module.consumes || []; - metadata.packagePath = modulePath; - callback(null, metadata); - }); - }); - } + if (!packagePath) + throw packageNotFoundError(plugin.packagePath, base); - // Node style package resolving so that plugins' package.json can be found relative to the config file - // It's not the full node require system algorithm, but it's the 99% case - // This throws, make sure to wrap in try..catch - function resolvePackageSync(base, packagePath) { - var originalBase = base; - if (!(base in packagePathCache)) { - packagePathCache[base] = {}; - } - var cache = packagePathCache[base]; - if (packagePath in cache) { - return cache[packagePath]; - } - var newPath; - if (packagePath[0] === "." || packagePath[0] === "/") { - newPath = resolve(base, packagePath); - if (!existsSync(newPath)) { - newPath = newPath + ".js"; - } - if (existsSync(newPath)) { - newPath = realpathSync(newPath); - cache[packagePath] = newPath; - return newPath; - } - } - else { - while (base) { - newPath = resolve(base, "node_modules", packagePath); - if (existsSync(newPath)) { - newPath = realpathSync(newPath); - cache[packagePath] = newPath; - return newPath; + let metadata = require(packagePath); + metadata.packagePath = packagePath; + + if (/package[.].json$/.test(packagePath)) { + metadata = metadata.module; + let modulePath = require.resolve(path.dirname(packagePath)); + let module = require(modulePath); + metadata.provides = metadata.provides || module.provides || []; + metadata.consumes = metadata.consumes || module.consumes || []; + metadata.packagePath = modulePath; } - base = resolve(base, '..'); + + Object.assign(plugin, metadata); } - } - var err = new Error("Can't find '" + packagePath + "' relative to '" + originalBase + "'"); - err.code = "ENOENT"; - throw err; - } - function resolvePackage(base, packagePath, callback) { - var originalBase = base; - if (!packagePathCache.hasOwnProperty(base)) { - packagePathCache[base] = {}; + return config; } - var cache = packagePathCache[base]; - if (cache.hasOwnProperty(packagePath)) { - return callback(null, cache[packagePath]); + + function packageNotFoundError(packagePath, base) { + let err = new Error(`Can't find ${packagePath} relative to ${base}`); + err.code = "ENOENT"; + return err; } - if (packagePath[0] === "." || packagePath[0] === "/") { - var newPath = resolve(base, packagePath); - exists(newPath, function(exists) { - if (exists) { - realpath(newPath, function(err, newPath) { - if (err) return callback(err); - - cache[packagePath] = newPath; - return callback(null, newPath); - }); - } else { - var err = new Error("Can't find '" + packagePath + "' relative to '" + originalBase + "'"); - err.code = "ENOENT"; - return callback(err); - } + + }()); + + // Otherwise use amd to load modules. + else(function() { + exports.loadConfig = loadConfig; + exports.resolveConfig = resolveConfig; + + function loadConfig(path, callback) { + require([path], function(config) { + resolveConfig(config, callback); }); } - else { - tryNext(base); - } - function tryNext(base) { - if (base == "/") { - var err = new Error("Can't find '" + packagePath + "' relative to '" + originalBase + "'"); - err.code = "ENOENT"; - return callback(err); - } + function resolveConfig(config, base, callback, errback) { + if (typeof base == "function") + return resolveConfig(config, "", arguments[1], arguments[2]); - var newPath = resolve(base, "node_modules", packagePath); - exists(newPath, function(exists) { - if (exists) { - realpath(newPath, function(err, newPath) { - if (err) return callback(err); - - cache[packagePath] = newPath; - return callback(null, newPath); - }); - } else { - var nextBase = resolve(base, '..'); - if (nextBase === base) - tryNext("/"); // for windows - else - tryNext(nextBase); + var paths = [], + pluginIndexes = {}; + config.forEach(function(plugin, index) { + // Shortcut where string is used for plugin without any options. + if (typeof plugin === "string") { + plugin = config[index] = { packagePath: plugin }; + } + // The plugin is a package over the network. We need to load it. + if (plugin.hasOwnProperty("packagePath") && !plugin.hasOwnProperty("setup")) { + paths.push((base || "") + plugin.packagePath); + pluginIndexes[plugin.packagePath] = index; } }); + // Mass-Load path-based plugins using amd's require + require(paths, function() { + var args = arguments; + paths.forEach(function(name, i) { + var module = args[i]; + var plugin = config[pluginIndexes[name]]; + plugin.setup = module; + plugin.provides = module.provides || plugin.provides || []; + plugin.consumes = module.consumes || plugin.consumes || []; + }); + callback(null, config); + }, errback); } - } + }()); + class ArchitectError extends Error { + constructor(message, plugin = {}) { + super(); + this.message = `${message} ${JSON.stringify(plugin)}`; + } + } -}()); -// Otherwise use amd to load modules. -else (function () { - exports.loadConfig = loadConfig; - exports.resolveConfig = resolveConfig; - function loadConfig(path, callback) { - require([path], function (config) { - resolveConfig(config, callback); - }); - } + // Check a plugin config list for bad dependencies and throw on error + function checkConfig(config, lookup) { - function resolveConfig(config, base, callback, errback) { - if (typeof base == "function") - return resolveConfig(config, "", arguments[1], arguments[2]); - - var paths = [], pluginIndexes = {}; - config.forEach(function (plugin, index) { - // Shortcut where string is used for plugin without any options. - if (typeof plugin === "string") { - plugin = config[index] = { packagePath: plugin }; + // Check for the required fields in each plugin. + config.forEach(function(plugin) { + if (plugin.checked) { return; } + if (!plugin.hasOwnProperty("setup")) { + throw new ArchitectError("Plugin is missing the setup function", plugin); + } + if (!plugin.hasOwnProperty("provides")) { + throw new ArchitectError("Plugin is missing the provides array ", plugin); } - // The plugin is a package over the network. We need to load it. - if (plugin.hasOwnProperty("packagePath") && !plugin.hasOwnProperty("setup")) { - paths.push((base || "") + plugin.packagePath); - pluginIndexes[plugin.packagePath] = index; + if (!plugin.hasOwnProperty("consumes")) { + throw new ArchitectError("Plugin is missing the consumes array ", plugin); } }); - // Mass-Load path-based plugins using amd's require - require(paths, function () { - var args = arguments; - paths.forEach(function (name, i) { - var module = args[i]; - var plugin = config[pluginIndexes[name]]; - plugin.setup = module; - plugin.provides = module.provides || plugin.provides || []; - plugin.consumes = module.consumes || plugin.consumes || []; - }); - callback(null, config); - }, errback); + + return checkCycles(config, lookup); } -}()); -exports.createApp = createApp; -exports.Architect = Architect; + function checkCycles(config, lookup) { + var plugins = []; + config.forEach(function(pluginConfig, index) { + plugins.push({ + packagePath: pluginConfig.packagePath, + provides: pluginConfig.provides.concat(), + consumes: pluginConfig.consumes.concat(), + i: index + }); + }); + + var resolved = { + hub: true + }; + var changed = true; + var sorted = []; -// Check a plugin config list for bad dependencies and throw on error -function checkConfig(config, lookup) { + while (plugins.length && changed) { + changed = false; - // Check for the required fields in each plugin. - config.forEach(function (plugin) { - if (plugin.checked) { return; } - if (!plugin.hasOwnProperty("setup")) { - throw new Error("Plugin is missing the setup function " + JSON.stringify(plugin)); - } - if (!plugin.hasOwnProperty("provides")) { - throw new Error("Plugin is missing the provides array " + JSON.stringify(plugin)); - } - if (!plugin.hasOwnProperty("consumes")) { - throw new Error("Plugin is missing the consumes array " + JSON.stringify(plugin)); - } - }); - - return checkCycles(config, lookup); -} - -function checkCycles(config, lookup) { - var plugins = []; - config.forEach(function(pluginConfig, index) { - plugins.push({ - packagePath: pluginConfig.packagePath, - provides: pluginConfig.provides.concat(), - consumes: pluginConfig.consumes.concat(), - i: index - }); - }); - - var resolved = { - hub: true - }; - var changed = true; - var sorted = []; - - while(plugins.length && changed) { - changed = false; - - plugins.concat().forEach(function(plugin) { - var consumes = plugin.consumes.concat(); - - var resolvedAll = true; - for (var i=0; i { + plugin.setup(plugin, imports, (err, provided) => { + if (err) return reject(err); + resolve(provided); }); }); - - Object.keys(unresolved).forEach(function(name) { - if (unresolved[name] === false) - delete unresolved[name]; - }); + } + + function setupPlugin(plugin, imports) { + if (plugin.setup.length > 2) + return asyncPlugin(plugin, imports); - var unresolvedList = Object.keys(unresolved); - var resolvedList = Object.keys(resolved); - var err = new Error("Could not resolve dependencies\n" - + (unresolvedList.length ? "Missing services: " + unresolvedList - : "Config contains cyclic dependencies" // TODO print cycles - )); - err.unresolved = unresolvedList; - err.resolved = resolvedList; - throw err; + return plugin.setup(plugin, imports); } - return sorted; -} - -function Architect(config) { - var app = this; - app.config = config; - app.packages = {}; - app.pluginToPackage = {}; - - var isAdditionalMode; - var services = app.services = { - hub: { - on: function (name, callback) { - app.on(name, callback); + class Architect extends EventEmitter { + constructor(config) { + super(); + this.config = config; + } + + get destructors() { + if (!this._destructors) + this._destructors = []; + + return this._destructors; + } + + get services() { + if (!this._services) { + let hub = { + on: this.on.bind(this) + }; + + this._services = { hub }; } + + return this._services; + } + + get ready() { + return this._isReady; } - }; - - // Check the config - var sortedPlugins = checkConfig(config); - - var destructors = []; - var recur = 0, callnext, ready; - function startPlugins(additional) { - var plugin = sortedPlugins.shift(); - if (!plugin) { - ready = true; - return app.emit(additional ? "ready-additional" : "ready", app); + + addDestructor(provided) { + if (!provided) return; + if (!provided.hasOwnProperty("onDestroy")) return; + this.destructors.push(provided.onDestroy); } - var imports = {}; - if (plugin.consumes) { - plugin.consumes.forEach(function (name) { - imports[name] = services[name]; + destroy() { + this.destructors.forEach((destroy) => { + destroy(); }); + + this._destructors = []; } - - var m = /^plugins\/([^\/]+)|\/plugins\/[^\/]+\/([^\/]+)/.exec(plugin.packagePath); - var packageName = m && (m[1] || m[2]); - if (!app.packages[packageName]) app.packages[packageName] = []; - - if (DEBUG) { - recur++; - plugin.setup(plugin, imports, register); - - while (callnext && recur <= 1) { - callnext = false; - startPlugins(additional); - } - recur--; + + getService(name) { + if (!this.services[name]) + throw new Error("Service '" + name + "' not found in architect app!"); + return this.services[name]; } - else { - try { - recur++; - plugin.setup(plugin, imports, register); - } catch (e) { - e.plugin = plugin; - app.emit("error", e); - throw e; - } finally { - while (callnext && recur <= 1) { - callnext = false; - startPlugins(additional); + + async loadAdditionalPlugins(additionalConfig, callback) { + this.once(this._isReady ? "ready-additional" : "ready", () => { + callback(null, this); + }); + + const sortedPlugins = checkConfig(additionalConfig, (name) => this.services[name]); + await exports.resolveConfig(additionalConfig); + + this.sortedPlugins = this.sortedPlugins.concat(sortedPlugins); + + if (this._isReady) { + for (let plugin of this.sortedPlugins) { + await this.startPlugin(plugin); } - recur--; } + else callback(); + this._emitReady(); } - - function register(err, provided) { - if (err) { return app.emit("error", err); } - plugin.provides.forEach(function (name) { - if (!provided.hasOwnProperty(name)) { - var err = new Error("Plugin failed to provide " + name + " service. " + JSON.stringify(plugin)); - err.plugin = plugin; - return app.emit("error", err); - } - services[name] = provided[name]; - app.pluginToPackage[name] = { - path: plugin.packagePath, - package: packageName, - version: plugin.version, - isAdditionalMode: isAdditionalMode - }; - app.packages[packageName].push(name); - - app.emit("service", name, services[name], plugin); - }); - if (provided && provided.hasOwnProperty("onDestroy")) - destructors.push(provided.onDestroy); - app.emit("plugin", plugin); - - if (recur) return (callnext = true); - startPlugins(additional); - } - } + async startPlugin(plugin) { + var imports = {}; - // Give createApp some time to subscribe to our "ready" event - (typeof process === "object" ? process.nextTick : setTimeout)(startPlugins); - - this.loadAdditionalPlugins = function(additionalConfig, callback){ - isAdditionalMode = true; - - exports.resolveConfig(additionalConfig, function (err, additionalConfig) { - if (err) return callback(err); - - app.once(ready ? "ready-additional" : "ready", function(app){ - callback(null, app); - }); // What about error state? - - // Check the config - hopefully this works - var _sortedPlugins = checkConfig(additionalConfig, function(name){ - return services[name]; + plugin.consumes.forEach((name) => { + imports[name] = this.services[name]; }); - - if (ready) { - sortedPlugins = _sortedPlugins; - // Start Loading additional plugins - startPlugins(true); + + + let provided = await setupPlugin(plugin, imports); + + for (let name of plugin.provides) { + if (provided.hasOwnProperty(name)) + continue; + + throw new ArchitectError("Plugin failed to provide " + name + " service. ", plugin); } - else { - _sortedPlugins.forEach(function(item){ - sortedPlugins.push(item); - }); + + for (let name of plugin.provides) { + let service = provided[name]; + this.services[name] = service; + this.emit("service", name, service, plugin); } - }); - } - this.destroy = function() { - destructors.forEach(function(destroy) { - destroy(); - }); + this.addDestructor(provided); + } - destructors = []; - }; -} -Architect.prototype = Object.create(EventEmitter.prototype, {constructor:{value:Architect}}); + async start() { + const sortedPlugins = await checkConfig(this.config); -Architect.prototype.getService = function(name) { - if (!this.services[name]) { - throw new Error("Service '" + name + "' not found in architect app!"); - } - return this.services[name]; -}; - -// Returns an event emitter that represents the app. It can emit events. -// event: ("service" name, service) emitted when a service is ready to be consumed. -// event: ("plugin", plugin) emitted when a plugin registers. -// event: ("ready", app) emitted when all plugins are ready. -// event: ("error", err) emitted when something goes wrong. -// app.services - a hash of all the services in this app -// app.config - the plugin config that was passed in. -function createApp(config, callback) { - var app; - try { - app = new Architect(config); - } catch(err) { - if (!callback) throw err; - return callback(err, app); - } - if (callback) { - app.on("error", done); - app.on("ready", onReady); - } - return app; + for (let plugin of sortedPlugins) { + await this.startPlugin(plugin); + } - function onReady(app) { - done(); - } + this.sortedPlugins = sortedPlugins; + this._emitReady(); + return this; + } - function done(err) { - if (err) { - app.destroy(); + _emitReady() { + let ready = this._isReady; + this._isReady = true; + this.emit(ready ? "ready-additional" : "ready", this); } - app.removeListener("error", done); - app.removeListener("ready", onReady); - callback(err, app); + + } + + exports.createApp = createApp; + exports.Architect = Architect; + + function delay(fn) { + (typeof process === "object" ? process.nextTick : setTimeout)(fn); } - return app; -} -return exports; + // Returns an event emitter that represents the app. It can emit events. + // event: ("service" name, service) emitted when a service is ready to be consumed. + // event: ("plugin", plugin) emitted when a plugin registers. + // event: ("ready", app) emitted when all plugins are ready. + // event: ("error", err) emitted when something goes wrong. + // app.services - a hash of all the services in this app + // app.config - the plugin config that was passed in. + function createApp(config, callback) { + var app = new Architect(config); + + // delayed execution allows + // the caller to consume the return value + // and attach eventlisteners + delay(() => { + app.start() + .then((app) => { + callback(null, app); + }) + .catch(callback); + }); + + return app; + } + + return exports; }); diff --git a/architect_test.js b/architect_test.js new file mode 100644 index 0000000..5acf492 --- /dev/null +++ b/architect_test.js @@ -0,0 +1,538 @@ +const architect = require("./architect"); +const test = require("tape"); +const fs = require("fs"); +const promisify = require("util").promisify; +const path = require("path"); + +const readFile = promisify(fs.readFile); +const unlink = promisify(fs.unlink); +const writeFile = promisify(fs.writeFile); +const mkdirp = promisify(require("mkdirp")); + +test("resolve config resolved", assert => { + const config = [{ + setup: function() { + // noop + }, + provides: ["foo"], + consumes: ["foo"] + }]; + + architect.resolveConfig(config, "", (err, resolvedConfig) => { + assert.ok(!err, "no error"); + assert.deepEqual(resolvedConfig, config); + assert.end(); + }); +}); + +test("resolve config from basepath + node_modules", async(assert) => { + const fakePlugin = ` + module.exports = { + setup: function(){ + // noop + }, + provides: ["foo"], + consumes: ["foo"] + } + `; + + let packagePath = "_fake/plugin_" + Date.now(); + let packageDir = "/tmp/_architect_test_fixtures/node_modules"; + let fullPath = packageDir + "/" + packagePath + ".js"; + + let config = [ + packagePath, + ]; + + await mkdirp(path.dirname(fullPath)); + await writeFile(fullPath, fakePlugin.toString()); + + architect.resolveConfig(config, path.dirname(packageDir), async(err, resolvedConfig) => { + assert.ok(!err); + + assert.equal(resolvedConfig[0].packagePath, fullPath); + assert.deepEqual(resolvedConfig[0].consumes, ["foo"]); + assert.deepEqual(resolvedConfig[0].provides, ["foo"]); + + await unlink(fullPath); + assert.end(); + }); +}); + +test("resolve config from basepath + node_modules, async", async(assert) => { + const fakePlugin = ` + module.exports = { + setup: function(){ + // noop + }, + provides: ["foo"], + consumes: ["foo"] + } + `; + + let packagePath = "_fake/plugin_" + Date.now(); + let packageDir = "/tmp/_architect_test_fixtures/node_modules"; + let fullPath = packageDir + "/" + packagePath + ".js"; + + let config = [ + packagePath, + ]; + + await mkdirp(path.dirname(fullPath)); + await writeFile(fullPath, fakePlugin.toString()); + + let resolvedConfig = await architect.resolveConfig(config, path.dirname(packageDir)); + + assert.equal(resolvedConfig[0].packagePath, fullPath); + assert.deepEqual(resolvedConfig[0].consumes, ["foo"]); + assert.deepEqual(resolvedConfig[0].provides, ["foo"]); + + await unlink(fullPath); + assert.end(); +}); + +test("it should start an architect app (classic)", async(assert) => { + const fakeConfig = [{ + packagePath: "foo/plugin", + setup: function(config, imports, register) { + register(null); + }, + provides: [], + consumes: ["bar.plugin"] + }, + { + packagePath: "bar/plugin", + setup: function(config, imports, register) { + register(null, { + "bar.plugin": { + iamBar: true + } + }); + }, + provides: ["bar.plugin"], + consumes: [] + } + ]; + + architect.createApp(fakeConfig, (err) => { + assert.ok(!err, "no err"); + assert.end(); + }); +}); + +test("it should provide imports", async(assert) => { + let iamBar = false; + + const fakeConfig = [{ + packagePath: "foo/plugin", + setup: function(config, imports, register) { + assert.ok(imports["bar.plugin"].iamBar); + iamBar = true; + register(); + }, + provides: [], + consumes: ["bar.plugin"] + }, + { + packagePath: "bar/plugin", + setup: function(config, imports, register) { + register(null, { + "bar.plugin": { + iamBar: true + } + }); + }, + provides: ["bar.plugin"], + consumes: [] + } + ]; + + architect.createApp(fakeConfig, (err) => { + assert.ok(!err, "no err"); + assert.ok(iamBar, "iamBar was imported"); + assert.end(); + }); +}); + +test("it should provide imports", async(assert) => { + let barDestroyed = false; + + const fakeConfig = [{ + packagePath: "foo/plugin", + setup: function(config, imports, register) { + assert.ok(imports["bar.plugin"].iamBar); + register(); + }, + provides: [], + consumes: ["bar.plugin"] + }, + { + packagePath: "bar/plugin", + setup: function(config, imports, register) { + register(null, { + onDestroy: function() { + barDestroyed = true; + }, + "bar.plugin": { + iamBar: true + } + }); + }, + provides: ["bar.plugin"], + consumes: [] + } + ]; + + architect.createApp(fakeConfig, (err, app) => { + assert.ok(!err, "no err"); + + app.destroy(); + + assert.ok(barDestroyed, "barDestroyed"); + + assert.end(); + }); +}); + + +test("it allow loading additionalPlugins", async(assert) => { + const fakeConfig = [{ + packagePath: "foo/plugin", + setup: function(config, imports, register) { + assert.ok(imports["bar.plugin"].iamBar); + register(); + }, + provides: [], + consumes: ["bar.plugin"] + }, + { + packagePath: "bar/plugin", + setup: function(config, imports, register) { + register(null, { + "bar.plugin": { + iamBar: true + } + }); + }, + provides: ["bar.plugin"], + consumes: [] + } + ]; + + let app = architect.createApp(fakeConfig, (err) => { + assert.ok(!err, "no err"); + }); + + app.on("ready", () => { + let loadedBar = false; + + const fakeAdditional = [{ + packagePath: "biz/plugin", + setup: function(config, imports, register) { + assert.ok(imports["bar.plugin"].iamBar); + loadedBar = true; + register(); + }, + provides: [], + consumes: ["bar.plugin"] + }]; + + app.loadAdditionalPlugins(fakeAdditional, (err) => { + assert.ok(!err, "no err"); + assert.ok(loadedBar, "loadedBar"); + assert.end(); + }); + }); +}); + +test("it detects cyclic dependencies (classic)", async(assert) => { + const fakeConfig = [{ + packagePath: "foo/plugin", + setup: function(config, imports, register) {}, + provides: ["foo.plugin"], + consumes: ["bar.plugin"] + }, + { + packagePath: "bar/plugin", + setup: function(config, imports, register) {}, + provides: ["bar.plugin"], + consumes: ["foo.plugin"] + } + ]; + + architect.createApp(fakeConfig, (err) => { + let expect = "Could not resolve dependencies\nConfig contains cyclic dependencies"; + assert.equal(err.message, expect); + assert.end(); + }); +}); + +test("it checks the provides", async(assert) => { + const fakeConfig = [{ + packagePath: "foo/plugin", + setup: function(config, imports, register) { + register(null); + }, + provides: [], + consumes: ["bar.plugin"] + }, + { + packagePath: "bar/plugin", + setup: function(config, imports, register) { + register(null, {}); + }, + provides: ["bar.plugin"], + consumes: [] + } + ]; + + architect.createApp(fakeConfig, (err) => { + assert.ok(/Plugin failed to provide bar.plugin service/.test(err.message)); + assert.end(); + }); +}); + +test("it checks all dependencies", async(assert) => { + const fakeConfig = [{ + packagePath: "foo/plugin", + setup: function(config, imports, register) {}, + provides: ["foo.plugin"], + consumes: ["bar.plugin"] + }, + { + packagePath: "bar/plugin", + setup: function(config, imports, register) {}, + provides: [], + consumes: [] + } + ]; + + architect.createApp(fakeConfig, (err) => { + let expect = "Could not resolve dependencies\nMissing services: bar.plugin"; + assert.equal(err.message, expect); + assert.end(); + }); +}); + +test("it validates config (consumes must be present)", async(assert) => { + const fakeConfig = [{ + packagePath: "foo/plugin", + setup: function(config, imports, register) {}, + provides: [], + }]; + + architect.createApp(fakeConfig, (err) => { + assert.ok(/Plugin is missing the consumes array/.test(err.message)); + assert.end(); + }); + +}); + +test("it validates config (provides must be present)", async(assert) => { + const fakeConfig = [{ + packagePath: "foo/plugin", + setup: function(config, imports, register) {}, + }]; + + architect.createApp(fakeConfig, (err) => { + assert.ok(/Plugin is missing the provides array/.test(err.message)); + assert.end(); + }); + +}); + +test("it validates config (setup must be present)", async(assert) => { + const fakeConfig = [{ + packagePath: "foo/plugin", + }]; + + architect.createApp(fakeConfig, (err) => { + assert.ok(/Plugin is missing the setup function/.test(err.message)); + assert.end(); + }); +}); + +test("it should start an architect app when plugin _returns_ value", async(assert) => { + const fakeConfig = [{ + packagePath: "foo/plugin", + setup: function(config, imports, register) { + register(null); + }, + provides: [], + consumes: ["bar.plugin"] + }, + { + packagePath: "bar/plugin", + setup: function(config, imports) { + return { + "bar.plugin": { + isBar: true + } + }; + }, + provides: ["bar.plugin"], + consumes: [] + } + ]; + + architect.createApp(fakeConfig, (err) => { + assert.ok(!err, "no err"); + assert.end(); + }); +}); + +test("it should start an architect app when plugin awaits", async(assert) => { + const fakeConfig = [{ + packagePath: "foo/plugin", + setup: function(config, imports, register) { + register(null); + }, + provides: [], + consumes: ["bar.plugin"] + }, + { + packagePath: "bar/plugin", + setup: async(config, imports) => { + let delay = new Promise(resolve => { + setTimeout(resolve, 100); + }); + + await delay; + + return { + "bar.plugin": { + isBar: true + } + }; + }, + provides: ["bar.plugin"], + consumes: [] + } + ]; + + architect.createApp(fakeConfig, (err) => { + assert.ok(!err, "no err"); + assert.end(); + }); +}); + +test("it should start an architect app when plugin returns promise", async(assert) => { + const fakeConfig = [{ + packagePath: "foo/plugin", + setup: function(config, imports, register) { + register(null); + }, + provides: [], + consumes: ["bar.plugin"] + }, + { + packagePath: "bar/plugin", + setup: async(config, imports) => { + return new Promise(resolve => { + resolve({ + "bar.plugin": { + isBar: true + } + }); + }); + }, + provides: ["bar.plugin"], + consumes: [] + } + ]; + + architect.createApp(fakeConfig, (err) => { + assert.ok(!err, "no err"); + assert.end(); + }); +}); + +test("it should start an architect app when plugin rejects promise", async(assert) => { + const fakeConfig = [{ + packagePath: "foo/plugin", + setup: function(config, imports, register) { + register(null); + }, + provides: [], + consumes: ["bar.plugin"] + }, + { + packagePath: "bar/plugin", + setup: async(config, imports) => { + return new Promise((resolve, reject) => { + reject("Foo error!"); + }); + }, + provides: ["bar.plugin"], + consumes: [] + } + ]; + + architect.createApp(fakeConfig, (err) => { + assert.equal(err, "Foo error!"); + assert.end(); + }); +}); + +test("it should start an architect app when plugin has an error", async(assert) => { + const fakeConfig = [{ + packagePath: "foo/plugin", + setup: function(config, imports, register) { + register(null); + }, + provides: [], + consumes: ["bar.plugin"] + }, + { + packagePath: "bar/plugin", + setup: async(config, imports) => { + let boink = 1; + boink(); + }, + provides: ["bar.plugin"], + consumes: [] + } + ]; + + architect.createApp(fakeConfig, (err) => { + assert.equal(err.message, "boink is not a function"); + assert.end(); + }); +}); + +test("it should start an architect app with await", async(assert) => { + const fakeConfig = [{ + packagePath: "foo/plugin", + setup: function(config, imports, register) { + register(null); + }, + provides: [], + consumes: ["bar.plugin"] + }, + { + packagePath: "bar/plugin", + setup: async(config, imports) => { + return { + "bar.plugin": { + isBar: true + } + }; + }, + provides: ["bar.plugin"], + consumes: [] + } + ]; + + const app = new architect.Architect(fakeConfig); + + app.on("ready", () => assert.end()); + + await app.start(); + + assert.ok(app.ready); + + let service = app.getService("bar.plugin"); + + assert.deepEqual(service, { isBar: true }); + +}); diff --git a/package.json b/package.json index d440745..3f75f4b 100644 --- a/package.json +++ b/package.json @@ -1,34 +1,37 @@ { - "name": "architect", - "description": "A Simple yet powerful plugin system for node applications", - "version": "0.1.13", - "author": "ajax.org B.V. ", - "contributors": [ - { - "name": "Tim Caswell", - "email": "tim@c9.io" - }, - { - "name": "Fabian Jakobs", - "email": "fabian@c9.io" - }, - { - "name": "Christoph Dorn", - "email": "christoph@christophdorn.com" - } - ], - "main": "architect.js", - "repository": { - "type": "git", - "url": "http://github.com/c9/architect.git" + "name": "architect", + "description": "A Simple yet powerful plugin system for node applications", + "version": "0.1.13", + "author": "ajax.org B.V. ", + "contributors": [ + { + "name": "Tim Caswell", + "email": "tim@c9.io" }, - "dependencies": {}, - "devDependencies": {}, - "optionalDependencies": {}, - "licenses": [ - { - "type": "MIT", - "url": "http://github.com/c9/architect/raw/master/LICENSE" - } - ] + { + "name": "Fabian Jakobs", + "email": "fabian@c9.io" + }, + { + "name": "Christoph Dorn", + "email": "christoph@christophdorn.com" + } + ], + "main": "architect.js", + "repository": { + "type": "git", + "url": "http://github.com/c9/architect.git" + }, + "dependencies": {}, + "devDependencies": { + "mkdirp": "^0.5.1", + "tape": "^4.8.0" + }, + "optionalDependencies": {}, + "licenses": [ + { + "type": "MIT", + "url": "http://github.com/c9/architect/raw/master/LICENSE" + } + ] }