pkgcfg

0.9.1 • Public • Published

pkgcfg 0.9.1

One configuration to rule them all

npm license travis greenkeeper mind BLOWN

.

logo

.

Install

npm install --save pkgcfg

Usage

Add pkgcfg tags to your package.json:

package.json:

{
  "name": "my-project",
  "version": "1.0.0",
  "main": "src/my-project.js",
  "min": "dist/${name}.min.js",
  "map": "dist/${name}.min.js.map",
  "dependencies": {
    "pkgcfg": "^0.9.1"
  }
}

Then, read your package.json with pkgcfg:

var pkg = require('pkgcfg')()  // <-- note the extra pair of braces
console.info(pkg.min)          // 'dist/my-project.min.js'
console.info(pkg.map)          // 'dist/my-project.min.js.map'

By default, pkgcfg will read in package.json, but you can read any other JSON file by passing the path to the file as an argument:

var myJSON = require('pkgcfg')('./path/to/my.json')

Use $ from the built-in tags, npm install one or more of the external tags, or roll your own tags. You can even use pkgcfg tags in NPM run scripts!

Remember that the standard NPM tooling does not understand pkgcfg tags, so you can't use them in the name, version, dependencies etc fields.

What is it?

Inspired by an article from Keith Cirkel promoting the use of npm for build scripting and the use of package.json for configuration, I tried to follow his advice. But I quickly learned the downside of JSON as a format, because it is completely static and lacks even the simplest dynamic behavior, such as referencing other nodes in the tree, environment variables, simple conditionals etc.

pkgcfg is a pluggable system to allow simple dynamic behavior to be specified in a JSON document, which is then executed by pkgcfg when it loads the JSON.

How does it work?

In the example above, ${name} tells pkgcfg that this snippet should be transformed using the tag function $, which is built-in and implements a simple reference mechanism based on object-path.

The syntax should feel pretty natural to anyone that has worked with templating libraries before. This is the basic form:

      tag{text-content}
       ^ ^     ^      ^
      /  |     |       \
   tag  open argument    close

An unbalanced close marker after the tag will close the tag. If your text contains close markers, you can use quotes to prevent them from being interpreted:

       tag{'text with } symbol'}
           ^                  ^
            \                /
  prevent interpretation of open/close symbols

If your text contains quotes, then you can escape these inside quoted text by replacing them with 2 consecutive quotes:

  tag{'text with '' in content'}
                  ^
                  |
             escaped quote

will result in the string text with ' in content being passed to tag.

In all cases, any tags contained inside the text-content will also be processed. This will happen before the result is passed to the tag function associated with tag.

E.g:

tag{Hello, user{name}!}

user{name} will be transformed first and if it yields Alice, then Hello, Alice! will be passed on to tag.

Normally, the raw text inside the tag will be passed to the tag function as a string, but by adding a modifier, you can let pkgcfg use JSON.parse to turn the text into an object or array:

tag{['some', 'text', 'with '' in it']}

In this case, pkgcfg will replace all single quotes (except escaped ones) into double quotes:

["some", "text", "with ' in it"]

and then call JSON.parse on it to get you a real array.

The same thing can be done with objects:

tag{{'some': 'object', 'with':'a '' in it'}}

Finally, the list modifiers will let pkgcfg pass multiple arguments to the tag function:

tag{('wow', 'multiple', 'arguments')}

tag will receive it's payload in three arguments instead of the usual one.

Built-in tags

The tags listed below are part of pkgcfg itself and require no extra dependencies.

${ref}

Yields the value of the referenced JSON node.

ref

Required, String. A valid object-path expression to another node in the JSON.

examples

{
  "name": "pkgcfg-example",
  "version": "1.0.0",
  "test": "Project ${name} ${version} is using pkgcfg!",
}

In this example, name and version are two very simple object-path expressions that will yield 'pkgcfg-example' and '1.0.0' respectively. But object-path allows much more complex expressions. E.g.:

{
  "nested": {
    "object": {
      "array": [
        "element-0",
        "element-1"
      ]
    }
  },
  "test": "${nested.object.array.1}"
}

In this example, ${nested.object.array.1} will yield 'element-1'. For more details see the object-path documentation.

NOTE: Loops are not supported:

{
  "start": "${end}",
  "end": "${start}"
}

This will create an infinite loop and likely crash your program. Don't do this!

Using external tags

pkgcfg allows you to use tags coming from external packages.

By convention, the name of the external package needed to use the tag has the form pkg<tag>, where <tag> is the name of the tag. For example, the env{} tag is contained in pkgenv, the eval{} tag in pkgeval etc.

To use tags from external packages:

npm install those packages.

E.G.: npm install --save pkgenv

Make sure the tags from those packages are available for use

Pkgcfg will only transform tags it knows about. There are three ways to let it know about tags coming from external packages:

  • have it discover tags based on the dependencies in package.json
  • register the tags in package.json
  • register the tags in the config being transformed
  • require the package before using pkgcfg .

