Add ConfigReader.ts
This commit is contained in:
parent
694d1857b3
commit
a5ef6bd981
6
package-lock.json
generated
6
package-lock.json
generated
@ -932,6 +932,12 @@
|
||||
"integrity": "sha512-/kQvbVzdEpOq4tEWT79yAHSM4nH4xMlhJv2GrLVQt4Qmo8yYsPdioBM1QpN/2GX1wkfMnyXvdoftvLUr0LBj7Q==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/js-yaml": {
|
||||
"version": "3.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-3.12.0.tgz",
|
||||
"integrity": "sha512-UGEe/6RsNAxgWdknhzFZbCxuYc5I7b/YEKlfKbo+76SM8CJzGs7XKCj7zyugXViRbKYpXhSXhCYVQZL5tmDbpQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/mime": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.0.tgz",
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
"@types/express-session": "^1.15.11",
|
||||
"@types/http-errors": "^1.6.1",
|
||||
"@types/jest": "^23.3.12",
|
||||
"@types/js-yaml": "^3.12.0",
|
||||
"@types/node": "^10.12.18",
|
||||
"@types/passport": "^1.0.0",
|
||||
"@types/passport-local": "^1.0.33",
|
||||
@ -26,10 +27,6 @@
|
||||
"@types/supertest": "^2.0.7",
|
||||
"@types/uuid": "^3.4.4",
|
||||
"axios": "^0.18.0",
|
||||
"react": "^16.7.0",
|
||||
"react-dom": "^16.7.0",
|
||||
"react-redux": "^6.0.0",
|
||||
"redux": "^4.0.1",
|
||||
"browserify": "^16.2.3",
|
||||
"buildfile": "^1.2.17",
|
||||
"jest": "^23.6.0",
|
||||
@ -37,6 +34,10 @@
|
||||
"loose-envify": "^1.4.0",
|
||||
"nodemon": "^1.18.9",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"react": "^16.7.0",
|
||||
"react-dom": "^16.7.0",
|
||||
"react-redux": "^6.0.0",
|
||||
"redux": "^4.0.1",
|
||||
"std-mocks": "^1.0.1",
|
||||
"supertest": "^3.3.0",
|
||||
"terser": "^3.14.1",
|
||||
|
||||
29
packages/server/src/config/Config.ts
Normal file
29
packages/server/src/config/Config.ts
Normal file
@ -0,0 +1,29 @@
|
||||
export class Config {
|
||||
constructor(protected readonly config: any) {}
|
||||
|
||||
get(key: string) {
|
||||
let value = this.config
|
||||
key.split('.').forEach(k => {
|
||||
if (!value.hasOwnProperty(k)) {
|
||||
throw new Error(`Property "${k}" from "${key}" does not exist`)
|
||||
}
|
||||
value = value[k]
|
||||
})
|
||||
return value
|
||||
}
|
||||
|
||||
has(key: string) {
|
||||
let c = this.config
|
||||
return key.split('.').every(k => {
|
||||
const has = c.hasOwnProperty(k)
|
||||
if (has) {
|
||||
c = c[k]
|
||||
}
|
||||
return has
|
||||
})
|
||||
}
|
||||
|
||||
value() {
|
||||
return this.config
|
||||
}
|
||||
}
|
||||
74
packages/server/src/config/ConfigReader.test.ts
Normal file
74
packages/server/src/config/ConfigReader.test.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import {ConfigReader} from './ConfigReader'
|
||||
import {join} from 'path'
|
||||
import {closeSync, openSync} from 'fs'
|
||||
|
||||
describe('ConfigReader', () => {
|
||||
|
||||
beforeAll(() => {
|
||||
closeSync(
|
||||
openSync(
|
||||
join(__dirname, 'test-files', 'package.json'),
|
||||
'w',
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
describe('read', () => {
|
||||
|
||||
it('reads and merges configuration files from package root', () => {
|
||||
const config = new ConfigReader(__dirname).read()
|
||||
expect(config.get('app.name')).toEqual(jasmine.any(String))
|
||||
})
|
||||
|
||||
it('reads and merges configuration files from CWD', () => {
|
||||
const config = new ConfigReader(
|
||||
join(__dirname, 'test-files', 'dir'),
|
||||
'/tmp/path',
|
||||
).read()
|
||||
expect(config.value()).toEqual({
|
||||
a: 1,
|
||||
b: 'test',
|
||||
c: {
|
||||
d: 'value',
|
||||
e: ['entry 3'],
|
||||
f: 'extra value',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('fails when not a single config file found', () => {
|
||||
expect(() => new ConfigReader('/tmp/test', '/tmp/test').read())
|
||||
.toThrowError('No config files found')
|
||||
})
|
||||
|
||||
it('succeeds when custom filename is read', () => {
|
||||
expect(() => new ConfigReader('/tmp/test', '/tmp/test').read())
|
||||
.toThrowError('No config files found')
|
||||
})
|
||||
|
||||
describe('environment variable', () => {
|
||||
const origConfig = process.env.CONFIG
|
||||
afterEach(() => {
|
||||
process.env.CONFIG = origConfig
|
||||
})
|
||||
|
||||
it('succeeds when config from env variable is read', () => {
|
||||
process.env.CONFIG = '---\na: 2'
|
||||
const config = new ConfigReader(
|
||||
join(__dirname, 'test-files', 'dir'),
|
||||
'/tmp/path',
|
||||
).read()
|
||||
expect(config.value()).toEqual({
|
||||
a: 2,
|
||||
b: 'test',
|
||||
c: {
|
||||
d: 'value',
|
||||
e: ['entry 3'],
|
||||
f: 'extra value',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
107
packages/server/src/config/ConfigReader.ts
Normal file
107
packages/server/src/config/ConfigReader.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import YAML from 'js-yaml'
|
||||
import {Config} from './Config'
|
||||
import {findPackageRoot} from '../files/Find'
|
||||
import {join} from 'path'
|
||||
import {readFileSync} from 'fs'
|
||||
|
||||
const isObject = (value: any) => value !== null && typeof value === 'object'
|
||||
|
||||
export class ConfigReader {
|
||||
protected readonly config: any = {}
|
||||
protected readonly locations: string[]
|
||||
protected readonly filenames: string[]
|
||||
|
||||
constructor(
|
||||
readonly path: string,
|
||||
readonly cwd: string|undefined = process.cwd(),
|
||||
readonly environment = 'CONFIG',
|
||||
) {
|
||||
const packageRoot = path && findPackageRoot(path)
|
||||
this.locations = packageRoot ? [packageRoot] : []
|
||||
if (cwd && cwd !== packageRoot) {
|
||||
this.locations.push(cwd)
|
||||
}
|
||||
this.filenames = [
|
||||
'default.yml',
|
||||
`${process.env.NODE_ENV || 'development'}.yml`,
|
||||
]
|
||||
}
|
||||
|
||||
read(filename?: string) {
|
||||
let success = 0
|
||||
for (const location of this.locations) {
|
||||
for (const fname of this.filenames) {
|
||||
const configFilename = join(location, 'config', fname)
|
||||
try {
|
||||
this.readFile(configFilename)
|
||||
} catch (err) {
|
||||
if (err.code !== 'ENOENT') {
|
||||
throw err
|
||||
}
|
||||
continue
|
||||
}
|
||||
success += 1
|
||||
}
|
||||
}
|
||||
|
||||
const env = process.env[this.environment]
|
||||
if (!filename && !env && !success) {
|
||||
throw new Error('No config files found')
|
||||
}
|
||||
|
||||
if (filename) {
|
||||
this.readFile(filename)
|
||||
}
|
||||
|
||||
if (env) {
|
||||
this.parse(env)
|
||||
}
|
||||
|
||||
return new Config(this.config)
|
||||
}
|
||||
|
||||
readFile(filename: string) {
|
||||
const yaml = readFileSync(filename, 'utf-8')
|
||||
const config = YAML.safeLoad(yaml)
|
||||
if (config) {
|
||||
this.mergeConfig(config, this.config)
|
||||
}
|
||||
}
|
||||
|
||||
parse(yaml: string) {
|
||||
const config = YAML.safeLoad(yaml)
|
||||
this.mergeConfig(config, this.config)
|
||||
}
|
||||
|
||||
protected mergeConfig(source: any, destination: any) {
|
||||
const stack = [{src: source, dest: destination}]
|
||||
|
||||
// let i = 0
|
||||
while (stack.length) {
|
||||
// i++
|
||||
const {src, dest} = stack.pop()!
|
||||
const keys = Object.keys(src)
|
||||
|
||||
keys.forEach(key => {
|
||||
// if (i > 100) {
|
||||
// throw new Error('overload')
|
||||
// }
|
||||
const value = src[key]
|
||||
if (isObject(value) && !Array.isArray(value)) {
|
||||
if (!dest.hasOwnProperty(key) ||
|
||||
Array.isArray(dest[key]) ||
|
||||
!isObject(dest[key])
|
||||
) {
|
||||
dest[key] = {}
|
||||
}
|
||||
stack.push({src: value, dest: dest[key]})
|
||||
return
|
||||
}
|
||||
dest[key] = value
|
||||
})
|
||||
}
|
||||
|
||||
return destination
|
||||
}
|
||||
|
||||
}
|
||||
1
packages/server/src/config/test-files/.gitignore
vendored
Normal file
1
packages/server/src/config/test-files/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
package.json
|
||||
6
packages/server/src/config/test-files/config/default.yml
Normal file
6
packages/server/src/config/test-files/config/default.yml
Normal file
@ -0,0 +1,6 @@
|
||||
a: 1
|
||||
b: 'test'
|
||||
c:
|
||||
d: value
|
||||
e:
|
||||
- entry
|
||||
5
packages/server/src/config/test-files/config/test.yml
Normal file
5
packages/server/src/config/test-files/config/test.yml
Normal file
@ -0,0 +1,5 @@
|
||||
c:
|
||||
d: value
|
||||
e:
|
||||
- entry 3
|
||||
f: extra value
|
||||
30
packages/server/src/files/Find.ts
Normal file
30
packages/server/src/files/Find.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import {resolve, join} from 'path'
|
||||
import {statSync, Stats} from 'fs'
|
||||
|
||||
export function findNearestDirectory(
|
||||
dir: string, filename: string,
|
||||
) {
|
||||
let currentDir = dir
|
||||
let lastDir = dir
|
||||
do {
|
||||
let s: Stats
|
||||
try {
|
||||
s = statSync(join(currentDir, filename))
|
||||
} catch (err) {
|
||||
lastDir = currentDir
|
||||
currentDir = resolve(currentDir, '..')
|
||||
continue
|
||||
}
|
||||
if (!s.isFile()) {
|
||||
lastDir = currentDir
|
||||
currentDir = resolve(currentDir, '..')
|
||||
continue
|
||||
}
|
||||
|
||||
return currentDir
|
||||
} while (lastDir !== currentDir)
|
||||
}
|
||||
|
||||
export function findPackageRoot(dir: string = __dirname) {
|
||||
return findNearestDirectory(dir, 'package.json')
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user