It's a working title.
This isn't so much a framework per se as it is a light wrapper around express
. Its duties encompass configuration loading, logging, and application lifecycle flow. Each of these is covered in a bit more detail below.
This includes a little boilerplate that allows the product to function as a module or a standalone executable.
var App = require('@myndzi/app-framework');
var extend = require('extend');
module.exports = function (_config) {
var config = extend({
root: __dirname,
name: 'myCoolApp',
modules: ['database', 'middleware', 'routes']
}, _config);
return App(config).call('listen');
};
if (require.main === module) {
module.exports();
}
app-framework
will process and react to certain commands given on the command line if enabled. The environment variable ALLOW_COMMANDS
or switch --allow-commands
must be passed to enable this behavior. When enabled, the following behaviors can be used:
- self-test - binds a handler that calls app.shutdown() after the 'listening' event is emitted. Can be used to exercise the entire async startup process.
- dump-config - disables all logging and binds two handlers: -- 'loaded' - dumps a pretty-printed JSON representation of the app config to stdout -- 'listening' - calls app.shutdown
If using npm scripts to run your app, you can execute these commands like so:
ALLOW_COMMANDS=true npm start self-test
ALLOW_COMMANDS=true npm -s start dump-config
The -s switch makes npm silent, so that you get pure JSON from stdout instead of the extra NPM logging at the beginning.
app-framework
utilizes convict
for configuration loading. Convict allows you to define a config schema; this schema can specify bindings to environment variables or command line switches. The result is a config object that's been validated against its schema and sourced from environment variables, command line arguments, and (possibly multiple) configuration files, making it extremely flexible.
app-framework
itself will attempt to load the following files:
- A configuration schema located in
<appRoot>/config/.schema.js
- A global default configuration file located in
<appRoot>/config/all.js
- An environment configuration file located in
<appRoot>/config/<environment>.js
- A system-local configuration file located in
/etc/<appName>.js
- A user-local configuration file located in
<homeDir>/.<appName>.js
All configuration files are .js
files, not .json
files; they must be valid node files, which creates a little boilerplate wrapping everything like module.exports = { ... };
, but being executable allows for a little logic when needed, for example to construct a path via the path
module. This may go back to json in future versions, as the conditional-logic requirements have mostly been solved by the evolution of this package itself.
app-framework
will also add to the existing config schema, when not present, the following items:
app.env
- the environment the application is running in, an enum:- development
- testing
- production
app.root
- the root directory of the applicationapp.name
- the name of the applicationlog.level
- configurable level for log output, an enum:- trace
- debug
- info
- warn
- error
- fatal
- none
These items obtain the following default values, if not specified:
app.env
-testing
if there is a global functiondescribe
defined,development
otherwiseapp.root
- the first parent directory ofrequire.main.filename
that contains apackage.json
fileapp.name
- the directory name of the app rootlog.level
- Depends on the environment:- For environment
testing
, log levelnone
- For environment
development
, log leveldebug
- Otherwise, log level
warn
- For environment
You can redefine these default schema items so long as your new definition is compatible. The above settings are used during pre-configuration (more on pre-configuration in the bootstrap section)
A few other config keys are expected/used by app-framework
, though it does not define a schema for them:
app.port
- the port to listen onapp.ip
- the ip to listen onapp.maxListeners
- configure the maxListeners value of theapp
event emitter; most likely to suppress warnings about too many listeners, e.g. on theshutdown
event- Log configuration (see below)
No logger will be loaded if the app is instantiated with the config option log.level
equal to none
.
Otherwise, app-framework
looks for the following configuration data:
log.types
- An array containing strings matching he types of log transports to enable; an enum:file
screen
syslog
ringbuffer
log.file.filename
- Whenfile
is selected, this is the filename to log messages to; default islog/app.log
; when a relative path is given, it is taken relative toapp.root
log.syslog.facility
- The facility to use when sending syslog messageslog.syslog.connection
- Connection options to pass to syslog2log.ringbuffer.limit
- Not yet implemented. Number of lines to retain in the ring buffer.
ringbuffer
is a special type of log that keeps a circular buffer of the last X log messages. Its intended use is to store log messages for dumping on unexpected exit (for example, when no screen logging is in use), but this behavior is not yet implemented.
Since this section can be disabled entirely, app-framework
does not create the schema automatically like the core config values above.
A basic stdout/stderr logger is used for bootstrapping and shutdown; these messages will not be captured by the configured log methods.
app-framework
implements a somewhat complex bootstrapping method designed to allow configuration of logging as well as logging of configuration, and promise-based asynchronous startup and shutdown. The startup flow goes like this:
- Pre-configuration
- Basic logger
- Full configuration
- Full logging
- Configured modules
Each of these steps is explained below:
This loads the core schema described above, and loads any injected config data relevant to the core schema taken from:
- Injected config (passed to
App()
when instantiating) - Environment variables
- Command line arguments
... along with defaults calculated as described in the Configuration section above.
This core set of data is used to locate and load other config files and so on, later.
If log.level
is none, this extends app
with a log
stub full of no-ops. Otherwise, it loads a basic logger that sends warn
and higher messages to stderr and info
and lower messages to stdout, prefixed with their level, such as: INFO: some log message
.
This behavior is not affected by the main config files, since they haven't been loaded yet.
This step finds and loads the full schema and any config files; it logs some debug output during this process, which is handled by the basic logger
This step takes the processed configuration and instantiates the configured log transports, extending app
with a full Bunyan instance at app.log
The modules
key passed on instantiation (App({ modules: [ ... ] })
) defines directories within <appRoot>/app
to load code from. Directories listed here will be searched for .js
files to load, and those files will be loaded accordingly. This method should soon be deprecated in favor of calling app.loadFiles
explicitly. Detailed behavior will be described in that section.
Returns a promise for the instantiated application.
App({
root: '/app/root',
name: 'myCoolApp',
modules: ['modules', 'to', 'load']
});
All .js
files in <appRoot>/app/<module>
are loaded for each item in the modules
array; directory loading is described in more detail under app.loadFiles
Merges an object into the app configuration:
app.configure({
caching: {
enabled: false
}
});
This doesn't necessarily make your app change behavior; for that, use the following:
Emitted when new configuration is loaded via app.configure()
app.on('configure', function (newConfig) {
// update behavior
});
Contains the configured environment
Contains the application root
A helper to load modules from <appRoot>/lib
:
var helper = app.lib('helper');
Equivalent to:
var helper = require('path').join(app.root, 'helper');
Allows for loading of lib
-dir files without needing to know your position in the file hierarchy
Returns a path relative to <appRoot>/app
Logs a message of the given level. Follows bunyan's semantics.
app.log.info('Hi guys');
app.log.error(new Error('oh no!'));
app.log.debug('loaded file: %s', filename);
Start the app listening on app.port
:app.ip
; returns a promise.
Emitted when a server has been created for the app:
app.on('server', function (server) {
// ...
});
Emitted when the server is listening:
app.on('listening', function (address, port, env) {
// ..
});
Equivalent to app.shutdown(EXIT_CODES.RELOAD, msg, timeout)
Shuts down the app. All arguments are optional. code
is the exit code to exit with; msg
is a message / reason; timeout is a number in milliseconds to wait for async shutdown handlers to clean up before giving up.
app.shutdown
does not actually call process.exit()
anymore; instead, it leaves that behavior to its caller.
code
defaults to EXIT_CODES.UNKNOWN
.
If all shutdown handlers concluded, the resolved value is the value of code
.
If the timeout is specified and expires before handlers have concluded, the code becomes EXIT_CODES.SHUTDOWN_TIMEOUT_EXCEEDED
.
Defined shutdown codes are in lib/exit-codes
, and as of this writing are as follows:
- RELOAD: -1
- OK: 0
- SHUTDOWN_NOT_AVAILABLE: 1
- SHUTDOWN_TIMEOUT_EXCEEDED: 2
- SERVER_ERROR: 3
- UNKNOWN: 99
Emitted before the shutdown process begins. code
, msg
, and timeout
are passed to this event after interpretation/defaults:
app.on('before shutdown', function (code, msg, timeout) {
app.log.warn('App shutting down: %s', msg);
});
Emitted when the app actually shuts down. Shutdown handlers that require asynchronous cleanup may indicate this by passing a promise or callback-expecting function like so:
app.on('shutdown', function (await) {
await(asyncShutdown); // callback
await(asyncShutdown$()); // promise
});
You may call await
as many times as you want, or even return your promise, though that is not recommended since it is an atypical use of event handlers.
Emitted when shutdown has concluded. If shutdown concludes due to a timeout, an object is passed like this:
{
code: <exit code>,
cleanupHandlers: [promises]
}
If shutdown does not time out, this object is passed instead:
{
code: <exit code>,
errors: [errors]
}
The latter provides an array with any/all collected errors during the shutdown process; this includes synchronous throws from event handlers as well as promise rejections and callbacks that gave error arguments.
Synchronous values and callbacks all get converted to promises by the await()
function; these promises are collected in an array and passed as cleanupHandlers
in the case of a timeout. This is primarily useful to try and figure out which handlers stalled.
Loads the files from <appRoot>/app/type
. Extends app
with a map app[type]
which maps filenames to their exports.
Loaded files are expected to export a function with the signature function (app[, ..opts]) { }
Files are required, then called with the above signature. Opts can be any number of arguments, and arguments[1]
passed to app.loadFiles()
will be arguments[1]
when calling the module's exported function.
All .js
files are loaded in arbitrary order; if you need more control over which files are loaded or their order, you can put an index.json
file in the directory in question containing an ordered array of which files to load.
Example:
myCoolApp/app/routes/foo.js
myCoolApp/app/routes/bar.js
myCoolApp/app/routes/baz.js
myCoolApp/app/routes/index.json
If you wanted to ensure the routes were loaded in the order 'baz', 'bar', 'foo', index.json
should contain:
['baz.js', 'bar.js', 'foo.js']
If you wanted to only load foo.js
, index.json
should contain:
['foo.js']
If foo.js
contained something like the following:
module.exports = function (app) {
return function () { console.log('foo!'); }
};
Then after routes
was loaded, there would be a key, app.routes.foo
which contained the function function () { console.log('foo!'); }
Emitted when the app bootstrap process has completed. This will not include any promise handlers bound to the result of calling App().