have pkgcfg discover tags based on dependencies

pkgcfg will look at the keys dependencies and devDependencies in your package.json and if they contain any packages whose name starts with 'pkg', it will register the tag based on the rest of the name. E.g. for a dependency named 'pkgenv', it will discover the tag 'env'. No need to explicitly require the package containing the tag before using pkgcfg:

var pkg = require('pkgcfg')() // requires `pkgenv` automatically when needed

Auto-discovery should work for all external tags listed on this page.

register the tags in package.json

For tags coming from packages that don't follow the naming convention, you can explicitly register the tags you are using by adding an entry pkgcfg to your package.json with a "tags" attribute, which should be an object with the tag name as key and the name of the package this tag comes from as the value:

package.json

{
  "pkgcfg": {
    "tags": {
      "env": "pkgenv"
    }
  }
}

pkgcfg will automatically require('pkgenv') when it encounters the env{} tag. No need to explicitly require it yourself:

var pkg = require('pkgcfg')() // requires `pkgenv` automatically when needed

register the tags in the config being transformed

You can also add a key named pkgcfg to the JSON being transformed. The format is exactly the same as for package.json:

my-json.json

{
  "pkgcfg": {
    "tags": {
      "env": "pkgenv"
    }
  }
}

Any tags registered in package.json will also be available.

pkgcfg will automatically require('pkgenv') when it encounters the env{} tag. No need to explicitly require it yourself:

var pkg = require('pkgcfg')('my-json.json') // requires `pkgenv` automatically when needed

require an external tag

This method is most explicit. It will work everywhere, but requires you to know beforehand which tags are used.

Just call require before you call pkgcfg:

require('pkgenv') // <-- make sure `env{}` tag is available
var pkg = require('pkgcfg')()

External tags

env{(name, [defaultValue])}

Reference environment variables.

eval{expr}

Evaluate Javascript expressions.

Anything missing?

Do you know of an available pkgcfg tag that's not in this list? Please let me know by reporting an issue (or better yet, a Pull Request) to this project's GitHub Issue Tracker, and I will add it to this list.

Roll your own

Writing your own tags is super easy! Let's write one that yields the value of some environment variable.

Write the tag function

First, define a function that will contain the logic for your tag:

function pkgenv(root, parents, node, name, defaultValue) {
 
}

The function should accept at least 3 arguments, which will be passed to it by pkgcfg when invoking the tag:

  • root: The root node of the config being processed
  • parents: An array containing all parents of the current node, closes parent last.
  • node: The node currently being processed.

If the tag needs to accept arguments, add these as extra arguments to the function as well. In this case we add 2 extra arguments, name and defaultValue, which can be used to pass the name of the environment variable to read and the default value to use when the environment variable is undefined.

Now implement the business logic of your tag. In this case it's some code to access process.env to read the environment. Here is the full function:

function pkgenv(root, parents, node, name, defaultValue) {
  if (!name) name = 'NODE_ENV'
  if (!defaultValue) defaultValue = ''
    return process.env[name] || defaultValue
}

Register the tag

After we have created a tag function, we need to register it with pkgcfg:

pkgcfg.registry.register('env', pkgenv)

Pkgcfg assumes that tags register themselves upon first require and are never unregistered. However, in some scenario's (e.g. testing) it may be desirable to call unregister to unregister the tag again:

pkgcfg.registry.unregister('env', pkgenv)

Using the knowledge from above, let's write the full pkgenv module:

var pkgcfg = require('pkgcfg')
 
function pkgenv(pkg, node, name, defaultValue) {
  if (!name) {name = 'NODE_ENV'}
  if (!defaultValue) {defaultValue = ''}
    return process.env[name] || defaultValue
}
 
pkgcfg.registry.register('env', pkgenv)
module.exports = pkgenv

(Optional) Publish your tag to NPM

Place your tag module in an NPM package, preferably called pkg<tag> and with pkgcfg in it's list of keywords, so others can re-use your brilliant code. Add some tests as well. Have a look at the pkgenv package to see an example.

(Optional) Let me know!

Add an issue in this project's issue tracker to let me know of your package and I will add it to the list of external packages above.

I registered a whole bunch of names starting with pkg that I felt I wanted to implement one day. If you want to make a certain tag and I have that name registered, just drop me an issue and I'll transfer that name to you.

.

Using pkgcfg tags in NPM run scripts

Even though normal NPM tooling does not understand pkgcfg tags, you can still use them in the scripts section, via the run command that comes with pkgcfg.

run [script]

{
  "name": "my-project",
  "version": "1.0.0",
  "main": "src/my-project.js",
  "scripts": {
    "echo": "run pkgcfg-echo",
    "pkgcfg-echo": "echo Hello, ${name} ${version}!"
  },
  "dependencies": {
    "pkgcfg": "^0.9.1"
  }
}

Note how we avoided using any pkgcfg tags in the echo script. It just uses run to call the pkgcfg-echo script.

