npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

customize

v4.0.4

Published

A simple framework to create customizable engines

Downloads

15,356

Readme

customize

NPM version

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.

Engines

Used by

Installation

npm install customize

Usage

The following example should demonstrate the usage of Customize and the files io-helper. Consider the following file tree

Creating an engine

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

const 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) {
    let 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 the files 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.

Loading a configuration

In order to see, how the preprocessor and the files-helper works, we can display the configuration after a merge:

const 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.

Running the engine

So far, we have loaded and displayed the preprocessed configuration. Now replace the .buildConfig()-call by .run()

const 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' }

Merging another configuration

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:

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.

const 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' }

Advanced usage

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.

Application of the principles

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.

Troubleshooting

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 merge
  • DEBUG=customize:base logs errors and status changes

API-reference

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.

customize

Create a new Customize object with an empty configuration

customize.debugState

For coverage testing: Expose the debugState object so it can be enabled an disabled in testcases

Kind: static property of customize

customize.debug

For coverage testing: Expose the debug object so it can be enabled an disabled in testcases

Kind: static property of customize

customize.Customize : customize

Exposes the constructor of the customize object

Kind: static property of customize

customize.overrider : customOverrider

Custom overrider-function (that is used as customizer in (lodash#merge)[https://lodash.com/docs#merge]

Kind: static property of customize

customize.withParent

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 |

customize.leaf ⇒ Promise

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 |

customize~Customize

Kind: inner class of customize

new Customize()

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: [ ... ] } }

customize.registerEngine(id, engine)

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. |

customize.configSchema()

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

customize.merge(config) ⇒ 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 |

customize.load(customizeModule) ⇒ Customize

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. |

customize.buildConfig() ⇒ Promise.<object>

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

customize.watched() ⇒ Promise.<object.<Array.<string>>>

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

customize.run([options]) ⇒ Promise.<object>

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 |

customize~customize() ⇒ Customize

Kind: inner method of customize
Api: public

IO/Helpers

Functions

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}>>>~~

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 |

License

customize is published under the MIT-license.

See LICENSE.md for details.

Release-Notes

For release notes, see CHANGELOG.md

Contributing guidelines

See CONTRIBUTING.md.