Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: makeomatic/ms-conf
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v6.0.1
Choose a base ref
...
head repository: makeomatic/ms-conf
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v7.0.0
Choose a head ref
  • 1 commit
  • 15 files changed
  • 1 contributor

Commits on Apr 23, 2020

  1. feat: uses typescript, remove side-effects from exports

    BREAKING CHANGE: typescript, no hidden side-effects, node 12+
    AVVS committed Apr 23, 2020

    Verified

    This commit was signed with the committer’s verified signature.
    AVVS Vitaly Aminev
    Copy the full SHA
    9117a75 View commit details
Showing with 811 additions and 1,667 deletions.
  1. +0 −12 .babelrc
  2. +1 −1 .eslintrc
  3. +1 −0 .gitignore
  4. +1 −1 .mocharc.json
  5. +1 −9 .nycrc
  6. +24 −19 package.json
  7. +0 −101 src/index.js
  8. +68 −0 src/index.ts
  9. +0 −211 src/load-config.js
  10. +209 −0 src/load-config.ts
  11. +2 −8 test/.eslintrc
  12. +65 −67 test/{index.spec.js → index.spec.ts}
  13. +18 −0 tsconfig.build.json
  14. +23 −0 tsconfig.json
  15. +398 −1,238 yarn.lock
12 changes: 0 additions & 12 deletions .babelrc

This file was deleted.

2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"extends": "makeomatic"
"extends": "makeomatic/typescript"
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -31,3 +31,4 @@ node_modules

