├── .eslintrc.js ├── .github └── workflows │ ├── nodejs.yaml │ ├── pre-release.yaml │ └── publish.yaml ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── .yarn └── releases │ └── yarn-sources.js ├── .yarnrc.yml ├── README.md ├── examples ├── config.yml └── index.ts ├── package.json ├── src ├── config-loader.options.ts ├── config-loader.spec.ts ├── config-loader.ts ├── index.ts └── node.extend.d.ts ├── test └── data │ ├── config-reload.yml │ └── config.yml ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/eslint-recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | 'prettier', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | rules: { 19 | '@typescript-eslint/interface-name-prefix': 'off', 20 | '@typescript-eslint/no-explicit-any': 'off', 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yaml: -------------------------------------------------------------------------------- 1 | 2 | name: Node.js CI 3 | 4 | on: 5 | push: 6 | branches: [ main ] 7 | pull_request: 8 | branches: [ main ] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-20.04 13 | 14 | strategy: 15 | matrix: 16 | node-version: ['14', '16'] 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v2 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v2 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | - name: Install dependencies 26 | run: yarn install --frozen-lockfile 27 | - name: Build package 28 | run: yarn build 29 | - name: Run tests 30 | run: yarn test 31 | - name: Run linter 32 | run: yarn lint 33 | -------------------------------------------------------------------------------- /.github/workflows/pre-release.yaml: -------------------------------------------------------------------------------- 1 | 2 | name: Node.js Package Pre-Release 3 | 4 | on: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-20.04 10 | steps: 11 | - name: Checkout repository 12 | uses: actions/checkout@v2 13 | - name: Use Node.js LTS 14 | uses: actions/setup-node@v2 15 | with: 16 | node-version: '14' 17 | - name: Install dependencies 18 | run: yarn install 19 | - name: Build package 20 | run: yarn build 21 | - name: Run tests 22 | run: yarn test 23 | 24 | publish-npm: 25 | needs: build 26 | runs-on: ubuntu-20.04 27 | steps: 28 | - name: Checkout repository 29 | uses: actions/checkout@v2 30 | - name: Use Node.js LTS 31 | uses: actions/setup-node@v2 32 | with: 33 | node-version: '14' 34 | registry-url: https://registry.npmjs.org/ 35 | - name: Install dependencies 36 | run: yarn install --frozen-lockfile 37 | - name: Build package 38 | run: yarn build 39 | - name: Publish 40 | run: npm publish --access=public 41 | env: 42 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 43 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | 2 | name: Node.js Package 3 | 4 | on: 5 | release: 6 | types: [created] 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-20.04 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v2 15 | - name: Use Node.js LTS 16 | uses: actions/setup-node@v2 17 | with: 18 | node-version: '14' 19 | - name: Install dependencies 20 | run: yarn install 21 | - name: Build package 22 | run: yarn build 23 | - name: Run tests 24 | run: yarn test 25 | 26 | publish-npm: 27 | needs: build 28 | runs-on: ubuntu-20.04 29 | steps: 30 | - name: Checkout repository 31 | uses: actions/checkout@v2 32 | - name: Use Node.js LTS 33 | uses: actions/setup-node@v2 34 | with: 35 | node-version: '14' 36 | registry-url: https://registry.npmjs.org/ 37 | - name: Install dependencies 38 | run: yarn install --frozen-lockfile 39 | - name: Build package 40 | run: yarn build 41 | - name: Publish 42 | run: npm publish --access=public 43 | env: 44 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Custom paths 2 | 3 | # Yarn v2 (not using Zero-Installs) 4 | .yarn/* 5 | !.yarn/releases 6 | !.yarn/plugins 7 | .pnp.* 8 | 9 | # Created by https://www.gitignore.io/api/osx,git,vim,node,code,linux,windows 10 | # Edit at https://www.gitignore.io/?templates=osx,git,vim,node,code,linux,windows 11 | 12 | ### Code ### 13 | .vscode/* 14 | !.vscode/settings.json 15 | !.vscode/tasks.json 16 | !.vscode/launch.json 17 | !.vscode/extensions.json 18 | 19 | ### Git ### 20 | # Created by git for backups. To disable backups in Git: 21 | # $ git config --global mergetool.keepBackup false 22 | *.orig 23 | 24 | # Created by git when using merge tools for conflicts 25 | *.BACKUP.* 26 | *.BASE.* 27 | *.LOCAL.* 28 | *.REMOTE.* 29 | *_BACKUP_*.txt 30 | *_BASE_*.txt 31 | *_LOCAL_*.txt 32 | *_REMOTE_*.txt 33 | 34 | ### Linux ### 35 | *~ 36 | 37 | # temporary files which can be created if a process still has a handle open of a deleted file 38 | .fuse_hidden* 39 | 40 | # KDE directory preferences 41 | .directory 42 | 43 | # Linux trash folder which might appear on any partition or disk 44 | .Trash-* 45 | 46 | # .nfs files are created when an open file is removed but is still being accessed 47 | .nfs* 48 | 49 | ### Node ### 50 | # Logs 51 | logs 52 | *.log 53 | npm-debug.log* 54 | yarn-debug.log* 55 | yarn-error.log* 56 | lerna-debug.log* 57 | 58 | # Diagnostic reports (https://nodejs.org/api/report.html) 59 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 60 | 61 | # Runtime data 62 | pids 63 | *.pid 64 | *.seed 65 | *.pid.lock 66 | 67 | # Directory for instrumented libs generated by jscoverage/JSCover 68 | lib-cov 69 | 70 | # Coverage directory used by tools like istanbul 71 | coverage 72 | *.lcov 73 | 74 | # nyc test coverage 75 | .nyc_output 76 | 77 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 78 | .grunt 79 | 80 | # Bower dependency directory (https://bower.io/) 81 | bower_components 82 | 83 | # node-waf configuration 84 | .lock-wscript 85 | 86 | # Compiled binary addons (https://nodejs.org/api/addons.html) 87 | build/Release 88 | 89 | # Dependency directories 90 | node_modules/ 91 | jspm_packages/ 92 | 93 | # TypeScript v1 declaration files 94 | typings/ 95 | 96 | # TypeScript cache 97 | *.tsbuildinfo 98 | 99 | # Optional npm cache directory 100 | .npm 101 | 102 | # Optional eslint cache 103 | .eslintcache 104 | 105 | # Optional REPL history 106 | .node_repl_history 107 | 108 | # Output of 'npm pack' 109 | *.tgz 110 | 111 | # Yarn Integrity file 112 | .yarn-integrity 113 | 114 | # dotenv environment variables file 115 | .env 116 | .env.test 117 | 118 | # parcel-bundler cache (https://parceljs.org/) 119 | .cache 120 | 121 | # next.js build output 122 | .next 123 | 124 | # nuxt.js build output 125 | .nuxt 126 | 127 | # rollup.js default build output 128 | dist/ 129 | 130 | # Uncomment the public line if your project uses Gatsby 131 | # https://nextjs.org/blog/next-9-1#public-directory-support 132 | # https://create-react-app.dev/docs/using-the-public-folder/#docsNav 133 | # public 134 | 135 | # Storybook build outputs 136 | .out 137 | .storybook-out 138 | 139 | # vuepress build output 140 | .vuepress/dist 141 | 142 | # Serverless directories 143 | .serverless/ 144 | 145 | # FuseBox cache 146 | .fusebox/ 147 | 148 | # DynamoDB Local files 149 | .dynamodb/ 150 | 151 | # Temporary folders 152 | tmp/ 153 | temp/ 154 | 155 | ### OSX ### 156 | # General 157 | .DS_Store 158 | .AppleDouble 159 | .LSOverride 160 | 161 | # Icon must end with two \r 162 | Icon 163 | 164 | # Thumbnails 165 | ._* 166 | 167 | # Files that might appear in the root of a volume 168 | .DocumentRevisions-V100 169 | .fseventsd 170 | .Spotlight-V100 171 | .TemporaryItems 172 | .Trashes 173 | .VolumeIcon.icns 174 | .com.apple.timemachine.donotpresent 175 | 176 | # Directories potentially created on remote AFP share 177 | .AppleDB 178 | .AppleDesktop 179 | Network Trash Folder 180 | Temporary Items 181 | .apdisk 182 | 183 | ### Vim ### 184 | # Swap 185 | [._]*.s[a-v][a-z] 186 | [._]*.sw[a-p] 187 | [._]s[a-rt-v][a-z] 188 | [._]ss[a-gi-z] 189 | [._]sw[a-p] 190 | 191 | # Session 192 | Session.vim 193 | Sessionx.vim 194 | 195 | # Temporary 196 | .netrwhist 197 | 198 | # Auto-generated tag files 199 | tags 200 | 201 | # Persistent undo 202 | [._]*.un~ 203 | 204 | # Coc configuration directory 205 | .vim 206 | 207 | ### Windows ### 208 | # Windows thumbnail cache files 209 | Thumbs.db 210 | Thumbs.db:encryptable 211 | ehthumbs.db 212 | ehthumbs_vista.db 213 | 214 | # Dump file 215 | *.stackdump 216 | 217 | # Folder config file 218 | [Dd]esktop.ini 219 | 220 | # Recycle Bin used on file shares 221 | $RECYCLE.BIN/ 222 | 223 | # Windows Installer files 224 | *.cab 225 | *.msi 226 | *.msix 227 | *.msm 228 | *.msp 229 | 230 | # Windows shortcuts 231 | *.lnk 232 | 233 | # End of https://www.gitignore.io/api/osx,git,vim,node,code,linux,windows 234 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": ".vscode/pnpify/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true 4 | } 5 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | yarnPath: .yarn/releases/yarn-sources.js 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-yaml-config 2 | 3 | [![Node.js CI](https://github.com/leafty/node-yaml-config/actions/workflows/nodejs.yaml/badge.svg)](https://github.com/leafty/node-yaml-config/actions/workflows/nodejs.yaml) 4 | 5 | Write your configuration files for node.js in yaml 6 | 7 | ## Installation 8 | 9 | ```bash 10 | npm install node-yaml-config 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```js 16 | var yaml_config = require('node-yaml-config'); 17 | 18 | var config = yaml_config.load(__dirname + '/config/config.yml'); 19 | 20 | console.log(config); 21 | ``` 22 | 23 | Or with Typescript: 24 | ```ts 25 | import { loadAsync } from 'node-yaml-config'; 26 | 27 | async function main() { 28 | const config = await loadAsync(__dirname + '/config/config.yml'); 29 | console.log(config); 30 | } 31 | main(); 32 | ``` 33 | 34 | ## Configuration Files 35 | 36 | In your configuration file: 37 | 38 | ```yaml 39 | default: 40 | server: 41 | port: 3000 42 | database: 43 | host: 'localhost' 44 | port: 27017 45 | development: 46 | database: 47 | db: 'dev_db' 48 | test: 49 | database: 50 | db: 'test_db' 51 | production: 52 | server: 53 | port: 8000 54 | database: 55 | db: 'prod_db' 56 | user: 'dbuser' 57 | password: 'pass' 58 | cache: 59 | dir: 'static' 60 | ``` 61 | 62 | **node-yaml-config** takes the configuration found in default, then overwrites it with the values found in the environment specific parts. The configuration file is loaded synchronously. 63 | 64 | ## API 65 | 66 | ### read(filename) 67 | 68 | Reads the configuration found in `filename`. 69 | 70 | ### load(filename[, env]) 71 | 72 | Load the configuration found in `filename` with the environment based on `NODE_ENV`. The environment can be forced with the `env` argument. 73 | 74 | **node-yaml-config** keeps parsed yaml files in memory to avoid reading files again. 75 | 76 | ### reload(filename) 77 | 78 | Reload the configuration found in `filename`. Later calls to `load` will show the new configuration. 79 | 80 | The file is loaded synchronously. 81 | 82 | ### readAsync(filename) 83 | 84 | Same as `read` but returns a Promise. 85 | 86 | ### loadAsync(filename[, env]) 87 | 88 | Same as `load` but returns a Promise. 89 | 90 | **node-yaml-config** keeps parsed yaml files in memory to avoid reading files again. 91 | 92 | ### reloadAsync(filename) 93 | 94 | Same as `reload` but returns a Promise. 95 | 96 | ## License 97 | 98 | Copyright (c) 2012-2021 Johann-Michael Thiebaut <[johann.thiebaut@gmail.com](mailto:johann.thiebaut@gmail.com)> 99 | 100 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 101 | 102 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 103 | 104 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 105 | -------------------------------------------------------------------------------- /examples/config.yml: -------------------------------------------------------------------------------- 1 | default: 2 | server: 3 | port: 3000 4 | database: 5 | host: 'localhost' 6 | port: 27017 7 | development: 8 | database: 9 | db: 'dev_db' 10 | test: 11 | database: 12 | db: 'test_db' 13 | production: 14 | server: 15 | port: 8000 16 | database: 17 | db: 'prod_db' 18 | user: 'dbuser' 19 | password: 'pass' 20 | cache: 21 | dir: 'static' 22 | -------------------------------------------------------------------------------- /examples/index.ts: -------------------------------------------------------------------------------- 1 | import { load } from 'node-yaml-config'; 2 | 3 | const config = load('examples/config.yml'); 4 | 5 | console.log(config); 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-yaml-config", 3 | "description": "Write your configuration files for node.js in yaml", 4 | "version": "1.0.0", 5 | "author": "Johann-Michael Thiebaut ", 6 | "license": "MIT", 7 | "main": "dist/index.js", 8 | "typings": "dist/index.d.ts", 9 | "files": [ 10 | "dist/" 11 | ], 12 | "directories": { 13 | "lib": "src", 14 | "example": "examples" 15 | }, 16 | "scripts": { 17 | "prebuild": "rimraf dist", 18 | "build": "yarn prebuild && tsc -p ./tsconfig.build.json", 19 | "build:watch": "tsc --watch", 20 | "format": "prettier --write \"src/**/*.ts\"", 21 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 22 | "test": "jest", 23 | "test:watch": "jest --watch", 24 | "test:cov": "jest --coverage", 25 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand" 26 | }, 27 | "dependencies": { 28 | "js-yaml": "^4.1.0", 29 | "node.extend": "^2.0.2" 30 | }, 31 | "devDependencies": { 32 | "@types/jest": "^27.0.2", 33 | "@types/js-yaml": "^4.0.3", 34 | "@types/node": "^16.10.2", 35 | "@typescript-eslint/eslint-plugin": "^4.32.0", 36 | "@typescript-eslint/parser": "^4.32.0", 37 | "eslint": "^7.32.0", 38 | "eslint-config-prettier": "^8.3.0", 39 | "eslint-plugin-import": "^2.24.2", 40 | "jest": "^27.2.4", 41 | "prettier": "^2.4.1", 42 | "rimraf": "^3.0.2", 43 | "ts-jest": "^27.0.5", 44 | "ts-node": "^10.2.1", 45 | "typescript": "^4.4.3" 46 | }, 47 | "homepage": "https://github.com/leafty/node-yaml-config", 48 | "repository": { 49 | "type": "git", 50 | "url": "https://github.com/leafty/node-yaml-config.git" 51 | }, 52 | "bugs": { 53 | "url": "https://github.com/leafty/node-yaml-config/issues" 54 | }, 55 | "keywords": [ 56 | "yaml", 57 | "config" 58 | ], 59 | "jest": { 60 | "moduleFileExtensions": [ 61 | "js", 62 | "json", 63 | "ts" 64 | ], 65 | "rootDir": "src", 66 | "testRegex": ".spec.ts$", 67 | "transform": { 68 | "^.+\\.(t|j)s$": "ts-jest" 69 | }, 70 | "coverageDirectory": "../coverage", 71 | "testEnvironment": "node" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/config-loader.options.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * config-loader.options.ts 3 | * ------------------------ 4 | * Options for ConfigLoader 5 | * Author: Johann-Michael Thiebaut 6 | */ 7 | 8 | /** Options for ConfigLoader */ 9 | export interface ConfigLoaderOptions { 10 | /** 11 | * Base path to load the configurations from. 12 | * 13 | * If not provided, [ConfigLoader] will default to the process working 14 | * directory. 15 | */ 16 | basePath?: string; 17 | } 18 | -------------------------------------------------------------------------------- /src/config-loader.spec.ts: -------------------------------------------------------------------------------- 1 | import { rename as renameLegacy } from 'fs'; 2 | import { resolve } from 'path'; 3 | import { promisify } from 'util'; 4 | import { ConfigLoader } from './config-loader'; 5 | 6 | const rename = promisify(renameLegacy); 7 | 8 | const testConfigDir = resolve(__dirname, '../test/data'); 9 | 10 | const configFile = 'config.yml'; 11 | const configFileBackup = 'config.yml.bak'; 12 | const configFileReload = 'config-reload.yml'; 13 | 14 | const configFileContents = { 15 | default: { 16 | database: { host: 'localhost', port: 27017 }, 17 | server: { port: 3000 }, 18 | }, 19 | development: { database: { db: 'dev_db' } }, 20 | production: { 21 | admins: ['superadmins', 'staff', 'leads'], 22 | cache: { dir: 'static' }, 23 | database: { db: 'prod_db', password: 'pass', user: 'dbuser' }, 24 | server: { port: 8000 }, 25 | }, 26 | test: { database: { db: 'test_db' } }, 27 | }; 28 | 29 | const configs = { 30 | development: { 31 | database: { db: 'dev_db', host: 'localhost', port: 27017 }, 32 | server: { port: 3000 }, 33 | }, 34 | test: { 35 | database: { db: 'test_db', host: 'localhost', port: 27017 }, 36 | server: { port: 3000 }, 37 | }, 38 | production: { 39 | admins: ['superadmins', 'staff', 'leads'], 40 | cache: { dir: 'static' }, 41 | database: { 42 | db: 'prod_db', 43 | host: 'localhost', 44 | password: 'pass', 45 | port: 27017, 46 | user: 'dbuser', 47 | }, 48 | server: { port: 8000 }, 49 | }, 50 | }; 51 | 52 | const reloadedConfigs = { 53 | development: { 54 | database: { db: 'dev_db', host: 'localhost', port: 27017 }, 55 | server: { port: 8080 }, 56 | }, 57 | test: { 58 | database: { db: 'test_db', host: 'localhost', port: 27017 }, 59 | server: { port: 8080 }, 60 | }, 61 | production: { 62 | admins: ['superadmins'], 63 | cache: { dir: 'cache' }, 64 | database: { 65 | db: 'prod_db', 66 | host: 'localhost', 67 | password: 'pass', 68 | port: 27017, 69 | user: 'dbuser', 70 | }, 71 | server: { port: 443 }, 72 | }, 73 | }; 74 | 75 | describe('ConfigLoader', () => { 76 | let configLoader: ConfigLoader; 77 | 78 | beforeEach(() => { 79 | configLoader = new ConfigLoader({ basePath: testConfigDir }); 80 | }); 81 | 82 | describe('read()', () => { 83 | it('should read a config file', () => { 84 | const contents = configLoader.read(configFile); 85 | 86 | expect(contents).toStrictEqual(configFileContents); 87 | }); 88 | 89 | it('should fail when the file does not exist', () => { 90 | expect(() => { 91 | configLoader.read('this_file_does_not_exits.yml'); 92 | }).toThrow('ENOENT'); 93 | }); 94 | }); 95 | 96 | describe('load()', () => { 97 | describe('should corectly load the different configurations', () => { 98 | it('development', () => { 99 | const config = configLoader.load(configFile, 'development'); 100 | 101 | expect(config).toStrictEqual(configs.development); 102 | }); 103 | it('test', () => { 104 | const config = configLoader.load(configFile, 'test'); 105 | 106 | expect(config).toStrictEqual(configs.test); 107 | }); 108 | it('production', () => { 109 | const config = configLoader.load(configFile, 'production'); 110 | 111 | expect(config).toStrictEqual(configs.production); 112 | }); 113 | }); 114 | 115 | describe('should not reread the file', () => { 116 | const move = async () => { 117 | await rename( 118 | resolve(testConfigDir, configFile), 119 | resolve(testConfigDir, configFileBackup), 120 | ); 121 | }; 122 | const restore = async () => { 123 | await rename( 124 | resolve(testConfigDir, configFileBackup), 125 | resolve(testConfigDir, configFile), 126 | ); 127 | }; 128 | 129 | afterEach(async () => { 130 | await restore(); 131 | }); 132 | 133 | it('development', async () => { 134 | configLoader.load(configFile); 135 | await move(); 136 | 137 | const config = configLoader.load(configFile, 'development'); 138 | 139 | expect(config).toStrictEqual(configs.development); 140 | }); 141 | it('test', async () => { 142 | configLoader.load(configFile); 143 | await move(); 144 | 145 | const config = configLoader.load(configFile, 'test'); 146 | 147 | expect(config).toStrictEqual(configs.test); 148 | }); 149 | it('production', async () => { 150 | configLoader.load(configFile); 151 | await move(); 152 | 153 | const config = configLoader.load(configFile, 'production'); 154 | 155 | expect(config).toStrictEqual(configs.production); 156 | }); 157 | }); 158 | }); 159 | 160 | describe('reload()', () => { 161 | describe('should correctly load the new configurations', () => { 162 | const move = async () => { 163 | await rename( 164 | resolve(testConfigDir, configFile), 165 | resolve(testConfigDir, configFileBackup), 166 | ); 167 | await rename( 168 | resolve(testConfigDir, configFileReload), 169 | resolve(testConfigDir, configFile), 170 | ); 171 | }; 172 | const restore = async () => { 173 | await rename( 174 | resolve(testConfigDir, configFile), 175 | resolve(testConfigDir, configFileReload), 176 | ); 177 | await rename( 178 | resolve(testConfigDir, configFileBackup), 179 | resolve(testConfigDir, configFile), 180 | ); 181 | }; 182 | 183 | afterEach(async () => { 184 | await restore(); 185 | }); 186 | 187 | it('development', async () => { 188 | configLoader.load(configFile); 189 | await move(); 190 | 191 | const config = configLoader.reload(configFile, 'development'); 192 | 193 | expect(config).toStrictEqual(reloadedConfigs.development); 194 | }); 195 | it('test', async () => { 196 | configLoader.load(configFile); 197 | await move(); 198 | 199 | const config = configLoader.reload(configFile, 'test'); 200 | 201 | expect(config).toStrictEqual(reloadedConfigs.test); 202 | }); 203 | it('production', async () => { 204 | configLoader.load(configFile); 205 | await move(); 206 | 207 | const config = configLoader.reload(configFile, 'production'); 208 | 209 | expect(config).toStrictEqual(reloadedConfigs.production); 210 | }); 211 | }); 212 | }); 213 | 214 | describe('readAsync()', () => { 215 | it('should read a config file', async () => { 216 | expect.assertions(1); 217 | 218 | const contents = await configLoader.readAsync(configFile); 219 | 220 | expect(contents).toStrictEqual(configFileContents); 221 | }); 222 | 223 | it('should fail when the file does not exist', async () => { 224 | expect.assertions(1); 225 | 226 | try { 227 | await configLoader.readAsync('this_file_does_not_exits.yml'); 228 | } catch (e) { 229 | expect(e).toMatchObject({ 230 | code: 'ENOENT', 231 | }); 232 | } 233 | }); 234 | }); 235 | 236 | describe('loadAsync()', () => { 237 | describe('should corectly load the different configurations', () => { 238 | it('development', async () => { 239 | const config = await configLoader.loadAsync(configFile, 'development'); 240 | 241 | expect(config).toStrictEqual(configs.development); 242 | }); 243 | it('test', async () => { 244 | const config = await configLoader.loadAsync(configFile, 'test'); 245 | 246 | expect(config).toStrictEqual(configs.test); 247 | }); 248 | it('production', async () => { 249 | const config = await configLoader.loadAsync(configFile, 'production'); 250 | 251 | expect(config).toStrictEqual(configs.production); 252 | }); 253 | }); 254 | 255 | describe('should not reread the file', () => { 256 | const move = async () => { 257 | await rename( 258 | resolve(testConfigDir, configFile), 259 | resolve(testConfigDir, configFileBackup), 260 | ); 261 | }; 262 | const restore = async () => { 263 | await rename( 264 | resolve(testConfigDir, configFileBackup), 265 | resolve(testConfigDir, configFile), 266 | ); 267 | }; 268 | 269 | afterEach(async () => { 270 | await restore(); 271 | }); 272 | 273 | it('development', async () => { 274 | await configLoader.loadAsync(configFile); 275 | await move(); 276 | 277 | const config = await configLoader.loadAsync(configFile, 'development'); 278 | 279 | expect(config).toStrictEqual(configs.development); 280 | }); 281 | it('test', async () => { 282 | await configLoader.loadAsync(configFile); 283 | await move(); 284 | 285 | const config = await configLoader.loadAsync(configFile, 'test'); 286 | 287 | expect(config).toStrictEqual(configs.test); 288 | }); 289 | it('production', async () => { 290 | await configLoader.loadAsync(configFile); 291 | await move(); 292 | 293 | const config = await configLoader.loadAsync(configFile, 'production'); 294 | 295 | expect(config).toStrictEqual(configs.production); 296 | }); 297 | }); 298 | }); 299 | 300 | describe('reloadAsync()', () => { 301 | describe('should correctly load the new configurations', () => { 302 | const move = async () => { 303 | await rename( 304 | resolve(testConfigDir, configFile), 305 | resolve(testConfigDir, configFileBackup), 306 | ); 307 | await rename( 308 | resolve(testConfigDir, configFileReload), 309 | resolve(testConfigDir, configFile), 310 | ); 311 | }; 312 | const restore = async () => { 313 | await rename( 314 | resolve(testConfigDir, configFile), 315 | resolve(testConfigDir, configFileReload), 316 | ); 317 | await rename( 318 | resolve(testConfigDir, configFileBackup), 319 | resolve(testConfigDir, configFile), 320 | ); 321 | }; 322 | 323 | afterEach(async () => { 324 | await restore(); 325 | }); 326 | 327 | it('development', async () => { 328 | await configLoader.loadAsync(configFile); 329 | await move(); 330 | 331 | const config = await configLoader.reloadAsync( 332 | configFile, 333 | 'development', 334 | ); 335 | 336 | expect(config).toStrictEqual(reloadedConfigs.development); 337 | }); 338 | it('test', async () => { 339 | await configLoader.loadAsync(configFile); 340 | await move(); 341 | 342 | const config = await configLoader.reloadAsync(configFile, 'test'); 343 | 344 | expect(config).toStrictEqual(reloadedConfigs.test); 345 | }); 346 | it('production', async () => { 347 | await configLoader.loadAsync(configFile); 348 | await move(); 349 | 350 | const config = await configLoader.reloadAsync(configFile, 'production'); 351 | 352 | expect(config).toStrictEqual(reloadedConfigs.production); 353 | }); 354 | }); 355 | }); 356 | }); 357 | -------------------------------------------------------------------------------- /src/config-loader.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * config-loader.ts 3 | * ---------------- 4 | * ConfigLoader is the class used to load yaml config files 5 | * Author: Johann-Michael Thiebaut 6 | */ 7 | 8 | import { readFile as readFileLegacy, readFileSync } from 'fs'; 9 | import { load } from 'js-yaml'; 10 | import * as extend from 'node.extend'; 11 | import { resolve } from 'path'; 12 | import { promisify } from 'util'; 13 | import { ConfigLoaderOptions } from './config-loader.options'; 14 | 15 | const readFile = promisify(readFileLegacy); 16 | 17 | /** Loads configuration from yaml files */ 18 | export class ConfigLoader { 19 | private readonly loadedFiles: Record = {}; 20 | 21 | constructor(private readonly options?: ConfigLoaderOptions) {} 22 | 23 | /** 24 | * Reads a yaml file 25 | */ 26 | read(filename: string): any { 27 | const path = this.getPath(filename); 28 | const data = load(readFileSync(path, { encoding: 'utf-8' })); 29 | this.loadedFiles[path] = data; 30 | return data; 31 | } 32 | 33 | /** 34 | * Loads a yaml configuration 35 | * If the file has already been parsed, the file is not read again. 36 | */ 37 | load(filename: string, env?: string): any { 38 | const path = this.getPath(filename); 39 | let data: any; 40 | 41 | if (this.loadedFiles.hasOwnProperty(path)) { 42 | data = this.loadedFiles[path]; 43 | } else { 44 | data = this.read(filename); 45 | } 46 | 47 | const _env = env || process.env.NODE_ENV || 'development'; 48 | const defaultConfig = data.default || {}; 49 | const extensionConfig = data[_env] || {}; 50 | 51 | return extend(true, extend(true, {}, defaultConfig), extensionConfig); 52 | } 53 | 54 | /** 55 | * Reloads a yaml configuration from disk 56 | * If the file has already been parsed, the file is not read again. 57 | */ 58 | reload(filename: string, env?: string): any { 59 | this.read(filename); 60 | return this.load(filename, env); 61 | } 62 | 63 | /** 64 | * Reads a yaml file (async variant) 65 | */ 66 | async readAsync(filename: string): Promise { 67 | const path = this.getPath(filename); 68 | const data = load(await readFile(path, { encoding: 'utf-8' })); 69 | this.loadedFiles[path] = data; 70 | return data; 71 | } 72 | 73 | /** 74 | * Loads a yaml configuration (async variant) 75 | * If the file has already been parsed, the file is not read again. 76 | */ 77 | async loadAsync(filename: string, env?: string): Promise { 78 | const path = this.getPath(filename); 79 | let data: any; 80 | 81 | if (this.loadedFiles.hasOwnProperty(path)) { 82 | data = this.loadedFiles[path]; 83 | } else { 84 | data = await this.readAsync(filename); 85 | } 86 | 87 | const _env = env || process.env.NODE_ENV || 'development'; 88 | const defaultConfig = data.default || {}; 89 | const extensionConfig = data[_env] || {}; 90 | 91 | return extend(true, extend(true, {}, defaultConfig), extensionConfig); 92 | } 93 | 94 | /** 95 | * Reloads a yaml configuration from disk (async variant) 96 | * If the file has already been parsed, the file is not read again. 97 | */ 98 | async reloadAsync(filename: string, env: string): Promise { 99 | await this.readAsync(filename); 100 | return this.loadAsync(filename, env); 101 | } 102 | 103 | private getPath(filename: string): string { 104 | if (this.options == undefined || this.options.basePath == undefined) { 105 | return resolve(filename); 106 | } 107 | return resolve(this.options.basePath, filename); 108 | } 109 | } 110 | 111 | /** Default ConfigLoader */ 112 | export const DEFAULT_CONFIG_LOADER = new ConfigLoader(); 113 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * node-yaml-config 3 | * ---------------- 4 | * Load yaml config files 5 | * Author: Johann-Michael Thiebaut 6 | */ 7 | 8 | import { DEFAULT_CONFIG_LOADER } from './config-loader'; 9 | 10 | export * from './config-loader'; 11 | export * from './config-loader.options'; 12 | 13 | export const read = DEFAULT_CONFIG_LOADER.read.bind(DEFAULT_CONFIG_LOADER); 14 | export const load = DEFAULT_CONFIG_LOADER.load.bind(DEFAULT_CONFIG_LOADER); 15 | export const reload = DEFAULT_CONFIG_LOADER.reload.bind(DEFAULT_CONFIG_LOADER); 16 | 17 | export const readAsync = DEFAULT_CONFIG_LOADER.readAsync.bind( 18 | DEFAULT_CONFIG_LOADER, 19 | ); 20 | export const loadAsync = DEFAULT_CONFIG_LOADER.loadAsync.bind( 21 | DEFAULT_CONFIG_LOADER, 22 | ); 23 | export const reloadAsync = DEFAULT_CONFIG_LOADER.reloadAsync.bind( 24 | DEFAULT_CONFIG_LOADER, 25 | ); 26 | -------------------------------------------------------------------------------- /src/node.extend.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'node.extend'; 2 | -------------------------------------------------------------------------------- /test/data/config-reload.yml: -------------------------------------------------------------------------------- 1 | default: 2 | server: 3 | port: 8080 4 | database: 5 | host: 'localhost' 6 | port: 27017 7 | development: 8 | database: 9 | db: 'dev_db' 10 | test: 11 | database: 12 | db: 'test_db' 13 | production: 14 | server: 15 | port: 443 16 | database: 17 | db: 'prod_db' 18 | user: 'dbuser' 19 | password: 'pass' 20 | cache: 21 | dir: 'cache' 22 | admins: 23 | - 'superadmins' 24 | -------------------------------------------------------------------------------- /test/data/config.yml: -------------------------------------------------------------------------------- 1 | default: 2 | server: 3 | port: 3000 4 | database: 5 | host: 'localhost' 6 | port: 27017 7 | development: 8 | database: 9 | db: 'dev_db' 10 | test: 11 | database: 12 | db: 'test_db' 13 | production: 14 | server: 15 | port: 8000 16 | database: 17 | db: 'prod_db' 18 | user: 'dbuser' 19 | password: 'pass' 20 | cache: 21 | dir: 'static' 22 | admins: 23 | - 'superadmins' 24 | - 'staff' 25 | - 'leads' 26 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "examples", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "declarationMap": true, 5 | "incremental": true, 6 | "module": "CommonJS", 7 | "outDir": "./dist", 8 | "removeComments": true, 9 | "sourceMap": true, 10 | "target": "ES2017", 11 | "strict": true, 12 | }, 13 | "exclude": ["node_modules", "dist", "examples"] 14 | } 15 | --------------------------------------------------------------------------------