Comparing version 0.0.3 to 0.1.1
475
architect.js
@@ -1,330 +0,265 @@ | ||
var path = require('path'); | ||
var dirname = require('path').dirname; | ||
var resolve = require('path').resolve; | ||
var existsSync = require('path').existsSync; | ||
var realpathSync = require('fs').realpathSync; | ||
var EventEmitter = require('events').EventEmitter; | ||
var inherits = require('util').inherits; | ||
exports.loadConfig = loadConfig; | ||
exports.resolveConfig = resolveConfig; | ||
exports.createApp = createApp; | ||
function createApp(config, options, callback) { | ||
if (typeof callback === "undefined") { | ||
callback = options; | ||
options = {}; | ||
} | ||
config = processConfig(config, options); | ||
// console.log("compiled config:"); | ||
// console.log(JSON.stringify(config)); | ||
startContainers(config, callback); | ||
exports.Architect = Architect; | ||
// 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) { | ||
var config = require(configPath); | ||
var base = dirname(configPath); | ||
return resolveConfig(config, base); | ||
} | ||
// Gather and preflight the config. | ||
exports.processConfig = processConfig; | ||
function processConfig(configPath, options) { | ||
options = options || {}; | ||
var config = {}; | ||
// Allow passing in either config path or config object | ||
if (typeof configPath === "object") { | ||
config = configPath; | ||
configPath = "<provided config object>"; | ||
if (!config.basePath) { | ||
var err = new Error("'basePath' required in config object"); | ||
return callback(err); | ||
function resolveConfig(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 }; | ||
} | ||
} else { | ||
configPath = require.resolve(configPath); | ||
config = require(configPath); | ||
} | ||
// Overwrite console object from config if set in createApp options | ||
if (typeof options.console !== "undefined") { | ||
config.console = options.console; | ||
} else { | ||
config.console = console; | ||
} | ||
// Default basePath to the dirname of the config file | ||
var basePath = config.basePath = config.basePath || path.dirname(configPath); | ||
// Resolve plugin paths to the basePath and merge in plugin configs from | ||
// package.json files. | ||
Object.keys(config.containers).forEach(function (containerName) { | ||
var containerConfig = config.containers[containerName]; | ||
var pluginsConfigs = containerConfig.plugins; | ||
pluginsConfigs && pluginsConfigs.forEach(function (pluginConfig, index) { | ||
// if plugin is a string it is interpreted as the package path | ||
if (typeof pluginConfig === "string") { | ||
pluginsConfigs[index] = pluginConfig = { | ||
packagePath: pluginConfig | ||
}; | ||
} | ||
// packagePath is required on all plugins | ||
if (!pluginConfig.hasOwnProperty("packagePath")) { | ||
var err = new Error("'packagePath' required in `" + | ||
configPath + "` at " + containerName + "[" + index + "]"); | ||
return callback(err); | ||
} | ||
var pluginConfigBase; | ||
// the architect app can inject plugins into the master | ||
if (pluginConfig.plugin) { | ||
if (containerName !== "master") | ||
return new Error("Plugins can only be injected into the master container"); | ||
var pluginConfigBase = pluginConfig.plugin; | ||
} | ||
else { | ||
// Replace with fully resolved path | ||
var packagePath = resolvePackage(basePath, pluginConfig.packagePath); | ||
pluginConfig.packagePath = packagePath; | ||
// Look up the provides and consumes in the package.json and merge. | ||
try { | ||
pluginConfigBase = require(packagePath).plugin; | ||
} catch(err) { | ||
throw new Error("Error '" + err + "' loading config from " + packagePath); | ||
// The plugin is a package on the disk. We need to load it. | ||
if (plugin.hasOwnProperty("packagePath")) { | ||
plugin.packagePath = resolvePackageSync(base, plugin.packagePath); | ||
var packageConf = require(plugin.packagePath); | ||
var defaults = packageConf.plugin || {}; | ||
Object.keys(defaults).forEach(function (key) { | ||
if (!plugin.hasOwnProperty(key)) { | ||
plugin[key] = defaults[key]; | ||
} | ||
} | ||
if (!pluginConfigBase) { | ||
throw new Error("Missing 'plugin' section in " + packagePath); | ||
} | ||
for (var key in pluginConfigBase) { | ||
if (!pluginConfig.hasOwnProperty(key)) { | ||
pluginConfig[key] = pluginConfigBase[key]; | ||
} | ||
} | ||
// provide defaults | ||
pluginConfig.provides = pluginConfig.provides || []; | ||
pluginConfig.consumes = pluginConfig.consumes || []; | ||
}); | ||
}); | ||
plugin.setup = require(dirname(plugin.packagePath)); | ||
plugin.consumes = plugin.consumes || []; | ||
plugin.provides = plugin.provides || []; | ||
} | ||
}); | ||
return config; | ||
} | ||
// Set a tmpdir for anything that might need it. | ||
config.tmpdir = config.tmpdir || path.join(process.cwd(), ".architect"); | ||
// Check a plugin config list for bad dependencies and throw on error | ||
function checkConfig(config) { | ||
// Tell which containers need to listen for inbound connections. Also set | ||
// name and tmpdir for all containers. | ||
Object.keys(config.containers).forEach(function (containerName) { | ||
var containerConfig = config.containers[containerName]; | ||
if (needsServe(config.containers, containerName)) { | ||
containerConfig.needsServe = true; | ||
// 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)); | ||
} | ||
containerConfig.name = containerName; | ||
containerConfig.tmpdir = config.tmpdir; | ||
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)); | ||
} | ||
}); | ||
// Make sure there are no dependency cycles that would prevent the app | ||
// from starting. | ||
checkCycles(config); | ||
return config; | ||
// Stamp it approved so we don't check it again. | ||
config.checked = true; | ||
} | ||
// pre flight dependency check | ||
function checkCycles(config) { | ||
var plugins = []; | ||
var containers = config.containers; | ||
Object.keys(containers).forEach(function(containerName) { | ||
var pluginConfigs = containers[containerName].plugins || []; | ||
pluginConfigs.forEach(function(pluginConfig) { | ||
plugins.push({ | ||
packagePath: pluginConfig.packagePath, | ||
provides: pluginConfig.provides.concat(), | ||
consumes: pluginConfig.consumes.concat() | ||
}); | ||
}); | ||
function checkCycles(config) { | ||
var plugins = []; | ||
config.forEach(function(pluginConfig) { | ||
plugins.push({ | ||
packagePath: pluginConfig.packagePath, | ||
provides: pluginConfig.provides.concat(), | ||
consumes: pluginConfig.consumes.concat() | ||
}); | ||
}); | ||
var resolved = { | ||
hub: true | ||
}; | ||
var changed = true; | ||
var resolved = { | ||
hub: true | ||
}; | ||
var changed = true; | ||
while(plugins.length && changed) { | ||
changed = false; | ||
while(plugins.length && changed) { | ||
changed = false; | ||
plugins.concat().forEach(function(plugin) { | ||
var consumes = plugin.consumes.concat(); | ||
plugins.concat().forEach(function(plugin) { | ||
var consumes = plugin.consumes.concat(); | ||
var resolvedAll = true; | ||
for (var i=0; i<consumes.length; i++) { | ||
var service = consumes[i]; | ||
if (!resolved[service]) { | ||
resolvedAll = false; | ||
} else { | ||
plugin.consumes.splice(plugin.consumes.indexOf(service), 1); | ||
} | ||
var resolvedAll = true; | ||
for (var i=0; i<consumes.length; i++) { | ||
var service = consumes[i]; | ||
if (!resolved[service]) { | ||
resolvedAll = false; | ||
} else { | ||
plugin.consumes.splice(plugin.consumes.indexOf(service), 1); | ||
} | ||
} | ||
if (!resolvedAll) | ||
return; | ||
if (!resolvedAll) | ||
return; | ||
plugins.splice(plugins.indexOf(plugin), 1); | ||
plugin.provides.forEach(function(service) { | ||
resolved[service] = true; | ||
}); | ||
changed = true; | ||
plugins.splice(plugins.indexOf(plugin), 1); | ||
plugin.provides.forEach(function(service) { | ||
resolved[service] = true; | ||
}); | ||
} | ||
if (plugins.length) { | ||
console.error("Could not resolve dependencies of these plugins:", plugins); | ||
console.error("Resovled services:", resolved); | ||
throw new Error("Could not resolve dependencies"); | ||
} | ||
changed = true; | ||
}); | ||
} | ||
function calcProvides(container) { | ||
var provides = {}; | ||
var plugins = container.plugins; | ||
plugins && plugins.forEach(function (plugin) { | ||
plugin.provides.forEach(function (service) { | ||
provides[service] = true; | ||
}); | ||
}); | ||
return provides; | ||
if (plugins.length) { | ||
console.error("Could not resolve dependencies of these plugins:", plugins); | ||
console.error("Resovled services:", resolved); | ||
throw new Error("Could not resolve dependencies"); | ||
} | ||
} | ||
function calcDepends(container, provides) { | ||
if (!container.plugins) return false; | ||
var i = container.plugins.length; | ||
while (i--) { | ||
var consumes = container.plugins[i].consumes; | ||
var j = consumes.length; | ||
while (j--) { | ||
if (provides[consumes[j]]) return true; | ||
function Architect(config) { | ||
var app = this; | ||
app.config = config; | ||
var services = app.services = { | ||
hub: { | ||
on: function (name, callback) { | ||
app.on(name, callback); | ||
} | ||
} | ||
return false; | ||
} | ||
}; | ||
function needsServe(containers, name) { | ||
var provides = calcProvides(containers[name]); | ||
// First calculate what all services this container provides. | ||
for (var key in containers) { | ||
if (!containers.hasOwnProperty(key)) continue; | ||
if (key === name) continue; | ||
if (calcDepends(containers[key], provides)) return true; | ||
// Check the config if it's not already checked. | ||
if (!config.checked) { | ||
try { | ||
checkConfig(config) | ||
} catch (err) { | ||
app.emit("error", err); | ||
} | ||
return false; | ||
} | ||
} | ||
exports.startContainers = startContainers; | ||
function startContainers(config, callback) { | ||
var hub = new EventEmitter(); | ||
var Agent = require('architect-agent').Agent; | ||
var running; | ||
var destructors = []; | ||
var containers = {}; | ||
(function startPlugins() { | ||
if (running) return; | ||
running = true; | ||
var pending = 0; | ||
do { | ||
var changed = false; | ||
config.forEach(function (plugin) { | ||
// Skip already-started and not-yet-ready plugins | ||
if (plugin.started) return; | ||
if (!plugin.consumes.every(function (name) { | ||
return services[name]; | ||
})) { return; } | ||
// This agent is used for the star topology of events. All child processes have access to it. | ||
var hubAgent = new Agent({ | ||
broadcast: broadcast | ||
}); | ||
pending++; | ||
// Create all the containers in parallel (as dumb workers). Then once | ||
// they are all created, tell them all to initialize in parallel. When | ||
// they are all ready, call the callback. | ||
var createLeft, readyLeft; | ||
createLeft = readyLeft = Object.keys(config.containers).length; | ||
// Create all the containers in parallel. | ||
Object.keys(config.containers).forEach(function (name) { | ||
var createContainer = (name === "master") ? | ||
require('./container').createContainer : | ||
spawnContainer; | ||
var imports = {}; | ||
plugin.consumes.forEach(function (name) { | ||
imports[name] = services[name]; | ||
}); | ||
plugin.started = true; | ||
plugin.setup(plugin, imports, function (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)); | ||
return app.emit("error", err); | ||
} | ||
services[name] = provided[name]; | ||
app.emit("service", name, services[name]); | ||
changed = true; | ||
}); | ||
if (provided && provided.hasOwnProperty("onDestroy")) | ||
destructors.push(provided.onDestroy); | ||
createContainer(name, broadcast, function (err, container) { | ||
if (err) throw err; | ||
containers[name] = container; | ||
broadcast("containerCreated", name); | ||
if (--createLeft) return; | ||
Object.keys(containers).forEach(function (name) { | ||
containers[name].initialize(config.containers[name]); | ||
pending--; | ||
app.emit("plugin", plugin); | ||
if (changed) { startPlugins(); } | ||
}); | ||
}); | ||
} while (changed); | ||
running = false; | ||
if (!pending) { | ||
app.emit("ready", app); | ||
} | ||
}()); | ||
this.destroy = function() { | ||
destructors.forEach(function(destroy) { | ||
destroy(); | ||
}); | ||
}); | ||
destructors = []; | ||
}; | ||
} | ||
inherits(Architect, EventEmitter); | ||
hub.on('containerReady', checkReady); | ||
// 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); | ||
if (callback) { | ||
app.on("error", onError); | ||
app.once("ready", onReady); | ||
function checkReady() { | ||
if (--readyLeft) return; | ||
broadcast("containersDone", Object.keys(containers)); | ||
callback(null, containers); | ||
} | ||
function onError(err) { | ||
app.removeListener("ready", done); | ||
app.destroy(); | ||
done(err, app); | ||
} | ||
// A function that all containers have access to that enables broadcasting. | ||
// The following kinds messages are broadcasts to all containers: | ||
// - serviceReady { container, socket, name, functions } | ||
// - servicesDone {} - all services are initialized | ||
// - serviceDied {serviceName} | ||
// - containerReady { container } | ||
// - containerDied {containerName} | ||
// - containersDone {} - all containers are initialized | ||
function broadcast(name, message) { | ||
if (config.console && config.console.info) { | ||
//console.info("BROADCAST: " + name, message); | ||
function onReady() { | ||
done(null, app); | ||
} | ||
hub.emit(name, message); | ||
process.nextTick(function () { | ||
Object.keys(containers).forEach(function (key) { | ||
if (typeof containers[key].onBroadcast !== "function") { | ||
console.log("containers[%s]", key, containers[key]); | ||
} | ||
containers[key].onBroadcast(name, message); | ||
}); | ||
}); | ||
} | ||
// Create a new container in a child process | ||
function spawnContainer(name, broadcast, callback) { | ||
var spawn = require('child_process').spawn; | ||
var Agent = require('architect-agent').Agent; | ||
var socketTransport = require('architect-socket-transport'); | ||
var child = spawn(process.execPath, [require.resolve('./worker-process.js')], { | ||
customFds: [-1, 1, 2], | ||
stdinStream: createPipe(true), | ||
env: { ARCHITECT_CONTAINER_NAME: name } | ||
}); | ||
var transport = socketTransport(child.stdin); | ||
hubAgent.attach(transport, function (container) { | ||
callback(null, container); | ||
}); | ||
child.stdin.resume(); | ||
// TODO: Monitor child for life | ||
var called = false; | ||
function done(err) { | ||
if (called) return; | ||
called = true; | ||
callback(err, app); | ||
} | ||
} | ||
return app; | ||
} | ||
// Taken from node's child process code. | ||
var Pipe; | ||
function createPipe(ipc) { | ||
// Lazy load | ||
if (!Pipe) { | ||
Pipe = process.binding('pipe_wrap').Pipe; | ||
} | ||
return new Pipe(ipc); | ||
} | ||
// 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 | ||
function resolvePackage(base, packagePath) { | ||
// This throws, make sure to wrap in try..catch | ||
var packagePathCache = {}; | ||
function resolvePackageSync(base, packagePath) { | ||
var originalBase = base; | ||
if (!packagePathCache.hasOwnProperty(base)) { | ||
packagePathCache[base] = {}; | ||
} | ||
var cache = packagePathCache[base]; | ||
if (cache.hasOwnProperty(packagePath)) { | ||
return cache[packagePath]; | ||
} | ||
if (packagePath[0] === "." || packagePath[0] === "/") { | ||
var newPath = path.resolve(base, packagePath, "package.json"); | ||
if (path.existsSync(newPath)) return newPath; | ||
var newPath = resolve(base, packagePath, "package.json"); | ||
if (existsSync(newPath)) { | ||
newPath = realpathSync(newPath); | ||
cache[packagePath] = newPath; | ||
return newPath; | ||
} | ||
} | ||
else { | ||
while (base) { | ||
var newPath = path.resolve(base, "node_modules", packagePath, "package.json"); | ||
if (path.existsSync(newPath)) return newPath; | ||
var newPath = resolve(base, "node_modules", packagePath, "package.json"); | ||
if (existsSync(newPath)) { | ||
newPath = realpathSync(newPath); | ||
cache[packagePath] = newPath; | ||
return newPath; | ||
} | ||
base = base.substr(0, base.lastIndexOf("/")); | ||
} | ||
} | ||
throw new Error("Can't find '" + packagePath + "' relative to '" + base + '"'); | ||
var err = new Error("Can't find '" + packagePath + "' relative to '" + originalBase + "'"); | ||
err.code = "ENOENT"; | ||
throw err; | ||
} | ||
{ | ||
"name": "architect", | ||
"description": "A Simple yet powerful plugin system for large-scale node applications", | ||
"version": "0.0.3", | ||
"author": "ajax.org B.V. <info@ajax.org>", | ||
"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.1", | ||
"author": "ajax.org B.V. <info@ajax.org>", | ||
"contributors": [ | ||
{ | ||
"name": "Tim Caswell", | ||
"email": "tim@c9.io" | ||
}, | ||
"dependencies": { | ||
"architect-agent": "~0.2.0", | ||
"architect-socket-transport": "~0.2.0" | ||
{ | ||
"name": "Fabian Jakobs", | ||
"email": "fabian@c9.io" | ||
}, | ||
"devDependencies": {}, | ||
"optionalDependencies": {}, | ||
"licenses" : [{ | ||
"type" : "MIT", | ||
"url" : "http://github.com/c9/architect/raw/master/LICENSE" | ||
}] | ||
{ | ||
"name": "Christoph Dorn", | ||
"email": "christoph@christophdorn.com" | ||
} | ||
], | ||
"main": "architect.js", | ||
"repository": { | ||
"type": "git", | ||
"url": "http://github.com/c9/architect.git" | ||
}, | ||
"dependencies": {}, | ||
"devDependencies": {}, | ||
"optionalDependencies": {}, | ||
"licenses": [ | ||
{ | ||
"type": "MIT", | ||
"url": "http://github.com/c9/architect/raw/master/LICENSE" | ||
} | ||
] | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 2 instances in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
0
2
4
1
21630
20
382
110
- Removedarchitect-agent@~0.2.0
- Removedarchitect-socket-transport@~0.2.0
- Removedarchitect-agent@0.2.2(transitive)
- Removedarchitect-socket-transport@0.2.2(transitive)
- Removedmsgpack-js@0.1.5(transitive)