# compiled source code
lib
*.tsbuildinfo
2 changes: 1 addition & 1 deletion .mocharc.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"require": ["@babel/register"]
"require": ["ts-node/register", "source-map-support/register"]
}
10 changes: 1 addition & 9 deletions .nycrc
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
{
"include": [
"src/*.js"
],
"require": [
"@babel/register"
],
"sourceMap": false,
"instrument": false,
"cache": true,
"all": true,
"reporter": [
"lcov",
"json",
43 changes: 24 additions & 19 deletions package.json
Original file line number Diff line number Diff line change
@@ -3,10 +3,13 @@
"version": "0.0.0-development",
"description": "wrapper over dotenv and nconf module for one line configuration loading",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"scripts": {
"prepublishOnly": "rimraf ./lib; babel ./src -d ./lib",
"test": "yarn lint && cross-env NODE_ENV=test nyc mocha",
"lint": "eslint ./src",
"prepublishOnly": "rimraf ./lib; yarn compile",
"compile": "tsc -b tsconfig.build.json",
"pretest": "yarn compile",
"test": "yarn lint && cross-env NODE_ENV=test nyc mocha ./test/**.spec.ts",
"lint": "eslint --ext .ts,.js ./src ./test/**.spec.ts",
"semantic-release": "semantic-release"
},
"keywords": [
@@ -29,30 +32,32 @@
},
"homepage": "https://github.com/makeomatic/ms-conf#readme",
"devDependencies": {
"@babel/cli": "^7.8.4",
"@babel/core": "^7.9.0",
"@babel/plugin-transform-strict-mode": "^7.8.3",
"@babel/register": "^7.9.0",
"@makeomatic/deploy": "^10.1.3",
"babel-eslint": "^10.1.0",
"babel-plugin-istanbul": "^6.0.0",
"@makeomatic/deploy": "^10.1.4",
"@types/bluebird": "^3.5.30",
"@types/debug": "^4.1.5",
"@types/glob": "^7.1.1",
"@types/lodash.mergewith": "^4.6.6",
"@types/lodash.reduce": "^4.6.6",
"@types/lodash.uniq": "^4.5.6",
"@types/mocha": "^7.0.2",
"@types/nconf": "^0.10.0",
"@types/sinon": "^9.0.0",
"bluebird": "^3.7.2",
"codecov": "^3.6.5",
"cross-env": "^7.0.2",
"eslint": "^6.8.0",
"eslint-config-airbnb-base": "^14.1.0",
"eslint-config-makeomatic": "^4.0.0",
"eslint-plugin-import": "^2.20.1",
"eslint-config-makeomatic": "^5.0.0",
"eslint-plugin-mocha": "^6.3.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-unicorn": "^17.2.0",
"mocha": "^7.1.1",
"nyc": "^15.0.0",
"sinon": "^9.0.1"
"nyc": "^15.0.1",
"sinon": "^9.0.2",
"source-map-support": "^0.5.18",
"ts-node": "^8.9.0",
"typescript": "^3.8.3"
},
"dependencies": {
"@makeomatic/confidence": "^5.0.0",
"camelcase": "^5.3.1",
"@makeomatic/confidence": "6.0.1",
"camelcase": "^6.0.0",
"debug": "^4.1.0",
"dotenv": "^8.2.0",
"eventemitter3": "^4.0.0",
101 changes: 0 additions & 101 deletions src/index.js

This file was deleted.

68 changes: 68 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import EventEmitter = require('eventemitter3');
import _debug = require('debug');
import { Store, Criteria } from '@makeomatic/confidence'
import { strict as assert } from 'assert'
import { loadConfiguration, append, prependDefaultConfiguration } from './load-config'

const debug = _debug('ms-conf')

// uses confidence API to access store
let store: Store
let defaultOpts: Criteria = {}
let crashOnError = false
const EE = new EventEmitter()

export { append, prependDefaultConfiguration, EE }

// use this on sighup
export function reload() {
debug('reloading configuration')
store = new Store(loadConfiguration(crashOnError))
EE.emit('reload', store)
}

// hot-reload enabler
export function enableReload() {
debug('enabling sigusr')
process.on('SIGUSR1', reload)
process.on('SIGUSR2', reload)
}

// hot-reload disabler
export function disableReload() {
debug('disabling sigusr')
process.removeListener('SIGUSR1', reload)
process.removeListener('SIGUSR2', reload)
}

export function get<Response>(key: string, opts: Criteria = defaultOpts): Response | undefined {
if (!store) reload()
return store.get<Response>(key, opts)
}

export function meta<Response>(key: string, opts: Criteria = defaultOpts): Response | undefined {
if (!store) reload()
return store.meta(key, opts)
}

export function setDefaultOpts(opts: Criteria) {
assert.ok(opts, 'must be an object')
assert.ok(typeof opts === 'object', 'must be an object')
defaultOpts = opts
}

export function onReload(fn: EventEmitter.ListenerFn) {
EE.on('reload', fn)
}

export function offReload(fn: EventEmitter.ListenerFn) {
EE.off('reload', fn)
}

export function setCrashOnError(val: boolean) {
crashOnError = val
}

export function getCrashOnError(): boolean {
return crashOnError
}
211 changes: 0 additions & 211 deletions src/load-config.js

This file was deleted.

209 changes: 209 additions & 0 deletions src/load-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import { strict as assert } from 'assert'
import _debug = require('debug')
import camelCase = require('camelcase');
import nconf = require('nconf');
import dotenv = require('dotenv');
import reduce = require('lodash.reduce');
import merge = require('lodash.mergewith');
import uniq = require('lodash.uniq');
import fs = require('fs');
import glob = require('glob');
import path = require('path');

const debug = _debug('ms-conf')
const { hasOwnProperty } = Object.prototype
const { isArray } = Array
const { env } = process
const verbose = hasOwnProperty.call(env, 'DOTENV_NOT_SILENT') === false
const cwd = process.cwd()

let appendConfiguration: any

// safe json parse
function parseJSONSafe(possibleJSON: string) {
try {
return JSON.parse(possibleJSON)
} catch (e) {
return possibleJSON
}
}

// make camelCase keys
const camelCaseKeys = (camelize: boolean) => function processKeys(obj: any, value: any, key: string) {
const camelized = camelize ? key : camelCase(key)

if (value && typeof value === 'object') {
reduce(value, processKeys, (obj[camelized] = {}))
} else {
obj[camelized] = parseJSONSafe(value)
}

return obj
}

/**
* @param _ overwrite value, not used
* @param srcValue
*/
const customizer = (_: any, srcValue: any | any[]) => {
if (Array.isArray(srcValue)) {
return srcValue
}

return undefined
}

// read file from path and try to parse it
const readFile = (configuration: any, crashOnError: boolean) => (absPath: string) => {
assert(path.isAbsolute(absPath), `${absPath} must be an absolute path`)

try {
// delete loaded file
delete require.cache[absPath]
debug('loading %s', absPath)
// eslint-disable-next-line @typescript-eslint/no-var-requires
merge(configuration, require(absPath), customizer)
} catch (e) {
process.stderr.write(`Failed to include file ${absPath}, err: ${e.message}\n`)
if (crashOnError) {
throw e
}
}
}

export function possibleJSONStringToArray(filePaths: string) {
let files
try {
files = JSON.parse(filePaths)
} catch (e) {
files = [filePaths]
}

if (!isArray(files)) {
throw new Error('NCONF_FILE_PATH must be a stringified array or a string')
}

return files
}

function resolve(filePath: string) {
return require.resolve(filePath)
}

function resolveAbsPaths(paths: string[]) {
const absolutePaths = paths.reduce((resolvedPaths: string[], filePath) => {
const stats = fs.statSync(filePath)
if (stats.isFile()) {
resolvedPaths.push(resolve(filePath))
} else if (stats.isDirectory()) {
// NOTE: can be improved
// this is an extra call, but we dont care since it's a one-time op
const absPaths = glob
.sync(`${filePath}/*.{ts,js,json}`)
.map(resolve)
.filter((x) => x.endsWith('.d.ts') === false)

resolvedPaths.push(...absPaths)
}

return resolvedPaths
}, [])

return uniq(absolutePaths)
}

export function globFiles(filePaths: string | string[], configuration: any = {}, crashOnError: boolean) {
// if we get parsed JSON array - use it right away
const files = isArray(filePaths)
? filePaths
: possibleJSONStringToArray(filePaths)

// prepare merger
const mergeFile = readFile(configuration, crashOnError)

// resolve paths and merge
resolveAbsPaths(files).forEach(mergeFile)

return configuration
}

export function loadConfiguration(crashOnError: boolean) {
// load dotenv
const dotenvConfig = {
verbose,
encoding: env.DOTENV_ENCODING || 'utf-8',
path: env.DOTENV_FILE_PATH || `${cwd}/.env`,
}

// load dotenv and report error
const result = dotenv.config(dotenvConfig)
if (result.error) {
debug('failed to load %s due to', dotenvConfig.path, result.error)
}

// do we camelize?
const camelize = hasOwnProperty.call(env, 'NCONF_NO_CAMELCASE')
const namespaceKey = env.NCONF_NAMESPACE
const filePaths = env.NCONF_FILE_PATH

// if we don't have it yet
assert(namespaceKey, 'NCONF_NAMESPACE must be present in order to parse your configuration')

// init nconf store
nconf.use('memory')
nconf.reset()
nconf.argv()
nconf.env({
separator: env.NCONF_SEPARATOR || '__',
match: hasOwnProperty.call(env, 'NCONF_MATCH') ? new RegExp(env.NCONF_MATCH as string, env.NCONF_MATCH_OPTS) : null,
whitelist: hasOwnProperty.call(env, 'NCONF_WHITELIST') ? JSON.parse(env.NCONF_WHITELIST as string) : null,
})

// pull camelCase data
const namespace = nconf.get(namespaceKey)
const configFromEnv = reduce(namespace, camelCaseKeys(camelize), {})
const config = Object.create(null)

if (filePaths) {
// nconf file does not merge configuration, it will either omit it
// or overwrite it, since it's JSON and is already parsed, what we will
// do is pass it into configuration as is after it was already camelCased and merged
//
// nconf.file(env.NCONF_FILE_PATH);

globFiles(filePaths, config, crashOnError)
}

merge(config, configFromEnv)

if (appendConfiguration !== undefined) {
merge(config, appendConfiguration, customizer)
}

return config
}

/**
* Add base configuration
*/
export function prependDefaultConfiguration(baseConfig: unknown) {
assert.ok(baseConfig, 'must be a path to specific location')
assert.ok(typeof baseConfig === 'string')

let files = null
if (env.NCONF_FILE_PATH) {
files = possibleJSONStringToArray(env.NCONF_FILE_PATH)
files.unshift(baseConfig)
} else {
files = [baseConfig]
}

env.NCONF_FILE_PATH = JSON.stringify(files)
}

/**
* Appends passed configuration to resolved config
*/
export function append(configuration: any) {
appendConfiguration = configuration
}
10 changes: 2 additions & 8 deletions test/.eslintrc
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
{
"plugins": [
"mocha"
],
"plugins": ["mocha"],
"env": {
"mocha": true
},
"rules": {
"prefer-arrow-callback": 0,
"global-require": 0
}
}
}
132 changes: 65 additions & 67 deletions test/index.spec.js → test/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,53 @@
const Promise = require('bluebird');
const assert = require('assert');
const sinon = require('sinon');
import { delay } from 'bluebird'
import assert = require('assert')
import sinon = require('sinon')
import * as store from '..'

const { env } = process;
const { env } = process

describe('Configuration loader', () => {
let store;
let mod;
let mod: any

env.DOTENV_FILE_PATH = `${__dirname}/.env`;
env.DOTENV_FILE_PATH = `${__dirname}/.env`
env.NCONF_FILE_PATH = JSON.stringify([
`${__dirname}/config.json`,
`${__dirname}/dir`,
]);
])

it('should load configuration', () => {
store = require('../src');
mod = store.get('/');
});
it('should load configuration', async () => {
mod = store.get('/')
})

it('should correctly use match env option', () => {
assert.equal(Object.keys(mod).length, 9);
assert.ok(mod.amqp);
assert.ok(mod.value);
assert.ok(mod.expanded);
});
assert.equal(Object.keys(mod).length, 9)
assert.ok(mod.amqp)
assert.ok(mod.value)
assert.ok(mod.expanded)
})

