Home

Awesome

<h1 align="center"> <img src="https://raw.githubusercontent.com/ngryman/artworks/master/fauda/heading/fauda@2x.png" alt="Fauda" with="600"> </h1> <h4 align="center">Configuration made simple.</h4> <p align="center"> <a href="https://www.npmjs.com/package/fauda"> <img src="https://img.shields.io/npm/v/fauda" /> </a> <a href="https://github.com/ngryman/fauda/actions"> <img src="https://img.shields.io/github/workflow/status/ngryman/fauda/ci" /> </a> </p> <p align="center"> <a href="#features">Features</a> • <a href="#getting-started">Getting Started</a> • <a href="#how-does-it-work">How does it work?</a> • <a href="#usage">Usage</a> • <a href="#api">API</a> • <a href="#cli">CLI</a> • <a href="#faq">FAQ</a> </p>

Features

Fauda is an all-in-one library that:

  1. loads options from multiple sources: env vars, CLI options, and configuration files.
  2. merges them together in one unified configuration object.
  3. normalizes it by validating against a JSON schema and setting default values.

It offers the following advantages:

Getting Started

<details> <summary><b>1️⃣ Install</b> Fauda</summary><br>
npm install fauda
</details> <details> <summary><b>2️⃣ Set up</b> your JSON schema</summary><br>

Fauda uses a JSON schema to load and normalize your configuration.

Create a schema.json file:

{
  "$schema": "http://json-schema.org/draft-07/schema",
  "title": "My awesome app configuration",
  "type": "object",
  "properties": {
    "$schema": {
      "description": "Path to my app's schema.",
      "type": "string"
    },
    "port": {
      "description": "The port the server listens to.",
      "type": "number",
      "default": 3000
    },
    "open": {
      "description": "Open in a browser tab if true.",
      "type": "boolean",
      "default": false
    },
    "mode": {
      "description": "Mode of the app.",
      "type": "string",
      "enum": ["development", "production"],
      "default": "${NODE_ENV}"
    },
    "publicPages": {
      "description": "A list of public pages.",
      "type": "array",
      "items": {
        "type": "string"
      }
    }
  },
  "required": ["publicPages"]
}

For more information on JSON schemas, you can take a look at their Getting Started guide.

</details> <details> <summary><b>3️⃣ Generate</b> types <i>(optional)</i></summary><br>

Generating types allows you to have a strongly typed configuration object in your code. As a bonus, it also enables autocompletion for Typescript configuration files!

Generate a src/configuration.ts file:

$ npx fauda types

This will generate the following file:

export interface Configuration {
  port?: number
  open?: boolean
  mode?: 'development' | 'production'
  publicPages: string[]
}

For more information about generating types, please take a look at the CLI section.

</details> <details> <summary><b>4️⃣ Load & validate</b> your configuration.</summary><br>

Assuming your package's name is my-app:

import { fauda } from 'fauda'
import { Configuration } from './configuration'

async function loadConfiguration() {
  try {
    const configuration = await fauda<Configuration>('my-app')
  } catch (err) {
    console.error(err.message)
  }
}
</details>

How does it work?

Fauda loads your configuration from several sources using the following order of precedence: environment variables > CLI options > configuration files.

Option names are inflected according the source's typical naming convention:

SourceCasingExample
Configuration filecamelpublicPages
CLI optionskebab--public-pages
Environment variableupper+snakeMY_APP_PUBLIC_PAGES

Once your configuration is loaded, Fauda normalizes it into a valid configuration object that your library / application can use. The normalization process validates your configuration using the provided JSON schema. It checks that the type of options are valid, required options are specified, sets default values, and also expand environment variables references!

<details> <summary>🙋🏻‍♂️ <i>What is environment variable expansion?</i></summary><br>

You can reference an environment variable name's as your option's value. Fauda will replace its value at runtime, giving you the opportunity to depend on any environment variable in your configuration.

For instance, if you have a mode option that varies depending on the NODE_ENV's value, you can do it like this:

"mode": {
  "default": "${NODE_ENV}"
}

Note that you can also reference environment variables in your JSON schema using the default value:

"mode": {
  "description": "Mode of the app.",
  "type": "string",
  "enum": ["development", "production"],
  "default": "${NODE_ENV}"
}
</details>

Usage

Configuration files

