nilo
v4.0.11
Published
A dependency injection toolset for building applications
Downloads
57
Readme
nilo
("ex nihilo")
Ex nihilo is a Latin phrase meaning "out of nothing". It often appears in conjunction with the concept of creation, as in creatio ex nihilo, meaning "creation out of nothing"—chiefly in philosophical or theological contexts, but also occurs in other fields.
A dependency injection toolset for building applications. In most cases this wouldn't be used directly but via a framework.
const { App, main } = require('nilo');
const app = new App();
main(app);
Concepts
- Scope: A set of dependency definitions
for objects that share a lifecycle.
E.g. the
singleton
scope is for objects that are created once while objects in therequest
scope are created for each request. Scopes can be nested which means that objects in therequest
scope may depend on objects in thesingleton
scope but not vice versa. - Injector: An instance of a scope, holding the individual objects.
For each request there will be exactly one injector for the
request
scope. - Key: An id that is used to request a dependency.
Usage
The rest of the docs will use singleton->xyz
as a shorthand for
"the dependency with key xyz
provided in the singleton
scope".
const {
Project, // the project on disk, allows loading files etc.
Registry, // DI dependency registration
App, // running application, includes instances of Project and Registry
main, // run CLI command for an app
} = require('nilo');
App
The App
class brings all the other pieces together.
It is the primary interface of nilo
.
app.appDirectory
: The root directory of this app.app.project
: TheProject
for this app.app.registry
: TheRegistry
for this app.
new App(appDirectory, frameworkDirectory)
app.initialize(): Promise<void>
Loads object-graph
interface files which are used to declare dependencies.
The default export of each file is expected to be a function
that will be invoked with the registry
.
Example:
module.exports = registry => {
registry.singleton.setFactory('answer', null, () => 42);
};
app.configure(): Promise<void>
Runs the following steps:
- Run
app.initialize()
if it hasn't happened yet. - Run
singleton->configure[]
hooks. Their job is to make sure that the config is available. - Run
singleton->afterConfigure[]
hooks. These are basic bootstrapping hooks that may require configuration to be available. None of them should be specific to a particular kind of app.
Standard Dependencies
singleton->app
The app
instance that is currently running.
singleton->project
The project
used to load modules.
singleton->registry
The registry
used to register dependencies.
Project
A collection of helpers that can be used to load additional files relative to the app's root directory.
new Project(appDirectory, frameworkDirectory)
appDirectory
: The app's root directory.frameworkDirectory
: The directory of the framework. When methods refer to "bundled" dependencies, they're talking about dependencies loaded from here. This can be convenient to handlenpm link
correctly.
project.loadInterfaceFiles(basename: string)
Interface files are files that follow a specific naming convention and are found in well-known locations:
{lib,modules}/*/$basename.{js,mjs}
- For each
dependencies
$key
inpackage.json
, from$key/$basename
. - Outside of
NODE_ENV=production
also for eachdevDependencies
key.
This function returns an array with one entry for each interface file:
moduleNamespace
: The namespace record for the file.defaultExport
: Convenience property formoduleNamespace.default
.specifier
: The specifier the file was loaded from, relative to the app's root directory.group
: The directory the file was found in.
Note that .mjs
file support requires you to be using a version of node with
builtin support for ES Modules. Currently this is Node 10+ with
the --experimental-modules
flag. Node 10.x seems to experience
segfaults under certain conditions, so we recommend 12+.
Registry
A set of three scopes, in order of nesting:
singleton
: Scope for objects that are created once and then reused.request
: Scope for objects that are created for each request.action
: Scope for ephemeral objects that are created for one aspect of handling a request.
DependencyQuery
A dependency query is either a string or a DependencyDescriptor
object.
They are used both when asking for a dependency (ask for) and when providing
one (provide). The following kinds of dependency queries are recognized,
each assuming that the resulting dependency is called x
:
x
: A required dependency (provide or ask for). It is expected to be provided exactly once. Descriptor:{ key: 'x' }
.x?
: An optional dependency (ask for only). If it is provided, it is expected to be provided exactly once. Descriptor:{ key: 'x', optional: true }
.x[]
: Getting all values of a multi-valued dependency (ask for only). It may be provided multiple times and all will be injected as an array. Descriptor:{ key: 'x', multiValued: true }
.x[y]
: A specific element of a multi-valued dependency with a uniqueindex
(provide or ask for). When providing a multi-valued dependency, this form has to be used. Descriptor:{ key: 'x', multiValued: true, index: 'y' }
.
registry.getProviderGraph()
Returns a structured object with information about all registered providers, where they have been registered, and what their dependencies are. This data can be used to provide inspection and other developer tooling.
registry.getSingletonInjector()
Creates an Injector
for the singleton
scope.
The injector returned will always be the same instance.
registry.getRequestInjector(request, response)
Creates an Injector
for the request
scope.
Both 'request'
and 'response'
will be available as dependencies.
For the same request
, it will return the same injector instance.
registry.getActionInjector(request, response, action)
Creates an Injector
for the action
scope,
based on the request
injector for the given request
object.
In addition to 'request'
and 'response
',
'action'
will be as a dependency.
It will always return a new injector.
For testing or trivial uses, you may omit all of the arguments to be given empty default values.
Registry.from(([scope, name, value] | (registry) => void)[]): Registry
Sometimes you might wish to create a registry from scratch, all-at-once,
particularly when testing. In this case, you may call the static method
Registry.from()
, passing it an array which contains items, each of which
is either:
- an tuple specifying a static entry on the specified
scope
- a function which accepts the
registry
being constructed and makes whatever calls on it it likes
This lets you do something like this:
const deps = Registry.from([
['singleton', 'x', 42],
['request', 'y', 88],
require('../one/object-graph'),
require('../another/object-graph'),
]).getActionInjector().getProvider();
// ^ this is a slightly neater way of doing something like:
const reg = new Registry();
reg.singleton.setValue('x', 42);
reg.request.setValue('y', 42);
require('../one/object-graph')(reg);
require('../another/object-graph')(reg);
const deps = reg.getActionInjector().getProvider();
injector.get(key)
Resolve the dependency specified the DependencyQuery
in key
and return the resulting object.
registry.singleton.setFactory('x', null, () => 'x-value');
const injector = registry.getSingletonInjector();
const x = injector.get('x');
x === 'x-value';
const y = injector.get('y?');
y === null;
// throws because `y` hasn't been provided:
injector.get('y');
For multi-valued dependencies,
the result will be an array with named properties for each index
.
Example:
registry.singleton.setFactory('x[a]', null, () => 'a-value');
registry.singleton.setFactory('x[b]', null, () => 'b-value');
const x = registry.getSingletonInjector().get('x[]');
x[0] === x.a && x.a === 'a-value';
x[1] === x.b && x.b === 'b-value';
injector.keys()
Returns an array of all registered dependency keys that could be created using this injector
.
injector.getProvider()
Get a magical proxy object that can be used to read dependencies.
While very convenient, it should be used sparingly.
Roughly speaking, reading properties from the provider is equivalent of passing the key to injector.get
.
scope.setFactory(key, deps, factory)
key
: ADependencyQuery
that this factory can fulfil.deps
: An array ofDependencyQuery
s that this factory depends on. If there are no dependencies, it may benull
.factory
: A function that takes an object with the fulfilled dependencies.
registry.singleton.setFactory(
'projectRootLength',
['project'],
({ project }) => project.root.length
);
registry.singleton.setFactory('pid', null, () => process.pid);
scope.setValue(key, value)
A convenience method for when a factory would always return the same value,
especially handy for things in the singleton
scope.
main(app, defaultCommand = 'start', argv = process.argv)
- Run
app.initialize()
. - Discover all available commands from
singleton->commands[]
. - Parse CLI options and run selected command, defaulting to
defaultCommand
. - Exit once the command resolves.