File-based hapi plugin composer
Note
Haute-couture is intended for use with hapi v19+ and nodejs v12+, in addition to several optional peer hapi plugins noted in package.json (see v3 for lower support).
Composes server
by making calls into hapi from file and directory contents as described in Files and directories. For example, the contents of each file in routes/
may be used to make a call to server.routes()
and define a hapi route. The same goes for many other directories, and many other methods in the hapi server/plugin interface starting here. Typically HauteCouture.compose()
is called as a subroutine of hapi plugin registration, and options
are plugin registration options:
const HauteCouture = require('@hapipal/haute-couture');
module.exports = {
name: 'my-hapi-plugin',
async register(server, options) {
// Custom plugin code can go here
await HauteCouture.compose(server, options);
}
};
When composeOptions
is specified then it may define the following properties:
dirname
- an absolute directory path in which to look for the files and directories described below, as well as those fromamendments
. It defaults to the directory path of the caller.amendments
- an object whose keys are directories and whose values are configuration about how to interpret the contents of that directory into calls to hapi. The keys represent directories relative todirname
, and we call these "places." The values follow the format of a haute manifest item (except the propertyplace
, which is defined by the value's key), and we call these "amendments".
There are many amendments that haute-couture uses to provide its default behavior, as described in Files and directories. In the case that amendments
defines an amendment at a place for which haute-couture has a default, the contents of amendment
will override the default. If amendments
contains a key whose value is false
, that place specified by that key will be ignored by haute-couture. You may also specify the key HauteCouture.default
or '$default'
to define defaults for all items. The following are amendment settings that can be changed through defaults:
recursive
- this option causes files to be picked-up recursively within their directory rather than just files that live directly underplace
. Flip this tofalse
if you would prefer not to have a nested folder structure, e.g.routes/users/login.js
versusroutes/users-login.js
.stopAtIndexes
- when used with therecursive
option, setting this option totrue
causes files adjacent to an index file (e.g.index.js
) to not be recursed. For example, of the filesroutes/users/index.js
androutes/users/schema.js
, the first would be seen by haute-couture and the second would be excluded.include
- may be a function(filename, path) => Boolean
or a RegExp wherefilename
(a filename without extension) andpath
(a file's path relative toplace
) are particular to files underplace
. When this option is used, a file will only be used as a call when the function returnstrue
or the RegExp matchespath
.exclude
- takes a function or RegExp, identically toinclude
. When this option is used, a file will only be used as a call when the function returnsfalse
or the RegExp does not matchpath
. This option defaults to exclude any file that lives under a directory namedhelpers/
.meta
- an object containing any meta information not required by haute-couture or haute, primarily for integration with other tools.
In addition to these settings and the standard haute item properties, you may also specify the following on an amendment:
before
- a single or array ofplace
values for which the given item should be positioned prior to other items. This modifies the order of the calls made to hapi onserver
.after
- a single or array ofplace
values for which the given item should be positioned subsequent to other items in the manifest. This modifies the order of the calls made to hapi onserver
.example
- an example value for this item (i.e. file contents withinplace
), primarily used by the hpal CLI to scaffold new files atplace
.
See the amendment example below for illustration, noting that the format of .hc.js
and the format of composeOptions.amendments
are identical.
When a call to HauteCouture.compose(server, options, composeOptions)
specifies no composeOptions.amendments
, haute-couture will check the relevant directory composeOptions.dirname
for a file named .hc.js
. Any amendments exported by this file are used identically to amendments passed as an argument. This is a nice way to keep haute-couture-related configuration separate from your plugin code, and also offer a standard way for tools such as the pal CLI to cater to your particular usage of haute-couture.
Haute-couture supports other file extensions as well, such as .hs.json and .hc.ts.
This example demonstrates how to use a .hc.js
file in order to swap-out schwifty's handling of Objection ORM models for a much simplified handling of Mongoose models. You can even continue to use hpal make
to scaffold your Mongoose models inside the models/
directory.
'use strict';
const HauteCouture = require('@hapipal/haute-couture');
const Mongoose = require('mongoose');
module.exports = {
name: 'my-hapi-plugin',
register: async (server, options) => {
// When registering this plugin pass something like this as plugin options:
// { mongoURI: 'mongodb://localhost/test' }
server.app.connection = Mongoose.createConnection(options.mongoURI);
await HauteCouture.compose(server, options);
}
};
'use strict';
module.exports = {
models: {
list: true,
signature: ['name', 'schema'],
method: (server, options, name, schema) => {
const { connection } = server.app;
// Access the Dog model as such in a route handler:
// const { Dog } = request.server.app.models;
server.app.models = server.app.models || {};
server.app.models[name] = connection.model(name, schema);
},
// This example below isn't essential. But it allows you to use
// `hpal make model <model-name>` in order to scaffold your
// Mongoose models from the command line.
example: {
$requires: ['mongoose'],
$value: {
name: 'ModelName',
schema: { $literal: 'new Mongoose.Schema({})'}
}
}
}
};
'use strict';
// Scaffolded using the CLI:
// npx hpal make model dog
const Mongoose = require('mongoose');
module.exports = {
name: 'Dog',
schema: new Mongoose.Schema({
name: String
})
};
Returns a function with the signature async function(server, options)
, identical in meaning to the signature of a hapi plugin. This function behaves identically to HauteCouture.compose(server, options, composeOptions)
with the final argument fixed.
module.exports = {
name: 'my-plugin',
register: HauteCouture.composeWith({
amendments: {
$default: {
recursive: false
}
}
})
};
Returns the default amendment at place
. For example, HauteCouture.amendment('auth/strategies')
will return the default amendment that defines the call to server.auth.strategy()
. When patch
is specified then it will be used to alter the returned amendment. When patch
is a function then it will be passed the default amendment and should return a patch to alter that amendment as described above.
await HauteCouture.compose(server, options, {
amendments: {
routes: HauteCouture.amendment('routes', {
recursive: false
})
}
});
This is most likely to be used by tooling that is interoperating with haute-couture, and isn't a part of everyday usage.
Returns haute-couture's default amendments with amendments
overrides applied. The result is an object whose keys are place
s and values are amendments as described in documentation for HauteCouture.compose()
.
This is most likely to be used by tooling that is interoperating with haute-couture, and isn't a part of everyday usage.
Returns the hapi haute manifest, incorporating optional amendments
to the manifest as described in documentation for HauteCouture.compose()
. Haute requires each manifest item have an item.dirname
, which will be set if dirname
is specified.
A symbol that may be used as a key to specify amendment defaults anywhere amendments
are passed. Note that '$default'
may also be used as a key for this purpose. When HauteCouture.default
and '$default'
both appear in amendments
, the value at HauteCouture.default
is used to determine defaults and the value at '$default'
is interpreted as a place (i.e. a directory named $default/
).
await HauteCouture.compose(server, options, {
amendments: {
// Do not look recursively into directories anywhere aside from routes/
[HauteCouture.default]: {
recursive: false
},
routes: HauteCouture.amendment('routes', {
recursive: true
})
}
});
We've worked hard to make haute-couture astute in mapping files and directories to hapi calls defining your server or plugin, on occasion even inferring pieces of configuration from filenames.
Here's the intuition for what's happening (it's pretty simple!). If a file place/my-file.js
exports contents
, then haute-couture will make a call on your server server.place(contents)
. For example, if { method, path, handler }
is exported from routes/my-route.js
, then haute-couture will define a route for you by calling server.route({ method, path, handler })
. The behavior is configurable as described in HauteCouture.compose()
using "amendments", but most of the time this is all that's happening. You will find that haute-couture is a pretty thin adapter between file contents and your hapi server.
Files will always export an array of values (representing multiple calls into hapi) or a single value (one call into hapi). When a hapi method takes more than one argument, a single value consists of an object whose keys are the names of the arguments and whose values are the intended argument values. The format of the argument values come from the hapi API unless otherwise specified.
For example, a file defining a new server method (representing a call to server.method(name, method)
) would export an object of the format { name, method }
.
Lastly, files can always export a function with signature function(server, options)
or async function(server, options)
that returns the intended value or array of values.
Here's the complete rundown of how files and directories are mapped to calls on your hapi server
. The order here reflects the order in which the calls would be made.
Note
You'll see that this library can be used in conjunction with several hapi plugins. Here are those plugins and their supported versions. When in doubt, you may reference the peer dependencies listed in this module's package.json.
- schwifty - v6+
- schmervice - v2+
- nes - v11+
- vision - v5+
path.js
- exportrelativeTo
orfunction(server, options)
that returnsrelativeTo
.path/index.js
- exportrelativeTo
orfunction(server, options)
that returnsrelativeTo
.
caches.js
- export an array ofoptions
orfunction(server, options)
that returns array ofoptions
.caches/index.js
- export an array ofoptions
orfunction(server, options)
that returns array ofoptions
.caches/some-cache-name.js
- exportoptions
orfunction(server, options)
that returnsoptions
. The cache'soptions.name
will be assigned'cache-name'
from the filename if a name isn't already specified.
plugins.js
- export an array ofplugins
orfunction(server, options)
that returns an array ofplugins
. Note thatplugins
typically takes the form{ plugin, options, once, routes }
.plugins/index.js
- export an array ofplugins
orfunction(server, options)
that returns an array ofplugins
.plugins/plugin-name.js
- exportplugins
orfunction(server, options)
that returnsplugins
. If a plugin isn't specified inplugins
it will berequire()
d using the filename. Scoped plugins may also be specified using a dot (.
) as a separator between the scope and the package name, e.g.plugins/@my-scope.my-package.js
would register the pluginrequire('@my-scope/my-package')
.
View manager (for vision)
view-manager.js
- exportoptions
orfunction(server, options)
that returnsoptions
.view-manager/index.js
- exportoptions
orfunction(server, options)
that returnsoptions
.
decorations.js
- export an array of objects{ type, property, method, options }
orfunction(server, options)
that returns an array of objects.decorations/index.js
- export an array of objects orfunction(server, options)
that returns an array of objects.decorations/decoration-name.js
- export an object orfunction(server, options)
that returns an object. Theproperty
will be assigned'decorationName'
camel-cased from the filename and path parts if it isn't already specified.decorations/[type].decoration-name.js
- export an object orfunction(server, options)
that returns an object. Thetype
will be inferred from the filename if it isn't already specified.decorations/[type]/decoration-name.js
- export an object orfunction(server, options)
that returns an object. Thetype
will be inferred from the path if it isn't already specified.
expose.js
- export an array of objects{ key, value }
orfunction(server, options)
that returns an array of objects.expose/index.js
- export an array of objects orfunction(server, options)
that returns an array of objects.expose/property-name.js
- export an object orfunction(server, options)
that returns an object. Thekey
will be assigned'propertyName'
camel-cased from the filename if it isn't already specified.
cookies.js
- export an array of objects{ name, options }
orfunction(server, options)
that returns an array of objects.cookies/index.js
- export an array of objects orfunction(server, options)
that returns an array of objects.cookies/cookie-name.js
- export an object orfunction(server, options)
that returns an object. Thename
will be assigned'cookie-name'
from the filename if it isn't already specified.
Model definitions (for schwifty)
models.js
- export an array ofmodels
orfunction(server, options)
that returns an array ofmodels
.models/index.js
- export an array ofmodels
orfunction(server, options)
that returns an array ofmodels
.models/model-name.js
- exportmodels
orfunction(server, options)
that returnsmodels
.
Service definitions (for schmervice)
services.js
- export an array ofserviceFactory
s orfunction(server, options)
that returns an array ofserviceFactory
s.services/index.js
- export an array ofserviceFactory
s orfunction(server, options)
that returns an array ofserviceFactory
s.services/service-name.js
- exportserviceFactory
orfunction(server, options)
that returnsserviceFactory
.
bind.js
- exportcontext
orfunction(server, options)
that returnscontext
.bind/index.js
- exportcontext
orfunction(server, options)
that returnscontext
.
dependencies.js
- export an array of objects{ dependencies, after }
orfunction(server, options)
that returns an array of objects.dependencies/index.js
- export an array of objects orfunction(server, options)
that returns an array of objects.dependencies/plugin-name.js
- export an object orfunction(server, options)
that returns an object.dependencies
will be derived from the filename if it is not already specified.
methods.js
- export an array of objects{ name, method, options }
orfunction(server, options)
that returns an array of objects.methods/index.js
- export an array of objects orfunction(server, options)
that returns an array of objects.methods/method-name.js
- export an object orfunction(server, options)
that returns an object. Thename
will be assigned'methodName'
camel-cased from the filename if it isn't already specified.
extensions.js
- export an array ofevents
orfunction(server, options)
that returns an array ofevents
.extensions/index.js
- export an array ofevents
orfunction(server, options)
that returns an array ofevents
.extensions/[event-type].js
- exportevents
orfunction(server, options)
that returnsevents
. Thetype
(of each item if there are multiple) will be assigned[event-type]
camel-cased from the filename if it isn't already specified. E.g.onPreHandler
-type events can be placed inextensions/on-pre-handler.js
.extensions/[event-type]/name.js
- exportevents
orfunction(server, options)
that returnsevents
. Thetype
(of each item if there are multiple) will be assigned[event-type]
camel-cased from the path if it isn't already specified. E.g.onPreHandler
-type events can be placed inextensions/on-pre-handler/my-handlers.js
.
auth/schemes.js
- export an array of objects{ name, scheme }
orfunction(server, options)
that returns an array of objects.auth/schemes/index.js
- export an array of objects orfunction(server, options)
that returns an array of objects.auth/schemes/scheme-name.js
- export an object orfunction(server, options)
that returns an object. Thename
will be assigned'scheme-name'
from the filename if it isn't already specified.
auth/strategies.js
- export an array of objects{ name, scheme, options }
orfunction(server, options)
that returns an array of objects.auth/strategies/index.js
- export an array of objects orfunction(server, options)
that returns an array of objects.auth/strategies/strategy-name.js
- export an object orfunction(server, options)
that returns an object. Thename
will be assigned'strategy-name'
from the filename if it isn't already specified.
auth/default.js
- exportoptions
orfunction(server, options)
that returnsoptions
.auth/default/index.js
- exportoptions
orfunction(server, options)
that returnsoptions
.
Subscriptions (for nes)
subscriptions.js
- export an array of objects{ path, options }
orfunction(server, options)
that returns an array of objects.subscriptions/index.js
- export an array of objects{ path, options }
orfunction(server, options)
that returns an array of objects.subscriptions/service-name.js
- export an object{ path, options }
orfunction(server, options)
that returns an object.
validator.js
- exportvalidator
orfunction(server, options)
that returnsvalidator
.validator/index.js
- exportvalidator
orfunction(server, options)
that returnsvalidator
.
routes.js
- export an array ofroute
orfunction(server, options)
that returns an array ofroute
.routes/index.js
- export an array ofroute
orfunction(server, options)
that returns an array ofroute
.routes/route-id.js
- exportroute
orfunction(server, options)
that returnsroute
. Ifroute
is a single route config object, the route'sconfig.id
will be assigned'route-id'
from the filename if it isn't already specified. The filename could just as easily represent a group of routes (rather than an id) and the file could export an array of route configs.
Structure of a haute manifest item
A haute manifest item describes the mapping of a file/directory's place and contents to a call to the hapi plugin (server
) API. In short, the place is mapped to a hapi plugin method, and the file contents are mapped to arguments for that method. It is an object of the form,
place
- a relative path to the file or directory, typically excluding any file extensions. E.g.'auth/strategies'
or'plugins'
.method
- the name of the method in the hapi plugin API. May be a deep method. E.g.'auth.strategy'
or'register'
. Also may be a function with signature(server, options, ...values) => void
wherevalues
are the call's arguments, originating from file contents (seesignature
below).signature
- (optional) an array of argument names taken by the hapi plugin's method. When omitted the entire file contents are passed as the sole argument. An argument may be marked as optional by surrounding it in brackets[]
. E.g.['name', '[options]']
would map file contents of the form{ name, options }
to a callserver.someMethod(name, options)
, and{ name }
to a callserver.someMethod(name)
.list
- (optional) whentrue
, indicates to call the hapi plugin method on either,- each item in an array exported at
place
, whenplace
represents a single file (e.g.plugins.js
) or a directory with an index file (e.g.plugins/index.js
) or, - each value exported by the files within
place
whenplace
is a directory without an index file (e.g.plugins/vision.js
,plugins/inert.js
).
- each item in an array exported at
useFilename
- (optional) whenlist
istrue
andplace
is a directory without an index file, then this option allows one to use the name of the each file withinplace
to modify its contents. Should be a function with signaturefunction(filename, value, path)
that receives the file'sfilename
(without file extension); its contents atvalue
; and the file's path relative toplace
. The function should return a new value to be used as arguments for hapi plugin API call.recursive
- whentrue
andlist
is in effect, this option causes files to be picked-up recursively withinplace
rather than just files that live directly underplace
.stopAtIndexes
- when used with therecursive
option, setting this option totrue
causes files adjacent to an index file (e.g.index.js
) to not be recursed.include
- may be a function(filename, path) => Boolean
or a RegExp wherefilename
(a filename without extension) andpath
(a file's path relative toplace
) are particular to files underplace
. When this option is used, a file will only be used as a call when the function returnstrue
or the RegExp matchespath
.exclude
- takes a function or RegExp, identically toinclude
. When this option is used, a file will only be used as a call when the function returnsfalse
or the RegExp does not matchpath
. This option defaults to exclude any file that lives under a directory namedhelpers/
.meta
- an object containing any meta information not required by haute-couture or haute, primarily for integration with other tools.