npm package: @l.systems/config.io
Description: configuration made easier
Author: L.systems SARL
Latest version: 1.0.2

# Config.io

Config.io is a simple to use but very flexible configuration library based on YAML files.

Its main features are:
- YAML based
- writable in a monitorable way (changes are not saved in the main config file)
- easily extensible format
- supports import from other YAML files or JS (commonjs & ESM)

It is compatible from Node 10.x (only for its Common.JS version) and from 13.2 for its ESM version. Node 10 support will be dropped when it will loose its LTS status.

**Note:** The test suite needing the `worker_threads` module (ATM), it does not run on node 10.

## Introduction

Though you should read the [Getting started](#getting-started) section to understand properly all concepts underlying this lib, you can skip reading to the [Full examples](#full-examples) if you prefer seeing sample use cases first and understand them properly later.

## Topics

<!-- toc -->

- [Getting started](#getting-started)
  * [Core concepts of the library](#core-concepts-of-the-library)
  * [Basic file naming scheme](#basic-file-naming-scheme)
  * [Auto start](#auto-start)
  * [Folder structure](#folder-structure)
  * [`null` and `undefined`](#null-and-undefined)
  * [Let's try it out!](#lets-try-it-out)
- [Full examples](#full-examples)
  * [With `simpleMode`](#with-simplemode)
  * [Without `simpleMode`](#without-simplemode)
- [Options](#options)
  * [options.simpleMode](#optionssimplemode)
  * [options.readOnly](#optionsreadonly)
  * [options.autoSave](#optionsautosave)
  * [options.save](#optionssave)
  * [options.allowAsync](#optionsallowasync)
  * [options.throwIfNotReady](#optionsthrowifnotready)
  * [options.customTags](#optionscustomtags)
  * [options.mapFilenamesTpl](#optionsmapfilenamestpl)
  * [options.environments](#optionsenvironments)
  * [options.inspectMode](#optionsinspectmode)
  * [options.resolve](#optionsresolve)
  * [options.autoload](#optionsautoload)
  * [options.baseDir](#optionsbasedir)
  * [options.configDir](#optionsconfigdir)
  * [options.logger](#optionslogger)
- [CommonJS and ESM](#commonjs-and-esm)
- [Tags provided by default](#tags-provided-by-default)
  * [!cmdline-option](#cmdline-option)
  * [!config](#config)
  * [!env](#env)
  * [!fallback](#fallback)
  * [!import](#import)
  * [!path](#path)
  * [!regexp](#regexp)
  * [!!timestamp](#timestamp)
- [API](#api)
  * [Starting `config.io`](#starting-configio)
  * [Waiting for async values](#waiting-for-async-values)
  * [Loading a single namespace explicitely](#loading-a-single-namespace-explicitely)
- [Writing to the configuration](#writing-to-the-configuration)
  * [Basic](#basic)
  * [Generate configuration files](#generate-configuration-files)
- [Creating custom tags](#creating-custom-tags)
- [Using the library in a npm package](#using-the-library-in-a-npm-package)
- [Contributing](#contributing)

<!-- tocstop -->

## Getting started

As usual, install config.io through npm:

```
npm install @l.systems/config.io
```

Once it's done, you will usually need to create a `config` folder at the root of your project.

### Core concepts of the library

There are 3 core concepts to understand to use this lib:
- namespaces
- environments
- configuration tags

Namespaces are the basic scoping mechanism of config values inside `config.io`. They will be filled out in different files. You can deactivate namespaces by setting the `simpleMode` option to `true` (see later in the configuration to know how to achieve this). It will basically make the library hide the namespace concept and expose a single unnamed namespace (that will sometimes be reffered as the `@` namespace) instead (though namespaces can still be used explicitely with the API).

Environments are sub units to namespaces. They allow you to easily change the configuration based on the context your program is executing. By default, `config.io` will load the following environments, in this order: `'default'`, `process.env.NODE_ENV || 'development'`, `'local'`. The later an environment is loaded, the more priority it has. If you have defined: `port: 3000` in the default env and `port: 80` in the local env, your app will get `port: 80` when reading the configuration.

Configuration tags are custom tags exposed thanks to the YAML syntax being so powerful. By default, you get access to a collection of custom tags allowing you to create more complex configurations easily. You can deactivate tags you don't like, and you can also add tags that you need, the library being very extensible on those points. Here is an exemple so you can see what it can do:

```yaml
port: !fallback
- !env NODE_PORT
- 3000
```

When accessing the `port` value, your app will read `3000` if the `NODE_PORT` environment variable is not set. Otherwise, it will get the `NODE_PORT` value.

### Basic file naming scheme

The file naming conventions can be modified using the `mapFilenamesTpl` option that contains a lodash template string. It defaults to `<%= ns || env %><% if (ns && env !== 'default') { %>.<%= env %><% } %>` but you are free to change it as you wish.

That generates the following names for the files (for each environment):
```sh
# simpleMode = true (unnamed namespace)
[envName] -> [envName].yaml

# simpleMode = false
[ns] + default -> ns.yaml
[ns] + [otherEnv] -> ns.[otherEnv].yaml
```

### Auto start

By default, `config.io` will try to be helpful and to load everything properly without you doing anything. It will "auto start" using default values.

The *auto start* mechanism is doing the following:
- load the `configio` namespace explicitely
- start the lib using the values in the namespace as its options

If you want to prevent the lib from doing an *auto start*, you can either:
- set the `CONFIGIO_NO_AUTO_START` environment variable to any non empty value
- pass the `--configio-no-auto-start` option to your program when starting it

If you prevent *auto start*, you will need to perform a [manual start](#starting-configio) using the API.

### Folder structure

There 2 important folders in the `config.io` library. The `baseDir` folder, and the `configDir` folder.

The `baseDir` is the folder from which every path in the configuration will be resolved. It defaults to a value that should be your project root. It can be provided either as a relative path from `process.cwd()` or as an absolute path. If not provided, `config.io` relies on the [npm's find-root package](https://www.npmjs.com/package/find-root). It basically search for the closest `package.json` in a folder stucture and is invoked like this: `findRoot(process.argv[1])`.

The `configDir` is the folder in which `config.io` looks for configuration files. It can be provided as either a relative path from `baseDir` or an absolute path. It defaults to `config` (the `config` folder in `baseDir`).

To explicitely set `baseDir`, you can either:
- set the `CONFIGIO_BASE_DIR` environment variable
- pass a `--configio-base-dir` option to your program
- prevent `config.io` from doing an auto start (see above) and start the lib manually using the [`baseDir` option](#starting-configio)

To explicitely set `configDir`, you can either:
- set the `CONFIGIO_DIR` environment variable
- pass a `--configio-dir` option to your program
- prevent `config.io` from doing an auto start (see above) and start the lib manually using the [`configDir` option](#starting-configio)

### `null` and `undefined`

Good to know: YAML as no concept of `undefined` but its definition of `null` is nearly exactly the same as the definition of `undefined` in JS. So `config.io` always casts `null` values to `undefined` in JS and `undefined` values in JS as `null` in YAML.

That as the big advantadge that most validation libraries in JS do not treat `null` in a property the same way as `undefined`. If a property is optional, it can often be `undefined` but not `null`. This way, your lib will get `undefined` and not complain!

The big issue is that, for now, there is absolutely no way to get a `null` out of the `config.io` library if needed.

You can create pretty easily a `!null` custom tag to achieve this though. If it turns out it's a common use case, I'll gladly accept a PR on this.

### Let's try it out!

Create a `config` folder with the following files:

`configio.yaml`:
```yaml
autoload:
- server
```

`server.yaml`:
```yaml
port: !fallback
- !env NODE_PORT
- 3000
```

Then create a `test.js` file in your project root looking like:
```js
const configIO = require('@l.systems/config.io')

console.log(configIO.server.port)
```

And here are results from executing this very simple program:
```sh
node test.js
# 3000

NODE_ENV=8080 node test.js
# 8080
```

As you can see, your namespace values are readily available from the API, under a nested property. The name of the namespace in the library will be a `camelCase` version of the namespace name used in the configuration. Meaning you can safely name your namespaces using either `-` or `_` like: `server-plugins` that will become `serverPlugins` in the JS API but will load the `server-plugins.yaml` and `server-plugins.[env].yaml` files.

If you are using the `simpleMode`, then the API will directly give you access to your namespace values. Eg:

`config/configio.yaml`:
```yaml
simpleMode: true
```

`config/default.yaml`:
```yaml
port: !fallback
- !env NODE_PORT
- 3000
```

`./test.js`:
```js
const configIO = require('@l.systems/config.io')

console.log(configIO.port)
```

Will have the same effects as the program described earlier.

**Note:** You can safely use custom tags in the `configio.yaml` file too. And since the API loads the config.io files as a namespace, you can also change the configuration using environments.

## Full examples

Those examples relies mostly on default values that should be fine for most projects.

### With `simpleMode`

Let's say we want to make a very simple server to serve static files. We only need to get some basic server configuration, like `port`, the `static` folder in which to get the files, etc. The `static` folder in most use case should be looked into the `dist` folder of the project, because we compile them. But when developing, we'd like to serve the `src` folder instead. In production, we'd also like to be able to change the folder served using the `--static` option.

The `simpleMode` is very usefull for this use case, because it can limit the number of files we create.

Let's create the following folder structure:
* package.json
* config
  * configio.yaml (it will contain options for `config.io`)
  * default.yaml (it will contain default values for our app)
  * development.yaml  (it will contain specific dev values)
  * local.yaml (it will contain specific local values, this file should usually not be commited)
* app.js

`config/configio.yaml`
```yaml
# we only need to activate simpleMode, other defaults are fine
simpleMode: true
```

`config/default.yaml`
```yaml
# define good defaults
server:
  # use the --port option, fallback to NODE_PORT env var and then to 3000 if needed
  port: !fallback
  - !cmdline-option
    name: port
    type: number
    description: port on wich the server should listen to
  - !env NODE_PORT
  - 3000

  # more options here

# by default, the static folder is in
static: !fallback
- !cmdline-option
  name: static
  type: string
  description: path to the static folder to serve
- !path dist
```

`config/development.yaml`
```yaml
# redefine specific values
static: !path src
```

`config/local.yaml`
```yaml
# I'm lazy and on my computer, I don't like to use --port or NODE_PORT so I override port
server:
  port: 8080
```

`app.js`
```js
const configIO = require('@l.systems/config.io')

// just for the example, get the values from the configuration
console.log(configIO.server.port, configIO.static)
```

And here are some results:
```sh
node app.js # 8080 /path/to/project/src
NODE_ENV=production node app.js # 8080 /path/to/project/dist
node app.js --static www # 8080 www

# let's remove config/local.yaml
rm config/local.yaml

node app.js # 3000 /path/to/project/src
NODE_PORT=8081 node app.js --port 8080 app.js # 8080 /path/to/project/src
NODE_PORT=8081 node app.js # 8081 /path/to/project/src
```

### Without `simpleMode`

If we have a more involved application, where the server part is only one part out of many, we might find it cleaner to separate configuration values by namespaces. This will help finding them by separating concerns. The easier way to use those is either by autoloading them all, making them available as properties on the root object, or loading part of them using the `!config` custom tag.

In this example, we will use a little of both, so you can grab a complex case in one go.

We won't create environments for every namespace to keep it cleaner, but you obviously can.

Let's create the following structure:
* package.json
* config
  * configio.yaml
  * logger.yaml
  * server.yaml
  * plugin-static.yaml
  * plugin-static.development.yaml
* app.js

`config/configio.yaml`
```yaml
autoload:
- server
- logger
```

`logger.yaml`
```yaml
mode: detailed
transports:
  console: true
  logstash: http://url.to.logstash:port/
```

`config/server.yaml`
```yaml
port: !fallback
- !env NODE_PORT
- 3000
plugins:
  static:
    bindTo:
      hostname: http://localhost:3000/
    options: !config plugin-static
```

`config/plugin-static.yaml`
```yaml
chroot: true
addTrailingSlash: true
path: !fallback
- !cmdline-option
  name: static
  type: string
  description: path to the static folder to serve
- !path dist
# …
```

`config/plugin-static.development.yaml`
```yaml
path: !path src
```

`app.js`
```js
const configIO = require('@l.systems/config.io')

// just for the example, get the values from the configuration
console.log(
  configIO.logger.transports.logstash,
  configIO.server.port,
  configIO.plugins.static.options.path,
)

// the plugin-static namespace is also made available on the root object
console.log(configIO.pluginStatic.path)
```

## Options

Here is a list of the options and their default values you can give the API to start it or set in your `configio.yaml` file:
```js
{
  simpleMode: false,
  readOnly: true,
  autoSave: true,
  save: 'sync',
  allowAsync:
    yargs.argv.allowAsync ||
    !!process.env.CONFIGIO_ALLOW_ASYNC ||
    false,
  throwIfNotReady: true,
  customTags: {},
  mapFilenamesTpl:
    "<%= ns || env %><% if (ns && env !== 'default') { %>.<%= env %><% } %>"
  environments: [
    'default',
    process.env.NODE_ENV || 'development'
  ],
  inspectMode: 'detailed',
  resolve: {},
  autoload: [],
  baseDir:
    yargs.argv.configioBaseDir ||
    process.env.CONFIGIO_BASE_DIR ||
    findRoot(process.argv[1]),
  configDir: yargs.argv.configioDir ||
    process.env.CONFIGIO_DIR ||
    'config',
  logger: console,
}
```

### options.simpleMode

*boolean* (defaults to **false**)

Allows to activate the `simpleMode`, very useful for simpler use cases.

### options.readOnly

*boolean* (defaults to **true**)

Allows to deactivate read-only mode. Without setting this to `false` explicitely, you won't be able to make change to configuration values. Since it is an edge case, the library considers safer to treat configuration as read only by default.

### options.autoSave

*boolean* (defaults to **true**)

If changes are allowed, should `config.io` try to save them automatically when you make one?

### options.save

*string* (defaults to **'sync'**)

If changes are allowed, how should they be saved when save occurs. Available values are:
- **sync**: save changes synchronously and immediately, very safe but slow
- **async**: wait for no changes for 10 consecutive ms, then save
- **logger**: don't actually save, log what would have been saved (using the `debug()` method of the logger)

### options.allowAsync

*boolean* (defaults to **false**)

Should `config.io` allow namespaces to load values that need an async op to be available?

### options.throwIfNotReady

*boolean* (defaults to **true**)

Throws if trying to access a namespace that has not loaded all its values yet.
It obviously has no meaning if `allowAsync` is `false`.

**Note:** It is very dangerous to set this to `false` since you have no more garantees that you use namespaces safely. You might try to access a value that will be still `undefined` and set later by `config.io`.

### options.customTags

*{tagName: string}: boolean | customTag* (defaults to **{}**)

It allows you to (de)activate already existing custom tags (using a boolean). By default, all `config.io` tags are available as well as the YAML 1.1 `timestamp` tag.

`config.io` use the underlying [yaml NPM package](https://eemeli.org/yaml/#yaml) to parse YAML files. You should look there to have more info on other basic tags you can activate (like `!!set`, `!!map`, etc).

### options.mapFilenamesTpl

*string* (defaults to **"<%= ns || env %><% if (ns && env !== 'default') { %>.<%= env %><% } %>"**)

Allows to change the file scheme used by `config.io`. See above for more details.

### options.environments

*[string]* (defaults to **['default', process.env.NODE_ENV || 'development']**)

Changes the environments loaded by `config.io`. It always append `'local'` last no matter what so don't put it in there.

### options.inspectMode

*string* (defaults to **'detailed'**)

Changes how `console.log()` and more generally how `util.inspect()` behaves on configuration objects and arrays. Possible values are:
- **detailed**: show how the values where constructed by displaying where custom tags where used and what their resolved values are
- **js**: show how your program will see the configuration
- **debug**: more useful for contributing to `config.io` than for using the lib, it shows internals

### options.resolve

*{namespace: string}: ({env: string}: string)* (defaults to **{}**)

It allows to change directories and names to load a specific env file. Eg:

```yaml
resolve:
  # use @ to refer to the unnamed namespace in simpleMode
  @:
    # load development.yaml instead of test.yaml for env test
    test: development.yaml
  # write namespaces names here using camel case (same for envs below)
  nsName:
    # load the [nsName].yaml file from [configDir]/sub/dir
    default: sub/dir
    # load [configDir]/sub/dir/other.yaml instead of [configDir]/[nsName].test.yaml
    test: sub/dir/other.yaml
```

### options.autoload

*[string]* (defaults to **[]**)

Namespaces to make available automatically after *auto start* or *manual start*.

**Note:** this option is not allowed in `simpleMode`.

### options.baseDir

*string* (defaults to **yargs.argv.configioBaseDir || process.env.CONFIGIO_BASE_DIR || findRoot(process.argv[1])**)

Change the base directory of `config.io`. It's not advised to set this option in `configio.yaml` file since either the file won't be found (since *auto start* won't be able to locate it) or it will only be active for subsequent loads, splitting namespaces folder between `configio`'s namespace and the others.

### options.configDir

*string* (defaults to **yargs.argv.configioDir || process.env.CONFIGIO_DIR || 'config'**)

Change the config directory of `config.io`. It's not advised to set this option in `configio.yaml` file since either the file won't be found (since *auto start* won't be able to locate it) or it will only be active for subsequent loads, splitting namespaces folder between `configio`'s namespace and the others.

### options.logger

*object { info: Function, debug: Function } | false* (defaults to **console**)

Allows to change the default logger used by `configio`. Set to `false` to deactivate logging.

**Note:** `logger` can also be a function containing both an `info` and a `debug` property.

## CommonJS and ESM

This package is available on both CommonJS & ESM versions. It uses the `exports` property of `package.json` to ease usage. So a simple:
```js
import configIO from '@l.systems/config.io'
```

should load the ESM version of the package properly and lead to the same usage patterns as:
```js
const configIO = require('@l.systems/config.io')
```

that should load the CJS version.

Though there are some tests ensuring that if both versions are used simultaneously everything works properly, important data being shared using a global safekeeped behind a Symbol property, it is still advised to stick with one version only (since some edge cases might arise).

## Tags provided by default

Most of the following tags accept sub tags in some of their properties when they have a map form. That allows for more complex tags combinations.

### !cmdline-option

This tag is useful to load data from the process' command line option into the configuration. It uses the [yargs NPM package](https://www.npmjs.com/package/yargs) behind the scene to parse the arguments.

We strongly advise you to use the same lib if you want to support custom options of your own (not loaded in the configuration).

There are two ways to use this tag, as a scalar, or as a map:
```yaml
# this will read the value of --some-option
someOption: !cmdline-option some-option
# this will read the value of --some-other-option but pass options to yargs to help it
# generate a beautiful --help message
someOpterOption: !cmdline-option
  name: some-other-option # accept sub tags
  type: string
  description: very useful option
  # …
```

### !config

This tag allows you to load a namespace explicitely (so it works even in `simpleMode`). It only allows for a namespace name in scalar form (for now at least), like this:

```yaml
# will load the server namespace and make it available in the server property of the config
server: !config server
```

Be careful, if the server namespace is loading async values, it will also make this one async!

### !env

Just like `!cmdline-option` but for environment variables. It has also 2 forms, either a scalar or a map:

```yaml
# set the value of the NODE_ENV environment variable in the mode property of the configuration
mode: !env NODE_ENV

# read the content of APP_DATA and try to parse it using JSON.parse()
# if parsing fails, use its string value instead
data: !env
  name: APP_DATA # accept sub tags
  json: true # json defaults to false
```

If you don't set `json` to `true` or use the scalar version, `!env` will try to cast the value as a
`Number`. If the result of the cast is not `NaN`, it will use this value instead of the string. Eg:
```yaml
port: !env NODE_PORT # returns 3000 and not '3000' for NODE_PORT=3000
```

### !fallback

This tag allows you to consider multiple values in order and return the first non `null` / `undefined` one:

```yaml
# Expose in port the value of the command line option --port
# if it's not set, fallback to the NODE_ENV environment variable
# if it's not set either, use 3000
port: !fallback
- !cmdline-option port
- !env NODE_PORT
- 3000
```

### !import

This tag allows you to load data from external files. It currently support 4 loading mechanisms:
- `yaml`
- `cjs` (loading CommonJS files)
- `mjs` (loading ESM modules)
- `js` (loading CommonJS files by default, can be configured more)

**Note**: loading an ESM module causes an async op, thus to do this you need to have set the option `allowAsync` to `true`, otherwise it will fail to load the namespace.

You can use this tag in its simpler scalar form:
```yaml
# supported file extensions:
# yaml, yml, json: use the yaml mode
# js, cjs: load CJS module (in this form, these are the same)
# mjs: load ESM module
import: !import some/file.cjs
```

Be careful, relative paths are resolved from `baseDir` and not `configDir` (this is usually more practical). You can also use absolute paths.

There is also a map form for this tag:
```yaml
import:
  path: some/file.js # accept sub tags
  # type, string, only available if the file extension is '.js', can be either:
  # - (default) commonjs (use require() to load the file)
  # - module (use import() to load the file, careful: async op even for CJS modules!)
  type: commonjs
  # property, string, path to a property in the module to expose instead of the whole loaded value
  property: some.sub.prop # supports sub properties
  # throw, boolean (defaults to true), throws if imported value in null or undefined
  throw: true
```

### !path

This tag allows to easily denote paths and handle their resolution from `baseDir`. It supports the 3 forms of YAML (scalar, map, seq):

```yaml
# most basic usage, will resolve(baseDir, 'some/path')
simplePath: !path some/path

# map form, useful to use sub tags, here causing it to resolve(baseDir, process.env.STATIC_DIR)
staticFolder: !path
  path: !env STATIC_DIR

# seq form, joins every element in the array, supports sub tags
# this will result in: 'public/' + process.env.IMAGE_DIR_NAME
imageFolder:
- public/
- !env IMAGE_DIR_NAME
```

### !regexp

This tags allows to easily load regexps from your configuration:

```yaml
re: !regexp /test/g
```

You can also write regexp directly in your configuration using this (see [Writing to the configuration](#writing-to-the-configuration) for more on this).

### !!timestamp

The YAML 1.1 `!!timestamp` tag is also available by default. It causes all strings looking like a date to be loaded as a `Date()` instead of a simple string.

```yaml
# will create a Date() on 2020-01-12 at 00:00:00 UTC
# here is the spec for the allowed formats: https://yaml.org/type/timestamp.html
from: 2020-01-12
```

## API

### Starting `config.io`

If you disabled *auto start*, you can perform at any time a *manual start*. If you do a *manual start* after an *auto start* or another *manual start*, it will extend the global options internally and conform to *simpleMode* or *autoload* once again. You should avoid to switch the *simpleMode* value (except if *auto start* did nothing).

```js
const configIO = require('@l.systems/config.io')

// performs a manual start
configIO({
  // options (see the options section)
})

// access your configuration values
console.log(configIO.server.port)
```

### Waiting for async values

If you are using async values, you need to wait for the load to be done. You can do this by performing a manual start with no options (that will basically do nothing but return a promise on the previous start completion) or wait the result of your manual start:

```js
const configIO = require('@l.systems/config.io')

const start = async () => {}
  // performs a manual start and wait for it to be done
  await configIO({
    // options (see the options section)
  })
  //- or wait for previous start to be ready
  await configIO()

  // access your configuration values
  console.log(configIO.server.port)
}

start()
```

### Loading a single namespace explicitely

You can always load explicitely a namespace, even in `simpleMode`. Here is how to achieve this:

```js
const configIO = require('@l.systems/config.io')

// ...
const namespace = /* await */ configIO('server')

console.log(namespace.port)
// ...
```

You can pass options to override defaults when loading a namespace:
```js
configIO('server', options)
```

Options are basically the same as the global ones with the following differences:
- `simpleMode` & `autoLoad` can't be provided
- `resolve` takes only a **{env: string}: string** object since the namespace only needs to know how to resolve its own envirnoment files

And there are 2 specific options that are available only in this case:
- `prependEnvironments` *[string]* (defaults to **[]**): environments to load before the default ones
- `appendEnvironments` *[string]* (defaults to **[]**): environments to load after the default ones (but still before local)

## Writing to the configuration

### Basic

The most simple use case is just to save configuration changes that could be done maybe through, let's say, an API call:

```js
const configIO = require('@l.systems/config.io')

// ...

const changePort = port =>
  configIO.server.port = port
```

And... That's it! `config.io` will take care to save the changes automatically. All changes are always made in the `local` environment. You cannot change other environments for safety reasons, and also to make changes easier to stop, since only changes will be visible in the `local` env and not all values loaded from other environments!

If you use `autoSave: false`, you need a way to save a namespace manually. That is achieved through the `saveConfigIO()` method, available on namespace's root:
```js
// manually trigger a save (using the specified strategy in the 'save' option)
configIO.server.saveConfigIO()
```

### Generate configuration files

To help you generate configuration files, the customTags classes used in the library are exposed. On recent node versions (supporting the `exports` property of `package.json`), you can access those using `config.io/tags/tag-name[.mjs]` modules.

**Note:** on older version of node, you will have to use the `config.io/commonjs/tags/tag-name` form to access those exports.

```js
const configIO = require('@l.systems/config.io')
const { Env } = require('@l.systems/config.io/tags/fallback')
const { Fallback } = require('@l.systems/config.io/tags/fallback')

configIO.server.port = new Fallback([
  new Env({ name: 'NODE_PORT' }),
  3000,
])
```

We advise you to use `autoSave: false` to do this. Once you have completed the values you want in `default.yaml`, save, and rename the local file to the correct name for the `default` env of this namespace. If you want to generate more than one env for a configuration, you will need to load different instances of node for each one, since, for now, there is no way to force reload a namespace that is already loaded. We might implement this later.

## Creating custom tags

You can easily create your own tags. For that, you can use either the [YAML API](https://eemeli.org/yaml/#custom-data-types) or use the higher level `createTag` method exposed.

`tags/null.js`:
```js
const { createTag } = require('@l.systems/config.io/create-tag') // careful, old node versions need commonjs/
const Null = module.exports.Null = class Null {
  valueOf() {
    return null
  }

  static yaml = {
    toYAML() {
      return null
    }
  }
}

module.exports.nullTag = createTag(Null)
```

`config/configio.yaml`:
```yaml
customTags:
  null: !import
    path: ./tags/null.js
    property: nullTag
```

And you're done!

## Using the library in a npm package

This is a very specific use case, but we wanted it to work! In this case, we strongly advise you to load your namespace explicitely and to provide your defaults through a prepended environment:

```js
const configIO = require('@l.systems/config.io')
const path = require('path')

const myPkgConfig = configIO('my-pkg', {
  allowAsync: false,    // recommended
  prependedEnvironments: ['my-pkg-default-values'],
  resolve: {
    // you can change this path to whatever seems fine to you
    myPkgDefaultValues: path.join(__dirname, 'default-values.yaml'),
  }
})
```

We strongly advise you either not to accept async ops and thus to force it being false or to always await your config safely.

If you want to load internal values that should not be changed by the software you are using, you can have them in a separate, private, namespace (where you specify `configDir` when loading it so it cannot be looked for in the normal configuration folder of the main software).

## Contributing

To get started, you need mercurial and the correct plugins. Here is how to install those:

```sh
# You obviously need to install pip before, on ubuntu run sudo apt install python-pip
pip install --user mercurial hg-evolve
```

Clone the repository and add in its `.hg/hgrc`:
```ini
[experimental]
evolution = all
topic-mode = enforce

[extensions]
evolve =
topic =

[hooks]
pretxncommit.prepublish = npm run prepublishOnly
```

Create a topic branch to host your code:
```sh
hg topic my-feature
```

For more on topic branches, [read this tutorial](https://www.mercurial-scm.org/doc/evolution/tutorials/topic-tutorial.html).

Make your changes, commit as you wish, do your thing! Once you're ready to submit a change, request `Developer` access through Gitlab (you need an account). You should be granted some quickly.

Then push your topic branch to the server:
```sh
hg push -r my-feature
```

And open a merge request from it to `branch/default`!

That's it!