it('should overwrite config values from file using env values', () => {
assert.strictEqual(mod.overwritten.by.env, true);
});
assert.strictEqual(mod.overwritten.by.env, true)
})

it('correctly omits options', () => {
assert.ifError(mod.omit);
assert.ok(mod.whitelist);
});
assert.ifError(mod.omit)
assert.ok(mod.whitelist)
})

it('does not expand values', () => {
assert.equal(mod.expanded, '$MS_CONF___VALUE');
assert.equal(mod.value, 'darn');
});
assert.equal(mod.expanded, '$MS_CONF___VALUE')
assert.equal(mod.value, 'darn')
})

it('parses correct json and returns original text if it is not', () => {
assert.deepEqual(mod.amqp.hosts, ['127.0.0.1']);
assert.equal(mod.amqp.invalidJson, '{"test":bad}');
});
assert.deepEqual(mod.amqp.hosts, ['127.0.0.1'])
assert.equal(mod.amqp.invalidJson, '{"test":bad}')
})

it('file was loaded and configuration was merged', () => {
assert.ok(mod.my);
assert.equal(mod.amqp.ssl, false);
});
assert.ok(mod.my)
assert.equal(mod.amqp.ssl, false)
})

it('produces correct configuration', () => {
assert.deepEqual(mod, {
@@ -72,76 +71,75 @@ describe('Configuration loader', () => {
pot: 'is-json',
array: [1, 3],
overwritten: { by: { env: true } },
});
});
})
})

