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==",
|
"integrity": "sha512-/kQvbVzdEpOq4tEWT79yAHSM4nH4xMlhJv2GrLVQt4Qmo8yYsPdioBM1QpN/2GX1wkfMnyXvdoftvLUr0LBj7Q==",
|
||||||
"dev": true
|
"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": {
|
"@types/mime": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.0.tgz",
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
"@types/express-session": "^1.15.11",
|
"@types/express-session": "^1.15.11",
|
||||||
"@types/http-errors": "^1.6.1",
|
"@types/http-errors": "^1.6.1",
|
||||||
"@types/jest": "^23.3.12",
|
"@types/jest": "^23.3.12",
|
||||||
|
"@types/js-yaml": "^3.12.0",
|
||||||
"@types/node": "^10.12.18",
|
"@types/node": "^10.12.18",
|
||||||
"@types/passport": "^1.0.0",
|
"@types/passport": "^1.0.0",
|
||||||
"@types/passport-local": "^1.0.33",
|
"@types/passport-local": "^1.0.33",
|
||||||
@ -26,10 +27,6 @@
|
|||||||
"@types/supertest": "^2.0.7",
|
"@types/supertest": "^2.0.7",
|
||||||
"@types/uuid": "^3.4.4",
|
"@types/uuid": "^3.4.4",
|
||||||
"axios": "^0.18.0",
|
"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",
|
"browserify": "^16.2.3",
|
||||||
"buildfile": "^1.2.17",
|
"buildfile": "^1.2.17",
|
||||||
"jest": "^23.6.0",
|
"jest": "^23.6.0",
|
||||||
@ -37,6 +34,10 @@
|
|||||||
"loose-envify": "^1.4.0",
|
"loose-envify": "^1.4.0",
|
||||||
"nodemon": "^1.18.9",
|
"nodemon": "^1.18.9",
|
||||||
"npm-run-all": "^4.1.5",
|
"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",
|
"std-mocks": "^1.0.1",
|
||||||
"supertest": "^3.3.0",
|
"supertest": "^3.3.0",
|
||||||
"terser": "^3.14.1",
|
"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