A simple framework to create customizable engines
Customize is an abstraction of bootprint's the merging-behaviour. It allows you to create your own projects and engines (other than Less and Handlebars) and create overridable configurations for those.
At its core, it uses lodash#mergeWith to merge configurations. It uses a customizer-function that supports promises and custom overrider functions attached to the object.
npm install customize
The following example should demonstrate the usage of Customize and the files
io-helper. Consider the following file tree
├─┬ dir1/
│ ├── a.md
│ └── b.md
├─┬ dir2/
│ └── a.md
├── engine-concat-files.js
├── example-buildConfig.js
├── example1.js
└── example2.js
The first thing we need, is an engine. For now, we create an engine that just
concatenates the contents of all files in a directory. We put this engine into
the file engine-concat-files.js
var files = require('customize/helpers-io').files
module.exports = {
// Optional input schema for engine-configurations
// If this is present, the JSON will be validated before being passed into "preprocessConfig"
schema: {
description: 'Path to a directory containing files',
type: 'string'
},
// Initial configuration when registering the engine.
defaultConfig: null,
// Files/Dirs to-be-watched with the default configuration
defaultWatched: [],
// This function is called for any `.merge` input.
// It converts the input into its mergable form
preprocessConfig: function (config) {
return files(config)
},
// This function is called to determine the files and directories
// to watch in developmentMode
watched: function (config) {
return [
// The config itself is the directory-path
config
]
},
// Runs the engine with a resolved configuration.
// The config contains no Promises anymore.
// The function returns an object
//
// {
// "filename.txt": "file-contents"
// }
//
run: function (config) {
var result = ''
Object.keys(config).forEach(filename => {
result += config[filename].contents + '\n'
})
return {
// Return a file called "concat.txt"
'concat.txt': result
}
}
}
-
The engine provides an empty default configuration. This configuration is used as long as no
.merge
and.load
function is called. -
The
preprocessor
of the engine assumes that the input configuration for this engine a path to a directory. It then uses thefiles
io-helper to convert this path into an object of lazy promises. -
The
run
-function concatenates the contents of the files. It returns an object{ "filename.txt": "contents", ... }
output file. The module customize-write-files can be used to write such files to disk in a node environment. In order to this to work, the contents must either be a string, a buffer or a readable stream. Strings will be stored in
utf-8
encoding.
In order to see, how the preprocessor and the files
-helper works, we can display
the configuration after a merge:
var customize = require('customize')
// Load files from one directory and merge with second
customize()
.registerEngine('files', require('./engine-concat-files'))
.merge({
files: 'dir1'
})
.buildConfig()
.then((result) => console.log(result.files))
The example creates a new Customize-instances, registers our engine under the name
files
and provides the path to a directory as configuration for the files
engine
(i.e. as property files
within the configuration object). It then uses the
.buildConfig()
function convert all nested promises to a single promise for the whole
config. This example prints the following result.
{ 'a.md': { contents: 'First file (from dir1)', path: 'dir1/a.md' },
'b.md': { contents: 'Second file (from dir1)', path: 'dir1/b.md' } }
We can see that the files
-call of the preprocessor converted the directory path into
an object containing a one property for each file in the directory.
So far, we have loaded and displayed the preprocessed configuration. Now replace the
.buildConfig()
-call by .run()
var customize = require('customize')
// Load files from one directory
customize()
.registerEngine('files', require('./engine-concat-files'))
.merge({
files: 'dir1'
})
.run()
.then((result) => console.log(result.files))
The engines run()
-method will now be executed with the resolved configuration,
which yields the following output:
{ 'concat.txt': 'First file (from dir1)\nSecond file (from dir1)\n' }
We now have a working customizable configuration. The only thing we have not tried
yet is to customize it. We are going to assume that someone, maybe Bob, wants to reuse
the configuration for my own purposes, because he really likes it, and it really does
exactly what he was looking for. Almost... Except, that the contents of the first file (a.md
)
needs to be replace by something else. In reality this might be a Handlebars partial to include
different contents, or an additional Less-file that changes some styles to follow Bob'
company's style-guide.
We can do this, by merging another configuration, but let's have a look at the directory tree before doing this:
├─┬ dir1/
│ ├── a.md
│ └── b.md
├─┬ dir2/
│ └── a.md
├── engine-concat-files.js
├── example-buildConfig.js
├── example1.js
└── example2.js
You can see that the second directory contains a file a.md
. We will use this file to
replace the file of the first directory.
var customize = require('customize')
// Load files from one directory and merge with second
customize()
.registerEngine('files', require('./engine-concat-files'))
.merge({
files: 'dir1'
})
.merge({
files: 'dir2'
})
.run()
.then((result) => console.log(result.files))
There is an additional call to .merge
in this code. Its input is also passed to the
engine's preprocessor, so now we get two objects containing files and their contents
and those are merged by the .mergeWith
-function of the lodash library,
so that in the above example, the property a.md
is replace by the value in the
second configuration. So the output of this example is
{ 'concat.txt': 'First file (from dir2)\nSecond file (from dir1)\n' }
This is the essence of customize
. Actually, things are a bit more complicated.
A custom overrider ensures (in this order)
- that nested objects can provide there own overrider function in a
_customize_custom_overrider
-property, - that array-values are concatenated rather than replaced
- and that promises are correctly merged.
Finally, the .files()
-helper does not return the file contents directly. It returns a promise for the
file contents. This promise is lazy and only evaluated when the .then()
-method is called. And it uses the
Customize.leaf()
method to attach custom overrider, so that a file-promise replaces its predecessor
without .then()
being called.
This means that files, whose contents is overridden by other files, are not opened for reading.
Currently, there is only the thought package uses customize, but bootprint uses the same principle.
In thought
the .thought/partials
directory is included to allow the user to override default Handlebars-partials with
custom verison.
In bootprint
the user can create packages with Handlebars-partials and Less-definitions, which include and override
partials and definitions from other packages.
Customize uses the debug module for debug logging. You can use the following channels to enable debugging:
DEBUG=customize:versions
logs versions of loaded modules (like it was the default in version 1.x)DEBUG=customize:state
logs the resolved state after a mergeDEBUG=customize:base
logs errors and status changes
This package will always support the latest version of NodeJS and as well as the current LTS version. In the future, it will not be considered a breaking change to drop support of a pre-LTS version of NodeJS.
The exported module is a function that creates a new empty Customize-instance.
Create a new Customize object with an empty configuration
- customize
- static
- .debugState
- .debug
- .Customize :
customize
- .overrider :
customOverrider
- .withParent
- .leaf ⇒
Promise
- inner
- ~Customize
- new Customize()
- .registerEngine(id, engine)
- .configSchema()
- .merge(config) ⇒
Customize
- .load(customizeModule) ⇒
Customize
- .buildConfig() ⇒
Promise.<object>
- .watched() ⇒
Promise.<object.<Array.<string>>>
- .run([options]) ⇒
Promise.<object>
- ~customize() ⇒
Customize
- ~Customize
- static
For coverage testing: Expose the debugState object so it can be enabled an disabled in testcases
Kind: static property of customize
For coverage testing: Expose the debug object so it can be enabled an disabled in testcases
Kind: static property of customize
Exposes the constructor of the customize
object
Kind: static property of customize
Custom overrider-function (that is used as customizer
in (lodash#merge)[https://lodash.com/docs#merge]
Kind: static property of customize
Wrap a function so that if it overrides another function, that function will
be available as this.parent
Kind: static property of customize
Read only: true
Api: public
Param |
---|
fn |
Create a promise that is regarded as leaf in the configuration tree. That means, that the overrider is not resolving this promise when overriding values. Promised object values will not be merged but replaced.
Kind: static property of customize
Access: public
Read only: true
Param | Type | Description |
---|---|---|
promiseOrValue | * |
a promise or a valude that represents the leaf |
Kind: inner class of customize
- ~Customize
- new Customize()
- .registerEngine(id, engine)
- .configSchema()
- .merge(config) ⇒
Customize
- .load(customizeModule) ⇒
Customize
- .buildConfig() ⇒
Promise.<object>
- .watched() ⇒
Promise.<object.<Array.<string>>>
- .run([options]) ⇒
Promise.<object>
This class does the actual work. When calling
require('customize')()
a new instance of this
class is returned with an empty configuration, so
new Customize(...)
should never be called outside
this module
config
and parentConfig
are of the form
{ engine: { config: ..., watched: [ ... ] } }
Register an engine
Kind: instance method of Customize
Access: public
Param | Type | Description |
---|---|---|
id | string |
the identifier of the engine. This identifier is also used within the config as key within the configuration object to identify the sub-configuration stored for this engine. |
engine | object |
a customize engine that is registered |
[engine.defaultConfig] | object |
the default configuration of the engine |
engine.preprocessConfig | function |
a preprocessor to convert a merge-configuration to the internal format of the engine |
engine.run | function |
the execution function of the engine (the merged config is passed as parameter |
engine.run | function |
the execution function of the engine (the merged config is passed as parameter) |
[engine.schema] | object |
a JSON-schema to validate the merge-configurations against. |
Returns the JSON-schema that configuration objects must match for this configuration. The schema does not contain main description property
Kind: instance method of Customize
Creates a new instance of Customize. The configuration values of the current Customize are used as default values and are overridden by the configuration provided as parameter.
Kind: instance method of Customize
Returns: Customize
- the new Customize instance
Api: public
Param | Type | Description |
---|---|---|
config | object |
configuration overriding the current configuration |
Inherit configuration config from another module.
a Customizer-module usually exports a function(Customize):Customize
which in tern calls Customize.merge
to create a new Customize instance.
This function needs to be passed in here.
A new Customize will be returned that overrides the current configuration with the configuration of the module.
Kind: instance method of Customize
Returns: Customize
- the Customize instance returned by the module
Access: public
Param | Type | Description |
---|---|---|
customizeModule | function |
that receives a Customize as paramater and returns a Customize with changed configuration. |
Return a promise for the merged configuration. This functions is only needed to inspect intermediate configuration results (i.e. for testing and documentation purposes)
Kind: instance method of Customize
Returns: Promise.<object>
- a promise for the whole configuration
Access: public
Return a promise for the files needing to be watched in watch-mode, indexed by engine.
Kind: instance method of Customize
Returns: Promise.<object.<Array.<string>>>
- a promise for the files to be watched.
Access: public
Run each engine with its part of the config.
Kind: instance method of Customize
Returns: Promise.<object>
- an object containing on property per registered engine
(the key is the engine-id) containing the result of each engine
Access: public
Param | Type | Description |
---|---|---|
[options] | object |
optional paramters |
[options.onlyEngine] | string |
the name of an engine if only a single engine should be executed |
Kind: inner method of customize
Api: public
- readFiles(directoryPath, [options]) ⇒
Promise.<object.<string, Promise.<{path:string, contents:string}>>>
An overridable directory which resolves to the contents of all its files (recursively). Returns an undefined value if the directory path is undefined.
files(directoryPath, [options]) ⇒Promise.<object.<string, Promise.<{path:string, contents:string}>>>
An overridable directory which resolves to the contents of all its files (recursively). Returns an undefined value if the directory path is undefined. The contents of each file is a UTF-8 encoded string.
readFiles(directoryPath, [options]) ⇒ Promise.<object.<string, Promise.<{path:string, contents:string}>>>
An overridable directory which resolves to the contents of all its files (recursively). Returns an undefined value if the directory path is undefined.
Kind: global function
Returns: Promise.<object.<string, Promise.<{path:string, contents:string}>>>
- an object containing
the relative file-path from the directoryPath as key and the file-path and the file-contents as value
Param | Type | Description |
---|---|---|
directoryPath | string | null | undefined |
the path to the directory |
[options] | object |
|
[options.glob] | string |
an optional glob pattern for filtering files |
[options.stream] | boolean |
if set to true, the contents of a file will be a readable stream instead of the actual data. |
[options.encoding] | string |
the file is expected to be encoded. This means that the instead of a Buffer, a string is returned. If the 'stream' option is set, the stream's encoding will be set via readable.setEncoding(encoding) |
files(directoryPath, [options]) ⇒ Promise.<object.<string, Promise.<{path:string, contents:string}>>>
Promise.<object.<string, Promise.<{path:string, contents:string}>>>
Deprecated
An overridable directory which resolves to the contents of all its files (recursively). Returns an undefined value if the directory path is undefined. The contents of each file is a UTF-8 encoded string.
Kind: global function
Returns: Promise.<object.<string, Promise.<{path:string, contents:string}>>>
- an object containing
the relative file-path from the directoryPath as key and the file-path and the file-contents as value
Param | Type | Description |
---|---|---|
directoryPath | string | null | undefined |
the path to the directory |
[options] | object |
|
[options.glob] | string |
an optional glob pattern for filtering files |
customize
is published under the MIT-license.
See LICENSE.md for details.
For release notes, see CHANGELOG.md
See CONTRIBUTING.md.