Now, you can call echo the old-fashioned way:

$ npm run echo
 
> my-project@1.0.0 echo c:\ws\my-project
> run pkgcfg-echo
 
Hello, my-project 1.0.0!

I recommend you have a look at the NPM scripts docs to learn how NPM treats the scripts section in your package.json before you dive in and start hacking. Avoid adding pkgcfg tags to the following standard NPM scripts, because they are expected to work using npm [script], which they won't if you use tags there:

  • publish, prepublish, postpublish
  • install, preinstall, postinstall
  • uninstall, preuninstall, postuninstall
  • version, preversion, postversion
  • test, pretest, posttest
  • start, prestart, poststart
  • stop, prestop, poststop
  • restart, prerestart, postrestart

You can still make use of pkgcfg for these scripts, just use run to run a different script and in that other script, use pkgcfg tags:

{
  "name": "my-project",
  "version": "1.0.0",
  "scripts": {
    "start": "run smart-start",
    "smart-start": "echo Yippee! We can use pkgcfg tags in ${name}!"
  },
  "dependencies": {
    "pkgcfg": "^0.9.1"
  }
}

Example project

If you want a simple example project demonstrating how pkgcfg works, check out pkgcfg-example.

Contributing

To hack on this code, fork this repo to your own account and clone the fork to your local machine.

Install

Go to the root folder of the cloned project and run npm install to install the dependencies:

$ npm install
added 39 packages from 306 contributors in 4.156s

Run tests

Then, run the Mocha tests:

$ npm test
 
> pkgcfg@0.9.1 test C:\ws\pkgcfg
> mocha pkgcfg.spec.js
 
 
  pkgcfg
    √ is a function
    √ returns an object representation of package.json
$test{} => calls $test(root, parents, node)
    √ calls transform functions associated to tags that it encounters during parsing
    ...
 
  39 passing (95ms)

The tests should all pass. To re-run the tests whenever you make a change, use

$ npm run dev

This will run Mocha with the --watch flag.

Write code and create a PR

You can now work on the code. Don't forget to create a branch for your new feature. Add some tests and make sure they pass. Then, commit and push your changes and create a PR.

Logging

This project supports logging with ulog. To enable debug logging for this package, set the DEBUG environment variable to include pkgcfg:

Windows

C:\>set DEBUG=pkgcfg

Unix/Mac

$ DEBUG=pkgcfg

Now, pkgcfg will log debug information as it runs. For example here is some output produced when running the tests with this environment variable set:

tag: ${[object Object],[object Object],[object Object],${name},name} =>  pkgcfg
tag: ${[object Object],[object Object],[object Object],${repository.type},repository.type} =>  git
tag: ${[object Object],[object Object],[object Object],${repository},repository} =>  { type'git', url: 'https://github.com/download/pkgcfg.git' }
tag: ${[object Object],[object Object],[object Object],${test.data},test.data} =>  { array: [ 'one config to rule them all''with''pkgcfg' ],
  'with-dash''ok',
  'with:colon''ok',
  'with.dot''ok',
  'with\'quote''ok' }
tag: ${[object Object],[object Object],[object Object],${keywords},keywords} =>  [ 'package.json''configuration''pkgcfg''pkgenv''pkgeval' ]
tag: ${[object Object],[object Object],[object Object],${test.data.array},test.data.array} =>  [ 'one config to rule them all''with''pkgcfg' ]
Error applying tag ${[object Object],[object Object],[object Object],${unresolved.transforms.return.unmodified.text},unresolved.transforms.return.unmodified.text}:  { Error
    at Object.$ (C:\ws\pkgcfg\pkgcfg.js:47:35)
    at transform (C:\ws\pkgcfg\pkgcfg.js:225:42)
    ...
    at bootstrap_node.js:502:3
  message: '${unresolved.transforms.return.unmodified.text} cannot be resolved.',
  name: 'QuietError' }
tag: ${[object Object],[object Object],[object Object],${test.data.array.length},test.data.array.length} =>  3
tag: ${[object Object],[object Object],[object Object],${test.data.array.2},test.data.array.2} =>  pkgcfg
tag: ${[object Object],[object Object],[object Object],${test.data.with-dash},test.data.with-dash} =>  ok
tag: ${[object Object],[object Object],[object Object],${test.data.with:colon},test.data.with:colon} =>  ok
convertQuotes(['test','data','with.dot']) ==>  ["test","data","with.dot"]
...

Issues

Add an issue in this project's issue tracker to let me know of any problems you find, or questions you may have.

Copyright

Copyright 2018 by Stijn de Witt. Some rights reserved.

License

Creative Commons Attribution 4.0 (CC-BY-4.0)

/pkgcfg/

    Package Sidebar

    Install

    npm i pkgcfg

    Weekly Downloads

    3

    Version

    0.9.1

    License

    CC-BY-4.0

    Unpacked Size

    74.4 kB

    Total Files

    9

    Last publish

    Collaborators

    • stijndewitt