it('enables hot-reload', () => {
store.enableReload();
store.enableReload()

env.NCONF_FILE_PATH = JSON.stringify([
`${__dirname}/config.json`,
`${__dirname}/dir`,
`${__dirname}/reload.json`,
]);
])

process.kill(process.pid, 'SIGUSR2');
process.kill(process.pid, 'SIGUSR2')

// SIGHUP comes in as async action
return Promise.delay(10)
return delay(10)
.then(() => {
assert(store.get('/reloaded'));
return null;
});
});
assert(store.get('/reloaded'))
return null
})
})

it('dynamic overwrites', () => {
store.append({
boose: 'works',
});
})

assert.equal(store.get('/boose'), undefined);
process.kill(process.pid, 'SIGUSR2');
assert.equal(store.get('/boose'), undefined)
process.kill(process.pid, 'SIGUSR2')

// SIGHUP comes in as async action
return Promise.delay(1).then(() => {
assert.equal(store.get('/boose'), 'works');
return null;
});
});
return delay(1).then(() => {
assert.equal(store.get('/boose'), 'works')
return null
})
})

it('disables hot-reload', () => {
store.disableReload();
store.disableReload()

env.NCONF_FILE_PATH = JSON.stringify([
`${__dirname}/config.json`,
`${__dirname}/dir`,
]);
])

// so that process doesnt die
const spy = sinon.spy();
const spy = sinon.spy()

process.on('SIGUSR2', spy);
process.kill(process.pid, 'SIGUSR2');
process.on('SIGUSR2', spy)
process.kill(process.pid, 'SIGUSR2')

// SIGHUP comes in as async action
return Promise
.delay(10)
return delay(10)
.then(() => {
assert.equal(store.get('/reloaded'), true);
assert.equal(spy.calledOnce, true);
return null;
assert.equal(store.get('/reloaded'), true)
assert.equal(spy.calledOnce, true)
return null
})
.finally(() => {
process.removeListener('SIGUSR2', spy);
});
});
process.removeListener('SIGUSR2', spy)
})
})

it('crashes on error when reading files', () => {
env.NCONF_FILE_PATH = JSON.stringify([
`${__dirname}/dir`,
]);
])

store.crashOnError = true;
assert.throws(() => store.reload(), 'must throw with malformed.json');
});
});
store.setCrashOnError(true)
assert.throws(() => store.reload(), 'must throw with malformed.json')
})
})
18 changes: 18 additions & 0 deletions tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib"
},
"include": [
"./src/*.ts",
"./src/**/*.ts"
],
"exclude": [
"**/node_modules/**",
"lib",
"dist",
"**.test.ts",
"**.spec.ts"
]
}
23 changes: 23 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node",
"target": "ES2019",
"declaration": true,
"alwaysStrict": true,
"sourceMap": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"preserveConstEnums": true,
"composite": true
},
"exclude": [
"**/node_modules/**",
"lib",
"dist"
]
}
1,636 changes: 398 additions & 1,238 deletions yarn.lock

Large diffs are not rendered by default.