This is a JavaScript application framework that allows us to maintain large, composable, isomorphic Node.js and Browser applications. It maintains a flexible, plugin-driven API that allows components to be separated by functionality while still sharing basic resources.
Here is a list of existing plugins that can be used with Appcore.
- appcore-log - Adds standardized console logging methods.
- appcore-config - Pulls in configuration from the cli and env.
Build a UMD bundle and use via a script tag. The variable Appcore
will be attached to window
.
<script type="text/javascript" src="appcore.js"></script>
var app = Appcore({ name: "myapp" });
If using Browserify or Node.js, you can install via NPM.
$ npm install appcore --save
var app = require("appcore")({ name: "myapp" });
To begin, make a new application instance. You can optionally pass in base configuration.
var app = Appcore({ name: "myapp" });
Appcore only helps to maintain large application infrastructure and makes no assumptions about the types of things an application might be doing. Appcore requires all functionality to be "adapted" through plugins so it can maintain compatibility with the rest of the system. Fortunately, plugins are easy to add:
// plugins are functions
app.use(function() {
console.log("This is my plugin!");
});
// or other apps
app.use(Appcore({ name: "myotherapp" }));
Every app has a life-cycle, maintained through a series of states, beginning at startup and ending when the app is fully running. Applications always want to enter the next state and will automatically do so unless prevented by a plugin using the wait()
method. In this way, app functionality can be initiated asynchronously, without interference from other plugins.
An app has a total of three executing states and one error state. An app will only enter the error state if it hasn't reached the final executing state. After the final state, errors are handled as normal. Here is the order states are performed in:
STARTUP --> READY --> RUNNING
You can listen for future state by using the quick methods of the same name. These methods are special in that if the app is already at that state or later, the function is executed immediately. All the states have the equivalent methods.
// log when app is ready
app.ready(function() {
console.log("My plugin is ready!");
});
// above is the equivalent of
app.startup(function() {
app.ready(function() {
console.log("My plugin is ready!");
});
});
As a plugin, you can prevent the app from moving to the next state by using app.wait()
to create a wait method that can be used asynchronously. In the below example, the .next()
function will not be called until the async task is finished.
app.use(function() {
doSomethingAsync(app.wait(function(err) {
// app enters FAIL state
if (err) return app.error(err);
// or app continues
console.log("did something async.");
}));
// log on the next state, READY
app.next(function() {
console.log("app has entered the next state!");
});
});
Once an application is constructed it will continue running until told not to. Make sure to apply all configuration before using plugins, since they are executed synchronously.
// set some configuration
app.set("myplugin", { do: "something" });
// add plugin that uses the config
app.use(function() {
var options = app.get("myplugin");
});
Application can be created as classes, so they can be extended and reused. This makes Appcore useful as a API format.
var Subapp = Appcore.create(function(options) {
this.defaults({ name: "myapp" })
this.set(options);
this.use(function() {
console.log("A custom application.");
});
});
var app = Subapp();
Creates a new appcore instance and immediately puts it into the STARTUP
state.
Applications created with this method are "plain" in that they are configured using the default method, which sets the first argument as the base options on the application. If you use .create()
or set a configure()
method, you can decide what to do with the arguments passed to the app.
var app = Appcore({ name: "myapp" });
Creates an appcore subclass. This is useful when creating an app that needs to used many times in the same environment.
configure
- The app's initialization method. This is run as soon as an instance is made, but after the app has entered the startup state. Arguments passed to the constructor are forwarded to this method.instanceProps
- An object of properties to attach to the subclass's prototype.classProps
- An object of properties to attach directly to the subclass.
// a subclass
var MyApp = Appcore.create(function(options) {
this.defaults({ name: "myapp" });
this.set(options);
}, {
foo: function() {
return this.get("foo");
}
});
// make any number of copies
var app1 = MyApp({ foo: "bar" });
var app2 = MyApp({ foo: true });
// call your custom method
app1.foo(); // "bar"
app2.foo(); // true
Adds any number of plugins to an application. The plugin should be a function or an Appcore instance. Function plugins are called with the application as context.
Plugins are technically run on the STARTUP
state. Since apps begin in startup, plugins are usually run immediately and synchronously.
Plugins generally add functionality to an app. Sometimes this leads to conflicts since plugins can be used more than once on the same application. To prevent conflicts, add a small self-awareness check at the start of the plugin.
app.use(function() {
// check if the variable already exists
if (this.myfn) return;
// add the custom functionality
this.myfn = function(){};
});
Calls the function fn
when the app is at or past the specified state. state
can be the string name of the state, or the integer value.
Apps run states in the following order. There is also a FAIL
state that the app can enter at anytime by calling the app.error()
method.
STARTUP -> READY -> RUNNING
This method is a little different from the emitted state events in that it operates with recursive eventing. Instead of listening for the exact state needed, onState()
listens for the next state and checks if it is the state needed. If so, fn
is called, otherwise it repeats the process by listening for the next state. This is very important because it leads to consistent ordering of events.
Let's demonstrate recursive eventing with two examples, one that uses .once()
and one that uses .onState()
.
When using the emitted events, the second state:running
event is called before the first one because of the order they end up running in. Using these methods can lead to code that is hard to reason about.
this.once("state:ready", function() {
this.once("state:running", function() {
console.log("first")
});
});
this.once("state:running", function() {
console.log("second");
});
// Prints:
// > second
// > first
The onState()
method fixes this by forcing the order they are declared in to be the order they are run.
this.onState("ready", function() {
this.onState("running", function() {
console.log("first")
});
});
this.onState("running", function() {
console.log("second");
});
// Prints:
// > first
// > second
Alias for app.onState("startup", fn)
.
Alias for app.onState("ready", fn)
.
Alias for app.onState("running", fn)
.
Alias for app.onState("fail", fn)
.
Calls function fn
when the app reaches the next state. If the app is at the RUNNING
or FAIL
state, fn
will never be called.
Prevents the app from moving to the next state. A function is returned that must be called to release the lock. The app will wait for all existing wait()
calls and will not proceed to the next state until all of the locks are released.
The wait method accepts a callback function that is called when the lock release function is called. In this way it can act as a wrapper around an asynchronous callback. The callback is called before the app moves to the next state, allowing for further wait()
calls.
var fs = require("fs");
// wait on data to arrive
app.startup(function() {
fs.readFile("./data.json", {
encoding: "utf-8"
}, app.wait(function(err, data) {
// pass error to app so it enters failing state
if (err) return app.error(err);
// load the configuration
app.data = JSON.parse(data);
}));
});
// won't be called until after data is on app
app.ready(function() {
console.log(app.data);
});
Let's the app know that something has gone wrong. err
can be any kind of error and is added to an internal app.errors
array.
If the app has not reached the RUNNING
state yet, the app is put into the FAIL
state on the next state change. Like the running state, the fail state is non-recoverable.
The app will emit an error
event with the error. This means that if there are no error listeners, the error is thrown and has the potential to crash the application. You can use appcore-log to log errors passed to this method, instead of throwing them.
Retrieves a value stored in the application's internal configuration by key. key
can be any complex path string, with parts separated by periods.
app.get();
app.get("mykey");
app.get("foo.bar.baz");
Sets value
at key
in the application configuration. If both the existing value at the key and value
are plain objects, value
is copied onto the existing object. Otherwise the value
replaces whatever was there.
app.set({ foo: { bar: true }});
app.set("foo", { hello: "world" });
app.get("foo"); // { bar: true, hello: "world" }
Sets value
at key
in the application configuration. This is slightly different from set()
in that no merging takes place, value
will be the absolute value at key
.
app.reset({ foo: { bar: true }});
app.reset("foo", { hello: "world" });
app.get("foo"); // { hello: "world" }
Sets value
at key
in the application configuration if and only if the existing value is undefined. This is like set()
in that plain objects are merged.
app.defaults({ foo: { bar: true }});
app.defaults("foo", { bar: "baz" });
app.get("foo"); // { bar: true }
Sets the value at key
to undefined.
app.set({ foo: { bar: true }});
app.unset("foo");
app.get("foo"); // undefined
Note: Appcore default options cannot be unset, since they are not properties of the application's options. Therefore something like the following will fail silently. Instead, set the value to null
.
app.unset("env");
app.get("env"); // "development"
app.set("env", null);
app.get("env"); // null
Gets browser safe configuration.
Generally there is configuration on your server that shouldn't be leaked to the client, things like passwords and secrets. To combat this, configuration must be explicitly whitelisted in order to be returned from this method. This can happen in two ways, adding a key to browserKeys
option or by setting a property within the browserOptions
object.
app.set("browserKeys", [ "public", "env" ]);
app.set("public", { foo: "bar" });
app.set("browserOptions.hello", "world");
app.getBrowserOptions(); // { public: { foo: "bar" }, env: "development", hello: "world" }
Sets value
at key
in browserOptions
. This is sugar for app.set("browserOptions", value)
.
app.setBrowserOption("hello", "world");
app.getBrowserOptions(); // { env: "development", hello: "world" }
These properties can be found on all app instances. All properties are immutable.
app.env
- The app environment as set in the configuration. By default this isprocess.env.NODE_ENV
, however it can be set withapp.set("env", "production")
.app.cwd
- The app's current working directory. By default this isprocess.cwd()
, but it can be set withapp.set("cwd", "/my/path")
.app.isRoot
- Whether or not the app has a parent Appcore application.app.isServer
- Whether or not the app is running on the server.app.isClient
- Whether or not the app is running in a browser.
Grunt is used to build a Browserify bundle from the original source found in lib/
. When the command below completes, the compiled and minified source will be saved to dist/
directory. These files can be used with most JavaScript interpreters such as Node.js and browsers.
$ npm run build
We have written several unit tests for Appcore. Run the command below to test Appcore on your machine:
$ npm test
You can also run the tests in your browser. Run the command below and point your browser to http://localhost:8000.
$ npm run test-browser