Fauda first searches for a config.${myApp} property in the package.json file of your users. If not found, it then searches for a various configuration files, starting from the current directory up to the root.

Here is a list of the configuration file names and formats that are supported:

FileFormat
.${myPackage}rcjson
${myPackage}.config.jsjs
${myPackage}.config.jsonjson
${myPackage}.config.tsts
${myPackage}.config.yamlyaml
${myPackage}.config.ymlyaml
.config/${myPackage}rcjson
.config/${myPackage}.config.jsjs
.config/${myPackage}.config.jsonjson
.config/${myPackage}.config.tsts
.config/${myPackage}.config.yamlyaml
.config/\${myPackage}.config.ymlyaml

CLI options

Fauda parses CLI options as you can expect from any other argument parsers!

Options are "kebab-"cased. For instance, the publicPages option is transposed as the --public-pages CLI argument.

<details> <summary>🙋🏻‍♂️ <i>What about arrays?</i></summary><br>

Arrays are supported in two ways.

  1. Declare a JSON-compatible array as value.
  2. Use the same argument multiple times.

Here's an example that gives the same result:

$ my-app --public-pages=/home --public-pages=/about
$ my-app --types="['/home', '/about']"
</details>

Environment variables

Fauda parses environment variables prefixed with your package's name. This is precaution to avoid name clashes with other application or system-wide environment variables.

For instance, if your package's name is my-app, Fauda will parse variables with the MY_APP_ prefix.

<details> <summary>🙋🏻‍♂️ <i>What about arrays?</i></summary><br>

Arrays are supported! You simply need to declare a JSON-compatible array wrapped between quotes.

Here's an example:

$ MY_APP_PUBLIC_PAGES="['/home', '/about']"
</details>

API

fauda

Loads, normalizes, and returns a configuration object from multiple sources.

Params

Examples

import { fauda } from 'fauda'

const configuration = await fauda('my-app', 'schema.json')

FaudaOptions

OptionTypeDefaultDescription
argsstring[]process.argvArray of CLI options, used by the CLI options loader.
envNodeJS.ProcessEnvprocess.envDictionary of environment variables, used by the environment variables loader.
cwdstringprocess.cwd()Array of CLI options, used by the configuration files loader.

normalize

Normalizes a configuration object using the provided JSON schema.

Params

Examples

import { normalize } from 'fauda'

const schema = {
  type: 'object',
  properties: {
    foo: {
      type: 'string',
      default: 'bar'
    }
  }
}

const safeConfig = await normalize({}, 'schema.json')
// { foo: 'bar' }

generateTypes

Returns the Configuration type definition inferred from the given JSON schema.

Params

Examples

import { promises as fs } from 'fs'
import { generateTypes } from 'fauda'

const source = await generateTypes('schema.json')
await fs.writeFile('src/configuration.ts', source)

CLI

Generate types

Fauda comes with a CLI utility that automatically generates types from your JSON schema. This is especially useful to provide a strongly typed configuration object to your Typescript project, but also to provide auto-completion for your users using a .ts configuration file.

By default, the types command will read a schema.json file and write the output to a src/configuration.ts file:

You can change these defaults with the following CLI options:

Examples

$ fauda types
$ fauda types -i my-schema.json -o src/types/configuration.ts
<details> <summary>🙋🏻‍♂️ <i>How can I integrate this in my workflow?</i></summary><br>

Typescript projects generally have a build script to transpile sources to plain JavaScript. You can generate your types right before using with the prebuild script:

{
  "scripts": {
    "build": "tsc",
    "prebuild": "fauda types"
  }
}

Usually you will also want to watch for changes to your schema and reflect these changes in the generated types to benefit from your IDE's automcompletion.

Assuming you have dev script that watches for changes, you could split your scripts to transpile your code and generate your types in two different scripts, and use a tool like npm-run-all to run them sequentially:

{
  "scripts": {
    "dev": "run-s dev:*",
    "dev:types": "fauda types",
    "dev:build": "tsc -w"
  }
}
</details>

FAQ

Why not supporting nested options?

Fauda only supports a flat options object, simply because it's easier to manipulate and to reason about. In my opinion, nested options are usually neither a good idea as it makes merging default options harder, nor necessary as one can express a sense of hierarchy using "dotted names" instead (eg. cooking.time) or just camel case (eg. cookingTime).