├── .circleci └── config.yml ├── .eslintrc ├── .gitignore ├── .prettierrc ├── .release-it.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── SECURITY.md ├── docs ├── .gitignore ├── .vuepress │ └── config.js ├── README.md ├── docs │ ├── changelog.md │ ├── community.md │ ├── guide │ │ ├── basic-usage.md │ │ ├── env.md │ │ ├── installation.md │ │ ├── load-priority.md │ │ └── options.md │ └── tutorial.md ├── package.json └── yarn.lock ├── examples ├── simple-async │ ├── README.md │ ├── index.js │ ├── package.json │ ├── simple.toml │ └── yarn.lock ├── simple │ ├── README.md │ ├── index.js │ ├── package.json │ ├── simple.toml │ └── yarn.lock ├── typescript-generics │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ ├── tsconfig.json │ ├── typescript-generics.yaml │ └── yarn.lock └── zero-config │ ├── README.md │ ├── index.js │ ├── package.json │ ├── yarn.lock │ └── zero-config.json5 ├── package.json ├── siroc.config.ts ├── src ├── defaults.ts ├── env.ts ├── index.ts ├── load.ts ├── merge.ts ├── options.ts └── parsers │ ├── index.ts │ ├── ini.ts │ ├── json.ts │ ├── json5.ts │ ├── toml.ts │ ├── util.ts │ └── yaml.ts ├── test ├── .config │ └── config-example.toml ├── cottonmouth.toml ├── defaults.test.ts ├── env.test.ts ├── example.toml ├── example.yml ├── index.test.ts ├── load.test.ts ├── merge.test.ts └── parsers │ ├── ini.test.ts │ ├── json.test.ts │ ├── json5.test.ts │ ├── toml.test.ts │ └── yaml.test.ts ├── tsconfig.json └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | executors: 4 | linux: 5 | docker: 6 | - image: cimg/base:stable 7 | 8 | orbs: 9 | codecov: codecov/codecov@1.2.5 10 | node: circleci/node@4.5.1 11 | 12 | jobs: 13 | test: 14 | parameters: 15 | node-version: 16 | type: string 17 | executor: linux 18 | steps: 19 | - checkout 20 | - node/install: 21 | node-version: << parameters.node-version >> 22 | install-yarn: true 23 | - restore_cache: 24 | name: Restore Yarn Package Cache 25 | keys: 26 | - yarn-packages-{{ checksum "yarn.lock" }} 27 | - run: 28 | name: Install Dependencies 29 | command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn 30 | - save_cache: 31 | name: Save Yarn Package Cache 32 | key: yarn-packages-{{ checksum "yarn.lock" }} 33 | paths: 34 | - ~/.cache/yarn 35 | - run: 36 | name: Install JUnit coverage reporter 37 | command: yarn add --dev jest-junit 38 | - run: 39 | name: Run Tests 40 | command: yarn test --ci --runInBand --reporters=default --reporters=jest-junit 41 | environment: 42 | JEST_JUNIT_OUTPUT_DIR: ./reports/junit/ 43 | - codecov/upload: 44 | file: ./coverage/clover.xml 45 | - store_test_results: 46 | path: ./reports/junit/ 47 | - store_artifacts: 48 | path: ./reports/junit 49 | 50 | workflows: 51 | tests: 52 | jobs: 53 | - test: 54 | matrix: 55 | parameters: 56 | node-version: ["14.17.3", "16.5.0"] 57 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "plugin:@typescript-eslint/recommended", 5 | "plugin:prettier/recommended" 6 | ], 7 | "rules": {}, 8 | "settings": { 9 | "import/resolver": { 10 | "node": { 11 | "extensions": [ ".js", ".jsx", ".ts", ".tsx" ], 12 | "paths": [ "src" ] 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "printWidth": 120, 6 | "tabWidth": 2 7 | } -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "@release-it/conventional-changelog": { 4 | "preset": { 5 | "name": "conventionalcommits", 6 | "types": [ 7 | { 8 | "type": "feat", 9 | "section": "Features" 10 | }, 11 | { 12 | "type": "fix", 13 | "section": "Bug Fixes" 14 | }, 15 | { 16 | "type": "docs", 17 | "section": "Documentation" 18 | }, 19 | { 20 | "type": "build", 21 | "section": "Build System/Dependencies" 22 | } 23 | ] 24 | }, 25 | "infile": "CHANGELOG.md" 26 | } 27 | }, 28 | "git": { 29 | "commitMessage": "chore: release v${version}", 30 | "tagName": "v${version}", 31 | "tagAnnotation": "v${version}" 32 | }, 33 | "github": { 34 | "release": true, 35 | "releaseName": "v${version}" 36 | } 37 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.4.0](https://github.com/lukecarr/c9h/compare/v0.3.0...v0.4.0) (2021-07-26) 2 | 3 | 4 | ### Features 5 | 6 | * **docs:** added google-analytics ([9019c70](https://github.com/lukecarr/c9h/commit/9019c70cc7bc135c102ca4a946eaa5ebc4980317)) 7 | * **docs:** added vuepress documentation site ([d37a022](https://github.com/lukecarr/c9h/commit/d37a02289efba7906cc816f6f279db977e821c4e)) 8 | * **options:** added `.config` dir to default paths ([e20ff0c](https://github.com/lukecarr/c9h/commit/e20ff0cac2adaa6467e990ede75c11bda14542c5)) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * **docs:** broken tutorial link on homepage ([04ff3b6](https://github.com/lukecarr/c9h/commit/04ff3b67fcf9abfdd83e3c37391db0cb3c2c2222)) 14 | * **ga:** removed redundant analytics deps ([c228d32](https://github.com/lukecarr/c9h/commit/c228d32ecab9a92cc556b5fd2bd7ab088e749851)) 15 | 16 | 17 | ### Documentation 18 | 19 | * **changelog:** added changelog to vuepress ([b9ad15b](https://github.com/lukecarr/c9h/commit/b9ad15be064a411f59ccf98047e9d2179aee8d7e)) 20 | * **footer:** added vuepress notice to footer ([dd3f7e1](https://github.com/lukecarr/c9h/commit/dd3f7e111707293589e58a5905d72f04fc3669ca)) 21 | * **nav:** added 5 min tutorial to nav ([12c1f29](https://github.com/lukecarr/c9h/commit/12c1f29eff6e6187e6b2e3529f7d3d45c455eaa2)) 22 | * **readme:** added used by section ([b78e834](https://github.com/lukecarr/c9h/commit/b78e8342ae435aa0ea2bc6c46bd8bf257a676c9f)) 23 | 24 | 25 | ### Build System/Dependencies 26 | 27 | * **deps:** replaced yaml with js-yaml ([fb1ea48](https://github.com/lukecarr/c9h/commit/fb1ea4831af3f4d547bc8cd2818af64069e56c94)) 28 | 29 | ## [0.3.0](https://github.com/lukecarr/c9h/compare/v0.2.1...v0.3.0) (2021-07-25) 30 | 31 | ### Features 32 | 33 | - added better support for generics ([cc04634](https://github.com/lukecarr/c9h/commit/cc046344d229363a42fe3ebac20fbfa32c81e5ea)) 34 | - **async:** added async load function ([ec356aa](https://github.com/lukecarr/c9h/commit/ec356aaec22d3990cee0708c8cde6fcf6583dda0)) 35 | - **example:** added typescript + generics example ([f9759eb](https://github.com/lukecarr/c9h/commit/f9759eb52b09682ba72aff8f2d0139f1e4bc2ef7)) 36 | - **example:** added zero-config example code ([08e9b08](https://github.com/lukecarr/c9h/commit/08e9b0841219f86688d3ce114b3b3a42c2fcaf25)) 37 | - **examples:** added simple async example ([d1ee2af](https://github.com/lukecarr/c9h/commit/d1ee2afae00ccdd26149ab434431261b5666a523)) 38 | - **options:** added option to change behaviour of multiple files ([cbaf80d](https://github.com/lukecarr/c9h/commit/cbaf80d0a06207e6f1df4d8a4757c2746de9197e)) 39 | 40 | ### Bug Fixes 41 | 42 | - **eslint:** fixed eslint errors ([0287afe](https://github.com/lukecarr/c9h/commit/0287afe572034b779314c6cbf3af1cf6d0787d9c)) 43 | - **examples:** added missing @types/node dep ([6bfb535](https://github.com/lukecarr/c9h/commit/6bfb535ea7055376f8e209bc27cb5818b40b1ca8)) 44 | - **tests:** fixed typo in load tests ([55b134a](https://github.com/lukecarr/c9h/commit/55b134a774447732f1e056ba3c071d07d79880e5)) 45 | 46 | ## [0.2.0](https://github.com/lukecarr/c9h/compare/v0.2.1...v0.2.1) (2021-07-20) 47 | 48 | ### Features 49 | 50 | - **options:** added filename as separate option ([4b8302a](https://github.com/lukecarr/c9h/commit/4b8302a6a53a76f0e37798b7bbffa68f33533eb1)) 51 | - **options:** added support for dynamic name ([7bad0c9](https://github.com/lukecarr/c9h/commit/7bad0c96f171bd522d61a30330ee5d3c16568eab)) 52 | 53 | ### Bug Fixes 54 | 55 | - **circleci:** typo in circleci config.yml ([1143a3a](https://github.com/lukecarr/c9h/commit/1143a3af885875c2911d22beb23bbe37cfdea697)) 56 | 57 | ## [0.1.1](https://github.com/lukecarr/c9h/compare/v0.2.1...v0.2.1) (2021-07-19) 58 | 59 | ### Bug Fixes 60 | 61 | - **env:** fixed error with special chars in prefix ([f1700ed](https://github.com/lukecarr/c9h/commit/f1700ed791001e7f166511f22ff711af2ddfa405)) 62 | - **env:** fixed issue with replacing prefix in env vars ([db59578](https://github.com/lukecarr/c9h/commit/db595780e2e08a504e39f0fba5880d94aad85178)) 63 | 64 | ## [0.1.0](https://github.com/lukecarr/c9h/compare/v0.2.1...v0.2.1) (2021-07-19) 65 | 66 | ### Features 67 | 68 | - **example:** added example folder ([e4eae3f](https://github.com/lukecarr/c9h/commit/e4eae3fb9bebb601d1aebbd7a5fbb8ce459c275d)) 69 | - initial package creation ([9a131dd](https://github.com/lukecarr/c9h/commit/9a131dd64202b4b2efc53ec18d3cb907a0b2e5e8)) 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Luke Carr 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![cottonmouth](https://user-images.githubusercontent.com/24438483/126182599-b04d8b13-6786-45e7-80be-3cdbb8086a05.png) 2 | 3 | # 🐍 cottonmouth 4 | 5 | ![npm](https://img.shields.io/npm/v/c9h) 6 | ![Codecov](https://img.shields.io/codecov/c/gh/lukecarr/c9h) 7 | ![CircleCI](https://img.shields.io/circleci/build/gh/lukecarr/c9h) 8 | ![Code Climate maintainability](https://img.shields.io/codeclimate/maintainability/lukecarr/c9h) 9 | ![npms.io (quality)](https://img.shields.io/npms-io/final-score/c9h?label=npms.io%20score) 10 | ![Snyk Vulnerabilities for npm package](https://img.shields.io/snyk/vulnerabilities/npm/c9h) 11 | ![npm bundle size](https://img.shields.io/bundlephobia/minzip/c9h) 12 | 13 | - 📁 **One library, many formats.** JSON, JSON5, INI, YAML, and TOML are all supported out-of-the-box as file formats! 14 | - 💻 **Environment variables.** Handle environment variables as a source of configuration with no effort! 15 | - 💯 **Zero configuration.** Cottonmouth works out-of-the-box using sensible defaults with no configuration required! 16 | - 💪 **Typescript.** Fully typed and self-documenting. 17 | - 🛠 **Extensible.** Bring your own file format parsers if we don't support your configuration files natively! 18 | 19 | > **Cottonmouth is still in development, but most desired functionality is present and breaking changes are unlikely.** 20 | 21 | ## Documentation 22 | 23 | You can find cottonmouth's documentation at [https://c9h.carr.sh/](https://c9h.carr.sh/). 24 | 25 | ### 5 Min Tutorial 26 | 27 | On cottonmouth's docs site, we've created a [5 Min Tutorial](https://c9h.carr.sh/docs/tutorial.html) to get you up and running in minutes! 28 | 29 | ## Related packages 30 | 31 | You can find a list of "community" packages related to cottonmouth on the [official docs site](https://c9h.carr.sh/docs/community.html)! 32 | 33 | ## Used by 34 | 35 | > Is your company/community using cottonmouth in production? Open an issue and get your logo added below! 36 | 37 | ## License 38 | 39 | Cottonmouth is licensed under the [`MIT License`](LICENSE). 40 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | The following table describes the versions of `cottonmouth` that are currently supported with security updates. 6 | 7 | | Version | Supported | 8 | | :-----: | :-------: | 9 | | 0.1.0 | ❌ | 10 | | 0.1.1 | ❌ | 11 | | 0.2.0 | ✅ | 12 | 13 | ## Responsible Disclosure Security Policy 14 | 15 | A responsible disclosure policy helps protect our users from publicly disclosed security vulnerabilities without a fix by employing a process where vulnerabilities are first triaged in a private manner, and only publicly disclosed after a reasonable time period that allows patching the vulnerability and providing an upgrade path for users. 16 | 17 | When contacting us directly, we will do our best efforts to respond in a reasonable time to resolve the issue. 18 | 19 | We kindly ask for you to refrain from malicious acts that put our users, any contributors, or the project iself at risk. 20 | 21 | ## Reporting a Vulnerability 22 | 23 | We consider the security of our systems a top priority. But no matter how much effort we put into system security, there can still be vulnerabilities present. 24 | 25 | If you discover a security vulnerability, please report the issue directly to me+oss@carr.sh. Do not use public communication methods (such as IRC, Discord, or Github issues/discussions). 26 | 27 | Your efforts to responsibly disclose your findings are sincerely appreciated and will be taken into account to acknowledge your contributions. 28 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # node modules 2 | node_modules 3 | 4 | # MacOS desktop services store 5 | .DS_Store 6 | 7 | # Log files 8 | *.log 9 | 10 | # Meta files 11 | TODOs.md 12 | 13 | # Editors 14 | .vscode 15 | .idea 16 | 17 | # VuePress temp directory 18 | .temp 19 | 20 | # Docs dist directory 21 | /vuepress 22 | 23 | # Typescript build dist 24 | packages/@vuepress/shared-utils/lib/ 25 | packages/@vuepress/shared-utils/types/ 26 | 27 | # Auto-generated changelog 28 | docs/changelog.md 29 | -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | const { description } = require('../../package') 2 | 3 | module.exports = { 4 | title: '🐍 Cottonmouth', 5 | description: description, 6 | 7 | head: [ 8 | ['meta', { name: 'theme-color', content: '#3eaf7c' }], 9 | ['meta', { name: 'apple-mobile-web-app-capable', content: 'yes' }], 10 | ['meta', { name: 'apple-mobile-web-app-status-bar-style', content: 'black' }] 11 | ], 12 | 13 | themeConfig: { 14 | repo: 'lukecarr/c9h', 15 | docsDir: 'docs', 16 | navbar: [ 17 | { 18 | text: '5 Min Tutorial', 19 | link: '/docs/tutorial.md', 20 | }, 21 | { 22 | text: 'Guide', 23 | link: '/docs/guide/installation.md', 24 | }, 25 | { 26 | text: 'Community', 27 | link: '/docs/community.md', 28 | }, 29 | { 30 | text: 'Changelog', 31 | link: '/docs/changelog.md', 32 | }, 33 | { 34 | text: 'NPM', 35 | link: 'https://npmjs.com/package/c9h', 36 | }, 37 | ], 38 | sidebar: [ 39 | '/docs/tutorial.md', 40 | { 41 | text: 'Guide', 42 | children: [ 43 | '/docs/guide/installation.md', 44 | '/docs/guide/basic-usage.md', 45 | '/docs/guide/options.md', 46 | '/docs/guide/env.md', 47 | '/docs/guide/load-priority.md', 48 | ], 49 | }, 50 | '/docs/community.md', 51 | '/docs/changelog.md', 52 | ], 53 | }, 54 | 55 | plugins: [ 56 | [ 57 | '@vuepress/plugin-google-analytics', 58 | { 59 | id: 'G-1KN79DNFR5', 60 | }, 61 | ] 62 | ], 63 | } 64 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | tagline: Zero-config config for Node.js! 4 | actions: 5 | - text: ⏱ 5 Min Tutorial 6 | link: /docs/tutorial.md 7 | features: 8 | - title: ☝️ One library, 📁 many formats 9 | details: JSON, JSON5, INI, YAML, and TOML are all supported out-of-the-box as file formats! 10 | - title: 💻 Environment variables 11 | details: Automatically handle environment variables as a source of configuration with no effort! 12 | - title: 💯 Zero configuration 13 | details: 🐍 Cottonmouth works out-of-the-box using sensible defaults with no configuration required! 14 | - title: 💪 TypeScript 15 | details: Fully typed and self-documenting, with support for generics! 16 | - title: 🛠 Extensible 17 | details: Bring your own file format parsers if we don't support your configuration files natively! 18 | - title: 19 | details: 20 | footer: Made by Luke Carr with ❤️ (and VuePress) 21 | --- 22 | -------------------------------------------------------------------------------- /docs/docs/changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.4.0](https://github.com/lukecarr/c9h/compare/v0.3.0...v0.4.0) (2021-07-26) 4 | 5 | ### Features 6 | 7 | * **docs:** added google-analytics ([9019c70](https://github.com/lukecarr/c9h/commit/9019c70cc7bc135c102ca4a946eaa5ebc4980317)) 8 | * **docs:** added vuepress documentation site ([d37a022](https://github.com/lukecarr/c9h/commit/d37a02289efba7906cc816f6f279db977e821c4e)) 9 | * **options:** added `.config` dir to default paths ([e20ff0c](https://github.com/lukecarr/c9h/commit/e20ff0cac2adaa6467e990ede75c11bda14542c5)) 10 | 11 | ### Bug Fixes 12 | 13 | * **docs:** broken tutorial link on homepage ([04ff3b6](https://github.com/lukecarr/c9h/commit/04ff3b67fcf9abfdd83e3c37391db0cb3c2c2222)) 14 | * **ga:** removed redundant analytics deps ([c228d32](https://github.com/lukecarr/c9h/commit/c228d32ecab9a92cc556b5fd2bd7ab088e749851)) 15 | 16 | ### Documentation 17 | 18 | * **changelog:** added changelog to vuepress ([b9ad15b](https://github.com/lukecarr/c9h/commit/b9ad15be064a411f59ccf98047e9d2179aee8d7e)) 19 | * **footer:** added vuepress notice to footer ([dd3f7e1](https://github.com/lukecarr/c9h/commit/dd3f7e111707293589e58a5905d72f04fc3669ca)) 20 | * **nav:** added 5 min tutorial to nav ([12c1f29](https://github.com/lukecarr/c9h/commit/12c1f29eff6e6187e6b2e3529f7d3d45c455eaa2)) 21 | * **readme:** added used by section ([b78e834](https://github.com/lukecarr/c9h/commit/b78e8342ae435aa0ea2bc6c46bd8bf257a676c9f)) 22 | 23 | ### Build System/Dependencies 24 | 25 | * **deps:** replaced yaml with js-yaml ([fb1ea48](https://github.com/lukecarr/c9h/commit/fb1ea4831af3f4d547bc8cd2818af64069e56c94)) 26 | 27 | ## [0.3.0](https://github.com/lukecarr/c9h/compare/v0.2.1...v0.3.0) (2021-07-25) 28 | 29 | ### Features 30 | 31 | * added better support for generics ([cc04634](https://github.com/lukecarr/c9h/commit/cc046344d229363a42fe3ebac20fbfa32c81e5ea)) 32 | * **async:** added async load function ([ec356aa](https://github.com/lukecarr/c9h/commit/ec356aaec22d3990cee0708c8cde6fcf6583dda0)) 33 | * **example:** added typescript + generics example ([f9759eb](https://github.com/lukecarr/c9h/commit/f9759eb52b09682ba72aff8f2d0139f1e4bc2ef7)) 34 | * **example:** added zero-config example code ([08e9b08](https://github.com/lukecarr/c9h/commit/08e9b0841219f86688d3ce114b3b3a42c2fcaf25)) 35 | * **examples:** added simple async example ([d1ee2af](https://github.com/lukecarr/c9h/commit/d1ee2afae00ccdd26149ab434431261b5666a523)) 36 | * **options:** added option to change behaviour of multiple files ([cbaf80d](https://github.com/lukecarr/c9h/commit/cbaf80d0a06207e6f1df4d8a4757c2746de9197e)) 37 | 38 | ### Bug Fixes 39 | 40 | * **eslint:** fixed eslint errors ([0287afe](https://github.com/lukecarr/c9h/commit/0287afe572034b779314c6cbf3af1cf6d0787d9c)) 41 | * **examples:** added missing @types/node dep ([6bfb535](https://github.com/lukecarr/c9h/commit/6bfb535ea7055376f8e209bc27cb5818b40b1ca8)) 42 | * **tests:** fixed typo in load tests ([55b134a](https://github.com/lukecarr/c9h/commit/55b134a774447732f1e056ba3c071d07d79880e5)) 43 | 44 | ## [0.2.0](https://github.com/lukecarr/c9h/compare/v0.2.1...v0.2.1) (2021-07-20) 45 | 46 | ### Features 47 | 48 | * **options:** added filename as separate option ([4b8302a](https://github.com/lukecarr/c9h/commit/4b8302a6a53a76f0e37798b7bbffa68f33533eb1)) 49 | * **options:** added support for dynamic name ([7bad0c9](https://github.com/lukecarr/c9h/commit/7bad0c96f171bd522d61a30330ee5d3c16568eab)) 50 | 51 | ### Bug Fixes 52 | 53 | * **circleci:** typo in circleci config.yml ([1143a3a](https://github.com/lukecarr/c9h/commit/1143a3af885875c2911d22beb23bbe37cfdea697)) 54 | 55 | ## [0.1.1](https://github.com/lukecarr/c9h/compare/v0.2.1...v0.2.1) (2021-07-19) 56 | 57 | ### Bug Fixes 58 | 59 | * **env:** fixed error with special chars in prefix ([f1700ed](https://github.com/lukecarr/c9h/commit/f1700ed791001e7f166511f22ff711af2ddfa405)) 60 | * **env:** fixed issue with replacing prefix in env vars ([db59578](https://github.com/lukecarr/c9h/commit/db595780e2e08a504e39f0fba5880d94aad85178)) 61 | 62 | ## [0.1.0](https://github.com/lukecarr/c9h/compare/v0.2.1...v0.2.1) (2021-07-19) 63 | 64 | ### Features 65 | 66 | * **example:** added example folder ([e4eae3f](https://github.com/lukecarr/c9h/commit/e4eae3fb9bebb601d1aebbd7a5fbb8ce459c275d)) 67 | * initial package creation ([9a131dd](https://github.com/lukecarr/c9h/commit/9a131dd64202b4b2efc53ec18d3cb907a0b2e5e8)) 68 | -------------------------------------------------------------------------------- /docs/docs/community.md: -------------------------------------------------------------------------------- 1 | # Community 2 | 3 | ## 🌐 fastify-c9h 4 | 5 | [fastify-c9h](https://github.com/lukecarr/fastify-c9h) is a Fastify plugin which decorates the Fastify instance with a `c9h` property for accessing your application's configuration values. 6 | 7 | ```ts 8 | import fastify from 'fastify'; 9 | import c9h from 'fastify-c9h'; 10 | 11 | const server = fastify({ logger: true }); 12 | 13 | server.register(c9h, { 14 | /* c9h options */ 15 | }); 16 | 17 | server.get('/', async function () { 18 | return { hello: this.c9h.your.config.variable }; 19 | }); 20 | 21 | // ... 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/docs/guide/basic-usage.md: -------------------------------------------------------------------------------- 1 | # Basic Usage 2 | 3 | You can find some basic usage of cottonmouth below. For a full e2e tutorial, take a look at our [5 Min Tutorial](/docs/tutorial/)! 4 | 5 | ```js 6 | const config = require('c9h')({ 7 | /* options */ 8 | }); 9 | // OR 10 | import c9h from 'c9h'; 11 | const config = c9h({ 12 | /* options */ 13 | }); 14 | ``` 15 | 16 | This will look for `your-package.{toml,yaml,yml,json,json5,ini}` in `./` (the current working directory), `/etc/your-package/`, and `$HOME/.your-package/`, and store the parsed config in the `config` variable. 17 | -------------------------------------------------------------------------------- /docs/docs/guide/env.md: -------------------------------------------------------------------------------- 1 | # Environment Variables 2 | 3 | Cottonmouth has out-of-the-box support for environment variables. 4 | 5 | Environment variables should be prefixed with your project's name (`options.name`) in uppercase and a trailing `_` (underscore) character. 6 | 7 | ::: warning 8 | All environment variables are parsed by cottonmouth as strings, even if they have numeric or boolean values. You will need to perform any value parsing in your application logic. 9 | ::: 10 | 11 | ## Example 12 | 13 | Given that your project's name is `c9h`, these environment variables: 14 | 15 | ```bash 16 | C9H_VERBOSE="yes" 17 | C9H_HTTP_ADDR="0.0.0.0" 18 | HTTP_PORT=3000 19 | ``` 20 | 21 | would be parsed into the this configuration object: 22 | 23 | ```json 24 | { 25 | "verbose": "yes", 26 | "http": { 27 | "addr": "0.0.0.0" 28 | } 29 | } 30 | ``` 31 | 32 | ::: warning 33 | It's important to note that the `HTTP_PORT` environment variable in the above example was not loaded by cottonmouth. This is because the variable wasn't prefixed by the `C9H_` (the project's uppercase name with a trailing underscore). 34 | ::: 35 | -------------------------------------------------------------------------------- /docs/docs/guide/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | Cottonmouth is available on NPM under the package name `c9h`. You can add it to your project like so: 4 | 5 | ```bash 6 | npm install c9h 7 | # OR 8 | yarn add c9h 9 | ``` 10 | -------------------------------------------------------------------------------- /docs/docs/guide/load-priority.md: -------------------------------------------------------------------------------- 1 | # Load Priority 2 | 3 | When cottonmouth loads your configuration, it assumes the following priority order for configuration values: 4 | 5 | 1. Default values specified in `options.defaults` (LOWEST) 6 | 2. Values parsed from a configuration file 7 | 3. Environment variables (HIGHEST) 8 | -------------------------------------------------------------------------------- /docs/docs/guide/options.md: -------------------------------------------------------------------------------- 1 | # Options 2 | 3 | Although cottonmouth is advertised as a "zero-config config", it's actually an extremely extensible package. 4 | 5 | When invoking `c9h()`, you can optionally provide an object contains various settings to configure how cottonmouth functions. The default values are included below: 6 | 7 | ```js 8 | const options = { 9 | name: process.env.npm_package_name || path.parse(process.cwd()).name, 10 | filename: options.name, 11 | defaults: {}, 12 | parsers: ['json', 'json5', 'toml', 'yaml', 'ini'], 13 | paths: [(name) => `${process.env.HOME}/.${name}`, process.cwd, (name) => `/etc/${name}`], 14 | mergeArray: true, 15 | mergeFiles: 'first', 16 | }; 17 | ``` 18 | 19 | ## `name` 20 | 21 | This is the name used to generate the directories (`options.paths`) where `cottonmouth` looks for your configuration file. 22 | 23 | This can be a string, or a function that returns a string (useful for scenarios when a dynamic name is desired, such as including the `NODE_ENV` env var value). 24 | 25 | By default, this is automatically generated as the name provided in your project's `package.json` file, or the name of your project's working directory. 26 | 27 | ## `filename` 28 | 29 | This is the name of your configuration file (excluding the extension). 30 | 31 | This can be a string, or a function that returns a string. 32 | 33 | By default, this is set to the value of the `name` option, which defaults to your project's `package.json` name property, or the name of your project's working directory. 34 | 35 | ## `defaults` 36 | 37 | This is the object containing any default values for your configuration. See [Value Priority](#value-priority) for guidance on the priority of configuration values from different sources. 38 | 39 | By default, there are no default configuration values. 40 | 41 | ## `parsers` 42 | 43 | This is an array of configuration file parsers that you'd like cottonmouth to use. Removing a parser from this array will allow you to ignore a configuration file in a specific file format (even if it exists). 44 | 45 | Each parser implements the `Parser` interface: 46 | 47 | ```ts 48 | export interface Parser { 49 | /** 50 | * Returns the file extensions that the parser supports. 51 | */ 52 | extensions(): string[]; 53 | /** 54 | * Attempts to parse a file contents, and returns the parsed 55 | * content. 56 | * 57 | * @param file The raw string contents of the file to parse. 58 | */ 59 | parse(file: string): Record; 60 | } 61 | ``` 62 | 63 | By default, all parsers (`[json, json5, toml, yaml, ini]`) are enabled. 64 | 65 | ::: tip 66 | **You can write your own parsers that implement the above interface, and provide them in this option! This way you can parse custom configuration file formats that aren't natively supported by cottonmouth!** 67 | ::: 68 | 69 | ## `paths` 70 | 71 | This is an array of functions that represent the different directories cottonmouth should look for your configuration file in. Each function should return a string which is the directory path, and the `name` option is provided to the function as an argument to allow for dynamic directories based on your project name. 72 | 73 | By default, the current working directory (`process.cwd`), the etc directory (`/etc/${name}`), and a hidden directory in your user's HOME directory (`$HOME/.${name}`). 74 | 75 | For example, if your project's name is `c9h` and your current working directory is `/var/www`, `cottonmouth` will look for your configuration file in these three places by default: `/var/www`, `/etc/c9h`, and `/home/lukecarr/.c9h`. 76 | 77 | ## `mergeArray` 78 | 79 | This dictates how arrays are handled in configuration files. If set to `true`, arrays in configuration files will be merged with the default value, rather than replaced. 80 | 81 | By default, this option is set to `true`. 82 | 83 | ## `mergeFiles` 84 | 85 | This dictates how cottonmouth handles scenarios where multiple configuration files are found. 86 | 87 | This can be one of: 88 | 89 | - `'merge'`: This indicates that c9h should merge multiple files if found, using the same method to merge files with default values. 90 | - `'error'`: This indicates that c9h should throw an error if multiple files are found. This mode is good if you're expecting to only have one file, and what c9h to flag when this isn't the case. 91 | - `'first'`: This indicates that c9h should use the first file that it finds, and silently ignore all others. 92 | 93 | By default, this option is set to `'first'`. 94 | 95 | ## Real-world example 96 | 97 | Given the following options: 98 | 99 | ```js 100 | const options = { 101 | name: 'c9h', 102 | filename: () => `config-${process.env.NODE_ENV}`, 103 | defaults: {}, 104 | parsers: ['json', 'json5', 'toml', 'yaml', 'ini'], 105 | paths: [(name) => `${process.env.HOME}/.${name}`, process.cwd, (name) => `/etc/${name}`], 106 | mergeArray: true, 107 | mergeFiles: 'merge', 108 | }; 109 | ``` 110 | 111 | Cottonmouth will look in the following directories: 112 | 113 | - `/home/lukecarr/.c9h` 114 | - `/your/current/directory` 115 | - `/etc/c9h` 116 | 117 | In each directory, cottonmouth will look for these files (where `$NODE_ENV` is replaced at runtime with the value of the `NODE_ENV` environment variable): 118 | 119 | - `config-$NODE_ENV.json` 120 | - `config-$NODE_ENV.json5` 121 | - `config-$NODE_ENV.toml` 122 | - `config-$NODE_ENV.yaml` 123 | - `config-$NODE_ENV.ini` 124 | 125 | If multiple configuration files are found, cottonmouth will merge their parsed contents together (because `options.mergeFiles == 'merge'`). 126 | -------------------------------------------------------------------------------- /docs/docs/tutorial.md: -------------------------------------------------------------------------------- 1 | # 5 Min Tutorial 2 | 3 | This five minute tutorial will get your Node.js project setup with 🐍 cottonmouth for configuration management! 4 | 5 | ## Project setup 6 | 7 | Let's setup a really basic Node.js project. If you already have a project setup with a `package.json` file, skip to [Installing](#installing). 8 | 9 | ```bash 10 | # Let's install Yarn (if you haven't already!) 11 | npm install -g yarn 12 | 13 | # Initialise the project in the current directory 14 | yarn init -y 15 | ``` 16 | 17 | You should now how have a directory that contains a `package.json` file. We can now install cottonmouth and start using it! 18 | 19 | ## Installing 20 | 21 | Installing cottonmouth is simple and takes just one command: 22 | 23 | ``` 24 | yarn add c9h 25 | ``` 26 | 27 | If you're just using NPM (and not Yarn), try this: 28 | 29 | ``` 30 | npm install c9h 31 | ``` 32 | 33 | ## Using 🐍 cottonmouth 34 | 35 | Now let's get started with cottonmouth! Create a file called `index.js` in your project's directory and paste in the following code (we'll go over it in a short while): 36 | 37 | ```js 38 | const config = require('c9h')(); 39 | console.log(JSON.stringify(config)); 40 | ``` 41 | 42 | On the first line, we are importing and invoking cottonmouth. We then store the return value (your parsed config, which is empty right now!) in `config`. 43 | 44 | On the next line, we just log the value of `config` to the console. 45 | 46 | ## Running the program 47 | 48 | We can now run the program and see if cottonmouth is working! 49 | 50 | Add a script to your project's `package.json` which runs `node index.js`. Your `package.json` should look similar to this (we've chosen `start` as the script name, but you can choose whatever you want!): 51 | 52 | ```json 53 | { 54 | "name": "your-directory", 55 | "scripts": { 56 | "start": "node index.js" 57 | }, 58 | "dependencies": { 59 | "c9h": "^0.3.0" 60 | } 61 | } 62 | ``` 63 | 64 | We can now run the program like so: 65 | 66 | ``` 67 | yarn start 68 | ``` 69 | 70 | If you're just using NPM (and not Yarn), try this: 71 | 72 | ``` 73 | npm run start 74 | ``` 75 | 76 | ## Creating a configuration file 77 | 78 | Now we can create a configuration file in your project's directory. Create a file with the same name as the name found in your `package.json` file. For the file format, it's up to you to choose your weapon of choice! 79 | 80 | ::: tip 81 | 🐍 cottonmouth supports JSON, JSON5, TOML, YAML, and INI configuration files out-of-the-box! 82 | ::: 83 | 84 | In our example, we've created a file called `c9h-test.yaml` which contains some YAML data: 85 | 86 | ```yaml 87 | port: 3000 88 | addr: '0.0.0.0' 89 | ``` 90 | 91 | Give your program a run again, and observe how cottonmouth has now loaded your newly created configuration file's data: 92 | 93 | ```bash 94 | yarn start 95 | # => {"port": 3000, "addr": "0.0.0.0"} 96 | ``` 97 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@c9h/docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "repository": "lukecarr/c9h/docs", 6 | "scripts": { 7 | "changelog": "(echo \"# Changelog\\n\"; cat ../CHANGELOG.md) > docs/changelog.md", 8 | "dev": "yarn changelog && vuepress dev", 9 | "build": "yarn changelog && vuepress build" 10 | }, 11 | "devDependencies": { 12 | "vuepress": "^2.0.0-beta.22", 13 | "@vuepress/plugin-google-analytics": "^2.0.0-beta.22" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/simple-async/README.md: -------------------------------------------------------------------------------- 1 | # Simple Example (Asynchronous) 2 | 3 | > This example is the **asynchronous** variation of the `/examples/simple` example. 4 | 5 | This is a very simple example of `cottonmouth` that includes the parsing of a TOML configuration file, with a default configuration provided, and an environment variable used to override a value. 6 | 7 | The configuration file is named `simple.toml` because `simple` is the name defined in this directory's `package.json` file. 8 | 9 | This name is also used as the prefix for environment variables (`SIMPLE_`). 10 | 11 | ## Running the example 12 | 13 | You can use this example yourself by installing the dependencies (just `c9h`) and then running the `start` script: 14 | 15 | ```bash 16 | npm ci && npm run start 17 | # OR 18 | yarn && yarn start 19 | ``` 20 | 21 | You should see the following output: 22 | 23 | ``` 24 | {"server":{"port":8080,"host":"0.0.0.0"}} 25 | ``` 26 | -------------------------------------------------------------------------------- /examples/simple-async/index.js: -------------------------------------------------------------------------------- 1 | // In reality, you wouldn't declare the env var this way 2 | // We are modifying process.env directly for demonstration purposes 3 | process.env.SIMPLE_SERVER_HOST = '0.0.0.0' 4 | 5 | const { load } = require('c9h') 6 | 7 | ;(async () => { 8 | const config = await load({ 9 | defaults: { 10 | server: { 11 | // This will be overriden by the TOML file 12 | port: 3000, 13 | // This will be overriden by the `SIMPLE_SERVER_HOST` env var 14 | host: '127.0.0.1', 15 | }, 16 | }, 17 | }) 18 | 19 | console.log(JSON.stringify(config)) 20 | // => {"server":{"port":8080,"host":"0.0.0.0"}} 21 | })() 22 | -------------------------------------------------------------------------------- /examples/simple-async/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node index.js" 7 | }, 8 | "dependencies": { 9 | "c9h": "../.." 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/simple-async/simple.toml: -------------------------------------------------------------------------------- 1 | [server] 2 | port = 8080 3 | -------------------------------------------------------------------------------- /examples/simple-async/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@iarna/toml@^3.0.0": 6 | version "3.0.0" 7 | resolved "https://registry.yarnpkg.com/@iarna/toml/-/toml-3.0.0.tgz#ccde5292fe9d348bbe93fe90d579fc442b72b0b3" 8 | integrity sha512-td6ZUkz2oS3VeleBcN+m//Q6HlCFCPrnI0FZhrt/h4XqLEdOyYp2u21nd8MdsR+WJy5r9PTDaHTDDfhf4H4l6Q== 9 | 10 | c9h@../..: 11 | version "0.2.1" 12 | dependencies: 13 | "@iarna/toml" "^3.0.0" 14 | ini "^2.0.0" 15 | json5 "^2.2.0" 16 | yaml "^1.10.2" 17 | 18 | c9h@latest: 19 | version "0.2.1" 20 | resolved "https://registry.yarnpkg.com/c9h/-/c9h-0.2.1.tgz#0bc088d25c44955b866c6746f5184451e0eb06ed" 21 | integrity sha512-qaCMYqyKXkOKIQzfuHXAKEmRTtcE/ASeCxFt5NJqCeRT4+iuOzLlM6WV68uxB2dUhAi+OmRwzP6nR03+tUsngQ== 22 | dependencies: 23 | "@iarna/toml" "^3.0.0" 24 | ini "^2.0.0" 25 | json5 "^2.2.0" 26 | yaml "^1.10.2" 27 | 28 | ini@^2.0.0: 29 | version "2.0.0" 30 | resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" 31 | integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== 32 | 33 | json5@^2.2.0: 34 | version "2.2.0" 35 | resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" 36 | integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA== 37 | dependencies: 38 | minimist "^1.2.5" 39 | 40 | minimist@^1.2.5: 41 | version "1.2.5" 42 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" 43 | integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== 44 | 45 | yaml@^1.10.2: 46 | version "1.10.2" 47 | resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" 48 | integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== 49 | -------------------------------------------------------------------------------- /examples/simple/README.md: -------------------------------------------------------------------------------- 1 | # Simple Example 2 | 3 | This is a very simple example of `cottonmouth` that includes the parsing of a TOML configuration file, with a default configuration provided, and an environment variable used to override a value. 4 | 5 | The configuration file is named `simple.toml` because `simple` is the name defined in this directory's `package.json` file. 6 | 7 | This name is also used as the prefix for environment variables (`SIMPLE_`). 8 | 9 | ## Running the example 10 | 11 | You can use this example yourself by installing the dependencies (just `c9h`) and then running the `start` script: 12 | 13 | ```bash 14 | npm ci && npm run start 15 | # OR 16 | yarn && yarn start 17 | ``` 18 | 19 | You should see the following output: 20 | 21 | ``` 22 | {"server":{"port":8080,"host":"0.0.0.0"}} 23 | ``` 24 | -------------------------------------------------------------------------------- /examples/simple/index.js: -------------------------------------------------------------------------------- 1 | // In reality, you wouldn't declare the env var this way 2 | // We are modifying process.env directly for demonstration purposes 3 | process.env.SIMPLE_SERVER_HOST = '0.0.0.0' 4 | 5 | const config = require('c9h')({ 6 | defaults: { 7 | server: { 8 | // This will be overriden by the TOML file 9 | port: 3000, 10 | // This will be overriden by the `SIMPLE_SERVER_HOST` env var 11 | host: '127.0.0.1', 12 | }, 13 | }, 14 | }) 15 | 16 | console.log(JSON.stringify(config)) 17 | // => {"server":{"port":8080,"host":"0.0.0.0"}} 18 | -------------------------------------------------------------------------------- /examples/simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node index.js" 7 | }, 8 | "dependencies": { 9 | "c9h": "../.." 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/simple/simple.toml: -------------------------------------------------------------------------------- 1 | [server] 2 | port = 8080 3 | -------------------------------------------------------------------------------- /examples/simple/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@iarna/toml@^3.0.0": 6 | version "3.0.0" 7 | resolved "https://registry.yarnpkg.com/@iarna/toml/-/toml-3.0.0.tgz#ccde5292fe9d348bbe93fe90d579fc442b72b0b3" 8 | integrity sha512-td6ZUkz2oS3VeleBcN+m//Q6HlCFCPrnI0FZhrt/h4XqLEdOyYp2u21nd8MdsR+WJy5r9PTDaHTDDfhf4H4l6Q== 9 | 10 | c9h@^0.2.1: 11 | version "0.2.1" 12 | resolved "https://registry.yarnpkg.com/c9h/-/c9h-0.2.1.tgz#0bc088d25c44955b866c6746f5184451e0eb06ed" 13 | integrity sha512-qaCMYqyKXkOKIQzfuHXAKEmRTtcE/ASeCxFt5NJqCeRT4+iuOzLlM6WV68uxB2dUhAi+OmRwzP6nR03+tUsngQ== 14 | dependencies: 15 | "@iarna/toml" "^3.0.0" 16 | ini "^2.0.0" 17 | json5 "^2.2.0" 18 | yaml "^1.10.2" 19 | 20 | ini@^2.0.0: 21 | version "2.0.0" 22 | resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" 23 | integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== 24 | 25 | json5@^2.2.0: 26 | version "2.2.0" 27 | resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" 28 | integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA== 29 | dependencies: 30 | minimist "^1.2.5" 31 | 32 | minimist@^1.2.5: 33 | version "1.2.5" 34 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" 35 | integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== 36 | 37 | yaml@^1.10.2: 38 | version "1.10.2" 39 | resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" 40 | integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== 41 | -------------------------------------------------------------------------------- /examples/typescript-generics/README.md: -------------------------------------------------------------------------------- 1 | # Zero Config Example 2 | 3 | This is an example of `cottonmouth` that demonstrates the library's zero-config capabilities. 4 | 5 | The configuration file is named `zero-config.json5` because `zero-config` is the name defined in this directory's `package.json` file, and this is where `cottonmouth` will look by default. 6 | 7 | This name is also used as the prefix for environment variables (`ZERO_CONFIG_`). 8 | 9 | ## Running the example 10 | 11 | You can use this example yourself by installing the dependencies (just `c9h`) and then running the `start` script: 12 | 13 | ```bash 14 | npm ci && npm run start 15 | # OR 16 | yarn && yarn start 17 | ``` 18 | 19 | You should see the following output: 20 | 21 | ``` 22 | {"http":{"https":true,"listen":{"addr":"0.0.0.0","port":3000}}} 23 | ``` 24 | -------------------------------------------------------------------------------- /examples/typescript-generics/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-generics", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "ts-node src/index.ts" 7 | }, 8 | "dependencies": { 9 | "c9h": "../.." 10 | }, 11 | "devDependencies": { 12 | "@types/node": "^16.4.2", 13 | "ts-node": "^10.1.0", 14 | "typescript": "^4.3.5" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/typescript-generics/src/index.ts: -------------------------------------------------------------------------------- 1 | import c9h from 'c9h' 2 | 3 | type Config = { 4 | hello: { 5 | there: string 6 | } 7 | } 8 | 9 | const config = c9h() 10 | 11 | console.log(config.hello.there) 12 | // => world 13 | -------------------------------------------------------------------------------- /examples/typescript-generics/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "ES2017", 5 | "module": "commonjs", 6 | "moduleResolution": "Node", 7 | "esModuleInterop": true, 8 | "strict": true, 9 | "declaration": true, 10 | "types": ["node"] 11 | }, 12 | "include": ["src"] 13 | } 14 | -------------------------------------------------------------------------------- /examples/typescript-generics/typescript-generics.yaml: -------------------------------------------------------------------------------- 1 | hello: 2 | there: 'world' 3 | -------------------------------------------------------------------------------- /examples/typescript-generics/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@iarna/toml@^3.0.0": 6 | version "3.0.0" 7 | resolved "https://registry.yarnpkg.com/@iarna/toml/-/toml-3.0.0.tgz#ccde5292fe9d348bbe93fe90d579fc442b72b0b3" 8 | integrity sha512-td6ZUkz2oS3VeleBcN+m//Q6HlCFCPrnI0FZhrt/h4XqLEdOyYp2u21nd8MdsR+WJy5r9PTDaHTDDfhf4H4l6Q== 9 | 10 | "@tsconfig/node10@^1.0.7": 11 | version "1.0.8" 12 | resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9" 13 | integrity sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg== 14 | 15 | "@tsconfig/node12@^1.0.7": 16 | version "1.0.9" 17 | resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.9.tgz#62c1f6dee2ebd9aead80dc3afa56810e58e1a04c" 18 | integrity sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw== 19 | 20 | "@tsconfig/node14@^1.0.0": 21 | version "1.0.1" 22 | resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2" 23 | integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg== 24 | 25 | "@tsconfig/node16@^1.0.1": 26 | version "1.0.1" 27 | resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.1.tgz#a6ca6a9a0ff366af433f42f5f0e124794ff6b8f1" 28 | integrity sha512-FTgBI767POY/lKNDNbIzgAX6miIDBs6NTCbdlDb8TrWovHsSvaVIZDlTqym29C6UqhzwcJx4CYr+AlrMywA0cA== 29 | 30 | "@types/node@^16.4.2": 31 | version "16.4.2" 32 | resolved "https://registry.yarnpkg.com/@types/node/-/node-16.4.2.tgz#0a95d7fd950cb1eaca0ce11031d72e8f680b775a" 33 | integrity sha512-vxyhOzFCm+jC/T5KugbVsYy1DbQM0h3NCFUrVbu0+pYa/nr+heeucpqxpa8j4pUmIGLPYzboY9zIdOF0niFAjQ== 34 | 35 | arg@^4.1.0: 36 | version "4.1.3" 37 | resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" 38 | integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== 39 | 40 | buffer-from@^1.0.0: 41 | version "1.1.1" 42 | resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" 43 | integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== 44 | 45 | c9h@../..: 46 | version "0.2.1" 47 | dependencies: 48 | "@iarna/toml" "^3.0.0" 49 | ini "^2.0.0" 50 | json5 "^2.2.0" 51 | yaml "^1.10.2" 52 | 53 | create-require@^1.1.0: 54 | version "1.1.1" 55 | resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" 56 | integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== 57 | 58 | diff@^4.0.1: 59 | version "4.0.2" 60 | resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" 61 | integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== 62 | 63 | ini@^2.0.0: 64 | version "2.0.0" 65 | resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" 66 | integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== 67 | 68 | json5@^2.2.0: 69 | version "2.2.0" 70 | resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" 71 | integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA== 72 | dependencies: 73 | minimist "^1.2.5" 74 | 75 | make-error@^1.1.1: 76 | version "1.3.6" 77 | resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" 78 | integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== 79 | 80 | minimist@^1.2.5: 81 | version "1.2.5" 82 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" 83 | integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== 84 | 85 | source-map-support@^0.5.17: 86 | version "0.5.19" 87 | resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" 88 | integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== 89 | dependencies: 90 | buffer-from "^1.0.0" 91 | source-map "^0.6.0" 92 | 93 | source-map@^0.6.0: 94 | version "0.6.1" 95 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" 96 | integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== 97 | 98 | ts-node@^10.1.0: 99 | version "10.1.0" 100 | resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.1.0.tgz#e656d8ad3b61106938a867f69c39a8ba6efc966e" 101 | integrity sha512-6szn3+J9WyG2hE+5W8e0ruZrzyk1uFLYye6IGMBadnOzDh8aP7t8CbFpsfCiEx2+wMixAhjFt7lOZC4+l+WbEA== 102 | dependencies: 103 | "@tsconfig/node10" "^1.0.7" 104 | "@tsconfig/node12" "^1.0.7" 105 | "@tsconfig/node14" "^1.0.0" 106 | "@tsconfig/node16" "^1.0.1" 107 | arg "^4.1.0" 108 | create-require "^1.1.0" 109 | diff "^4.0.1" 110 | make-error "^1.1.1" 111 | source-map-support "^0.5.17" 112 | yn "3.1.1" 113 | 114 | typescript@^4.3.5: 115 | version "4.3.5" 116 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4" 117 | integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA== 118 | 119 | yaml@^1.10.2: 120 | version "1.10.2" 121 | resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" 122 | integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== 123 | 124 | yn@3.1.1: 125 | version "3.1.1" 126 | resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" 127 | integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== 128 | -------------------------------------------------------------------------------- /examples/zero-config/README.md: -------------------------------------------------------------------------------- 1 | # Zero Config Example 2 | 3 | This is an example of `cottonmouth` that demonstrates the library's zero-config capabilities. 4 | 5 | The configuration file is named `zero-config.json5` because `zero-config` is the name defined in this directory's `package.json` file, and this is where `cottonmouth` will look by default. 6 | 7 | This name is also used as the prefix for environment variables (`ZERO_CONFIG_`). 8 | 9 | ## Running the example 10 | 11 | You can use this example yourself by installing the dependencies (just `c9h`) and then running the `start` script: 12 | 13 | ```bash 14 | npm ci && npm run start 15 | # OR 16 | yarn && yarn start 17 | ``` 18 | 19 | You should see the following output: 20 | 21 | ``` 22 | {"http":{"https":true,"listen":{"addr":"0.0.0.0","port":3000}}} 23 | ``` 24 | -------------------------------------------------------------------------------- /examples/zero-config/index.js: -------------------------------------------------------------------------------- 1 | const config = require('c9h')() 2 | 3 | console.log(JSON.stringify(config)) 4 | // => {"http":{"https":true,"listen":{"addr":"0.0.0.0","port":3000}}} 5 | -------------------------------------------------------------------------------- /examples/zero-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zero-config", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node index.js" 7 | }, 8 | "dependencies": { 9 | "c9h": "../.." 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/zero-config/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@iarna/toml@^3.0.0": 6 | version "3.0.0" 7 | resolved "https://registry.yarnpkg.com/@iarna/toml/-/toml-3.0.0.tgz#ccde5292fe9d348bbe93fe90d579fc442b72b0b3" 8 | integrity sha512-td6ZUkz2oS3VeleBcN+m//Q6HlCFCPrnI0FZhrt/h4XqLEdOyYp2u21nd8MdsR+WJy5r9PTDaHTDDfhf4H4l6Q== 9 | 10 | c9h@^0.2.1: 11 | version "0.2.1" 12 | resolved "https://registry.yarnpkg.com/c9h/-/c9h-0.2.1.tgz#0bc088d25c44955b866c6746f5184451e0eb06ed" 13 | integrity sha512-qaCMYqyKXkOKIQzfuHXAKEmRTtcE/ASeCxFt5NJqCeRT4+iuOzLlM6WV68uxB2dUhAi+OmRwzP6nR03+tUsngQ== 14 | dependencies: 15 | "@iarna/toml" "^3.0.0" 16 | ini "^2.0.0" 17 | json5 "^2.2.0" 18 | yaml "^1.10.2" 19 | 20 | ini@^2.0.0: 21 | version "2.0.0" 22 | resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" 23 | integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== 24 | 25 | json5@^2.2.0: 26 | version "2.2.0" 27 | resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" 28 | integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA== 29 | dependencies: 30 | minimist "^1.2.5" 31 | 32 | minimist@^1.2.5: 33 | version "1.2.5" 34 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" 35 | integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== 36 | 37 | yaml@^1.10.2: 38 | version "1.10.2" 39 | resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" 40 | integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== 41 | -------------------------------------------------------------------------------- /examples/zero-config/zero-config.json5: -------------------------------------------------------------------------------- 1 | { 2 | http: { 3 | https: true, 4 | listen: { 5 | addr: '0.0.0.0', 6 | port: 3000, 7 | }, 8 | }, 9 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "c9h", 3 | "version": "0.4.0", 4 | "description": "Zero-config config for Node.js", 5 | "keywords": [ 6 | "config", 7 | "c9h", 8 | "environment", 9 | "variables", 10 | "env-var", 11 | "envvar", 12 | "configuration", 13 | "typescript" 14 | ], 15 | "homepage": "https://c9h.carr.sh/", 16 | "bugs": "https://github.com/lukecarr/c9h/issues", 17 | "repository": "lukecarr/c9h", 18 | "funding": { 19 | "url": "https://github.com/lukecarr/c9h#donations" 20 | }, 21 | "license": "MIT", 22 | "author": "Luke Carr (https://carr.sh/)", 23 | "exports": { 24 | ".": { 25 | "import": "./dist/index.mjs", 26 | "require": "./dist/index.cjs" 27 | } 28 | }, 29 | "main": "dist/index.cjs", 30 | "module": "dist/index.mjs", 31 | "types": "dist/index.d.ts", 32 | "files": [ 33 | "dist" 34 | ], 35 | "scripts": { 36 | "build": "siroc build", 37 | "test": "jest", 38 | "release": "yarn test && yarn build && npx release-it", 39 | "lint:eslint": "eslint \"src/**/*.ts\" \"test/**/*.ts\" --fix", 40 | "lint:prettier": "prettier *.md **/*.md *.json **/*.yml --write", 41 | "lint": "yarn lint:eslint && yarn lint:prettier" 42 | }, 43 | "jest": { 44 | "collectCoverage": true, 45 | "preset": "ts-jest", 46 | "testEnvironment": "node" 47 | }, 48 | "dependencies": { 49 | "@iarna/toml": "^3.0.0", 50 | "ini": "^2.0.0", 51 | "js-yaml": "^4.1.0", 52 | "json5": "^2.2.0" 53 | }, 54 | "devDependencies": { 55 | "@release-it/conventional-changelog": "^3.0.1", 56 | "@types/ini": "^1.3.30", 57 | "@types/jest": "^26.0.24", 58 | "@types/js-yaml": "^4.0.2", 59 | "@types/yaml": "^1.9.7", 60 | "@typescript-eslint/eslint-plugin": "^4.28.4", 61 | "@typescript-eslint/parser": "^4.28.4", 62 | "eslint": "^7.31.0", 63 | "eslint-config-prettier": "^8.3.0", 64 | "eslint-plugin-prettier": "^3.4.0", 65 | "jest": "^27.0.6", 66 | "prettier": "^2.3.2", 67 | "release-it": "^14.10.0", 68 | "siroc": "^0.14.0", 69 | "ts-jest": "^27.0.3", 70 | "typescript": "^4.3.5" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /siroc.config.ts: -------------------------------------------------------------------------------- 1 | import { defineSirocConfig } from 'siroc' 2 | 3 | export default defineSirocConfig({ 4 | rollup: { 5 | output: { 6 | exports: 'named', 7 | }, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /src/defaults.ts: -------------------------------------------------------------------------------- 1 | import parsers, { Parser } from './parsers'; 2 | 3 | /** 4 | * The default parsers that are used by c9h. 5 | * 6 | * This is set to all available parsers. 7 | */ 8 | export const defaultParsers: Parser[] = parsers; 9 | 10 | /** 11 | * The default directories that c9h should search for the configuration 12 | * files in. 13 | * 14 | * This is set to the current working directory (`process.cwd`), the etc 15 | * directory (``/etc/${name}``), and a hidden directory in your user's 16 | * HOME directory (``$HOME/.${name}``) are searched by c9h. 17 | */ 18 | export const defaultPaths: ((name: string) => string)[] = [ 19 | (name: string): string => `${process.env.HOME}/.${name}`, 20 | (): string => process.cwd(), 21 | (): string => `${process.cwd()}/.config`, 22 | (name: string): string => `/etc/${name}`, 23 | ]; 24 | -------------------------------------------------------------------------------- /src/env.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Parses environment variables into an object. Variables 3 | * are filtered based on the provided prefix, and `_` characters 4 | * in variable names are used to split the variable and introduce 5 | * nesting. 6 | * 7 | * @param prefix The env var prefix used to filter variables. 8 | * @param env The object containing env vars. 9 | * @returns The parsed object. 10 | */ 11 | /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ 12 | export function parse(prefix: string, env = process.env): T { 13 | /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ 14 | const result = {} as any; 15 | 16 | for (const [key, value] of Object.entries(env)) { 17 | if (!key.startsWith(prefix)) { 18 | continue; 19 | } 20 | 21 | const keys = key.replace(prefix, '').toLowerCase().split('_'); 22 | 23 | let current = result; 24 | let currentKey; 25 | 26 | while ((currentKey = keys.shift())) { 27 | if (keys.length) { 28 | current[currentKey] = {}; 29 | current = current[currentKey]; 30 | } else { 31 | current[currentKey] = value; 32 | } 33 | } 34 | } 35 | 36 | return result; 37 | } 38 | 39 | /** 40 | * Converts a c9h name to an environment variable prefix. `@` 41 | * characters are removed, `-` and `/` characters are replaced 42 | * with `_`, and lowercase letters are converted to uppercase. 43 | * 44 | * An error is thrown if any other characters (unsupported) are 45 | * found in `name`. 46 | * 47 | * @param name The c9h name to convert to an env var prefix. 48 | * @returns The converted name as an env var prefix. 49 | */ 50 | export function toPrefix(name: string): string { 51 | // Remove @ chars, and replace - or / with _ 52 | name = name.replace(/[@]/g, '').replace(/[-\/]/g, '_').toUpperCase(); 53 | 54 | if (!name.match(/^[A-Z0-9_]+$/)) { 55 | throw new Error( 56 | `Invalid characters were provided for c9h name. The name must only include letters, digits, '-', '_', '@', and '/'.`, 57 | ); 58 | } 59 | 60 | return `${name}_`; 61 | } 62 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { parse } from 'path'; 2 | import { parse as parseEnv, toPrefix } from './env'; 3 | import { merge } from './merge'; 4 | import { Options } from './options'; 5 | import { defaultPaths, defaultParsers } from './defaults'; 6 | import { load as loadFile, loadSync as loadFileSync } from './load'; 7 | 8 | /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ 9 | export async function load(options?: Options): Promise { 10 | let name = options?.name || process.env.npm_package_name || parse(process.cwd()).name; 11 | let filename = options?.filename || name; 12 | const defaults = options?.defaults || {}; 13 | const paths = options?.paths || defaultPaths; 14 | const parsers = options?.parsers || defaultParsers; 15 | const mergeArray = options?.mergeArray === undefined ? false : options.mergeArray; 16 | const mergeFiles = options?.mergeFiles || 'first'; 17 | 18 | name = typeof name === 'function' ? name() : name; 19 | filename = typeof filename === 'function' ? filename() : filename; 20 | 21 | const loaded = await loadFile( 22 | filename, 23 | paths.map((fn) => fn(name as string)), 24 | parsers, 25 | mergeFiles !== 'first', 26 | ); 27 | 28 | if (mergeFiles === 'error' && loaded.length > 1) { 29 | throw new Error("`options.mergeFiles` is set to 'error', but multiple files were found!"); 30 | } 31 | 32 | const env = parseEnv(toPrefix(name)); 33 | 34 | return merge(defaults, [...loaded, env], { mergeArray }); 35 | } 36 | 37 | /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ 38 | export function loadSync(options?: Options): T { 39 | let name = options?.name || process.env.npm_package_name || parse(process.cwd()).name; 40 | let filename = options?.filename || name; 41 | const defaults = options?.defaults || {}; 42 | const paths = options?.paths || defaultPaths; 43 | const parsers = options?.parsers || defaultParsers; 44 | const mergeArray = options?.mergeArray === undefined ? false : options.mergeArray; 45 | const mergeFiles = options?.mergeFiles || 'first'; 46 | 47 | name = typeof name === 'function' ? name() : name; 48 | filename = typeof filename === 'function' ? filename() : filename; 49 | 50 | const loaded = loadFileSync( 51 | filename, 52 | paths.map((fn) => fn(name as string)), 53 | parsers, 54 | mergeFiles !== 'first', 55 | ); 56 | 57 | if (mergeFiles === 'error' && loaded.length > 1) { 58 | throw new Error("`options.mergeFiles` is set to 'error', but multiple files were found!"); 59 | } 60 | 61 | const env = parseEnv(toPrefix(name)); 62 | 63 | return merge(defaults, [...loaded, env], { mergeArray }); 64 | } 65 | 66 | export default loadSync; 67 | -------------------------------------------------------------------------------- /src/load.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync, promises } from 'fs'; 2 | import { join } from 'path'; 3 | import { Parser } from './parsers'; 4 | 5 | /** 6 | * Asynchronously checks if a file exists. 7 | * 8 | * @param path The path of the file to check. 9 | * @returns True if the file exists, otherwise false. 10 | */ 11 | export async function fileExists(path: string): Promise { 12 | return promises.stat(path).then( 13 | () => true, 14 | () => false, 15 | ); 16 | } 17 | 18 | /** 19 | * This method attempts to find configuration files matching a provided name 20 | * in an assortment of directories. The extension of the files is dictated by 21 | * the parsers provided to this function. 22 | * 23 | * @param name The name of the file (excluding extension) to load. 24 | * @param paths An array of directory paths to search for the file to load in. 25 | * @param parsers An array of parsers to handle the file if found and loaded. 26 | * @param many Whether many files should be parsed, or just the first file 27 | * found. 28 | * @returns The parsed contents of the loaded files. 29 | */ 30 | /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ 31 | export async function load(name: string, paths: string[], parsers: Parser[], many: boolean): Promise { 32 | const found = []; 33 | 34 | for (const path of paths) { 35 | for (const parser of parsers) { 36 | for (const extension of parser.extensions()) { 37 | const file = join(path, `${name}.${extension}`); 38 | if (await fileExists(file)) { 39 | const contents = await promises.readFile(file, { encoding: 'utf-8' }); 40 | const parsed = parser.parse(contents); 41 | 42 | if (!many) { 43 | return [parsed]; 44 | } 45 | 46 | found.push(parsed); 47 | } 48 | } 49 | } 50 | } 51 | 52 | return found; 53 | } 54 | 55 | /** 56 | * This method attempts to find configuration files matching a provided name 57 | * in an assortment of directories. The extension of the files is dictated by 58 | * the parsers provided to this function. 59 | * 60 | * @param name The name of the file (excluding extension) to load. 61 | * @param paths An array of directory paths to search for the file to load in. 62 | * @param parsers An array of parsers to handle the file if found and loaded. 63 | * @param many Whether many files should be parsed, or just the first file 64 | * found. 65 | * @returns The parsed contents of the loaded files. 66 | */ 67 | /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ 68 | export function loadSync(name: string, paths: string[], parsers: Parser[], many: boolean): T[] { 69 | const found = []; 70 | 71 | for (const path of paths) { 72 | for (const parser of parsers) { 73 | for (const extension of parser.extensions()) { 74 | const file = join(path, `${name}.${extension}`); 75 | if (existsSync(file)) { 76 | const contents = readFileSync(file, { encoding: 'utf-8' }); 77 | const parsed = parser.parse(contents); 78 | 79 | if (!many) { 80 | return [parsed]; 81 | } 82 | 83 | found.push(parsed); 84 | } 85 | } 86 | } 87 | } 88 | 89 | return found; 90 | } 91 | -------------------------------------------------------------------------------- /src/merge.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any,@typescript-eslint/explicit-module-boundary-types */ 2 | /** 3 | * Checks if a value is an object or not. 4 | * 5 | * @param obj The value to check. 6 | * @returns True if the provided value is an object, otherwise false. 7 | */ 8 | export function isObject(obj: any): boolean { 9 | return obj && typeof obj === 'object' && !Array.isArray(obj); 10 | } 11 | 12 | /** 13 | * Options used to configure the behaviour of the merge function. 14 | */ 15 | export type MergeOptions = { 16 | /** 17 | * This indicates whether arrays should be merged or replaced 18 | * when found in both the original and merging object. 19 | */ 20 | mergeArray?: boolean; 21 | }; 22 | 23 | /** 24 | * Performs a deep merge of two or more objects, and returns the 25 | * merged result. 26 | * 27 | * @param target The original object. 28 | * @param sources An array of objects to merge into the original. 29 | * @param options Options used to configure the merge behaviour. 30 | * @returns The merged result. 31 | */ 32 | export function merge(target: any, sources: any[], options?: MergeOptions): any { 33 | if (!sources.length) { 34 | return target; 35 | } 36 | 37 | const source = sources.shift(); 38 | 39 | if (isObject(target) && isObject(source)) { 40 | for (const [key, value] of Object.entries(source)) { 41 | if (isObject(value)) { 42 | if (!target[key]) { 43 | target[key] = {}; 44 | } 45 | merge(target[key], [value], options); 46 | } else if (Array.isArray(value) && options?.mergeArray) { 47 | if (!target[key]) { 48 | target[key] = []; 49 | } 50 | target[key] = [...target[key], ...value]; 51 | } else { 52 | target[key] = value; 53 | } 54 | } 55 | } 56 | 57 | return merge(target, sources, options); 58 | } 59 | -------------------------------------------------------------------------------- /src/options.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from './parsers'; 2 | 3 | /** 4 | * Represents different behaviour modes for merging multiple 5 | * configuration files. 6 | * 7 | * `merge`: This indicates that c9h should merge multiple files if 8 | * found, using the same method to merge files with default 9 | * values. 10 | * 11 | * `error`: This indicates that c9h should throw an error if multiple 12 | * files are found. This mode is good if you're expecting to 13 | * only have one file, and what c9h to flag when this isn't 14 | * the case. 15 | * 16 | * `first`: This indicates that c9h should use the first file that it 17 | * finds, and silently ignore all others. 18 | */ 19 | type FileMergeMode = 'merge' | 'error' | 'first'; 20 | 21 | /** 22 | * Options used to configure c9h's behaviour. 23 | */ 24 | export type Options = { 25 | /** 26 | * This is the name of the c9h instance. It's supplied 27 | * to the functions declared in the `paths` property. 28 | * 29 | * This can be a string, or a function that returns a string 30 | * (to allow for dynamic names). 31 | * 32 | * By default, this is the `name` property of your project's 33 | * `package.json` file or the name of your project's current 34 | * working directory. 35 | */ 36 | name?: string | (() => string); 37 | /** 38 | * This is the filename of the configuration file (excluding 39 | * the file extension, as this is provided by the parser). 40 | * 41 | * This can be a string, or a function that returns a string 42 | * (to allow for dynamic filenames). 43 | * 44 | * By default, this is identical to the `name` property. 45 | */ 46 | filename?: string | (() => string); 47 | /** 48 | * Default configuration values. These values will be overriden 49 | * by values found in configuration files and environment 50 | * variable values. 51 | * 52 | * By default, no defaults are set. 53 | */ 54 | defaults?: Partial; 55 | /** 56 | * The parsers that c9h should use when looking for configuration 57 | * files. 58 | * 59 | * By default, all parsers are enabled (JSON, JSON5, TOML, YAML, 60 | * and INI). 61 | */ 62 | parsers?: Parser[]; 63 | /** 64 | * The directories that c9h should search for the configuration files 65 | * in. 66 | * 67 | * By default, the current working directory (`process.cwd`), the etc 68 | * directory (``/etc/${name}``), and a hidden directory in your user's 69 | * HOME directory (``$HOME/.${name}``) are searched by c9h. 70 | */ 71 | paths?: ((name: string) => string)[]; 72 | /** 73 | * This indicates whether arrays should be merged or replaced 74 | * when found in both the default values and configuration files. 75 | */ 76 | mergeArray?: boolean; 77 | /** 78 | * This indicates c9h's behaviour when multiple configuration files 79 | * are found. 80 | */ 81 | mergeFiles?: FileMergeMode; 82 | }; 83 | -------------------------------------------------------------------------------- /src/parsers/index.ts: -------------------------------------------------------------------------------- 1 | import json from './json'; 2 | import json5 from './json5'; 3 | import toml from './toml'; 4 | import yaml from './yaml'; 5 | import ini from './ini'; 6 | 7 | /** 8 | * Represents a parser that parses a file contents, and returns 9 | * a parsed configuration object. 10 | */ 11 | export interface Parser { 12 | /** 13 | * Returns the file extensions that the parser supports. 14 | */ 15 | extensions(): string[]; 16 | /** 17 | * Attempts to parse a file contents, and returns the parsed 18 | * content. 19 | * 20 | * @param file The raw string contents of the file to parse. 21 | */ 22 | /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ 23 | parse(file: string): T; 24 | } 25 | 26 | const parsers: Parser[] = [ini, json, json5, toml, yaml]; 27 | 28 | export default parsers; 29 | -------------------------------------------------------------------------------- /src/parsers/ini.ts: -------------------------------------------------------------------------------- 1 | import ini from 'ini'; 2 | import { create } from './util'; 3 | 4 | /** 5 | * The INI parser uses the `ini` NPM package to parse 6 | * `.ini` files. 7 | */ 8 | export default create(['ini'], (file: string) => ini.parse(file) as T); 9 | -------------------------------------------------------------------------------- /src/parsers/json.ts: -------------------------------------------------------------------------------- 1 | import { create } from './util'; 2 | 3 | /** 4 | * The JSON parser uses the built-in `JSON.parse` 5 | * function to parse `.json` files. 6 | */ 7 | export default create(['json'], JSON.parse); 8 | -------------------------------------------------------------------------------- /src/parsers/json5.ts: -------------------------------------------------------------------------------- 1 | import json5 from 'json5'; 2 | import { create } from './util'; 3 | 4 | /** 5 | * The JSON5 parser uses the `json5` NPM package to parse 6 | * `.json5` files. 7 | */ 8 | export default create(['json5'], json5.parse); 9 | -------------------------------------------------------------------------------- /src/parsers/toml.ts: -------------------------------------------------------------------------------- 1 | import toml from '@iarna/toml'; 2 | import { create } from './util'; 3 | 4 | /** 5 | * The TOML parser uses the `@iarna/toml` NPM package 6 | * to parse `.toml` files (that meet the TOML 1.0.0-rc.1 7 | * specification). 8 | */ 9 | export default create(['toml'], (file: string) => toml.parse(file) as unknown as T); 10 | -------------------------------------------------------------------------------- /src/parsers/util.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '.'; 2 | 3 | /** 4 | * This utility function creates a new `Parser` interface. 5 | * 6 | * @param extenstions The file extensions that this parser supports. 7 | * @param parse The parsing function for this parser, which accepts 8 | * a file contents string. 9 | * @returns The created parser interface implementation. 10 | */ 11 | export function create(extenstions: string[], parse: (file: string) => T): Parser { 12 | return { 13 | extensions() { 14 | return extenstions; 15 | }, 16 | parse, 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/parsers/yaml.ts: -------------------------------------------------------------------------------- 1 | import yaml from 'js-yaml'; 2 | import { create } from './util'; 3 | 4 | /** 5 | * The YAML parser uses the `yaml` NPM package to parse 6 | * `.yaml` and `.yml` files. 7 | */ 8 | export default create(['yaml', 'yml'], (file: string) => yaml.load(file) as unknown as T); 9 | -------------------------------------------------------------------------------- /test/.config/config-example.toml: -------------------------------------------------------------------------------- 1 | hello = ['world'] 2 | -------------------------------------------------------------------------------- /test/cottonmouth.toml: -------------------------------------------------------------------------------- 1 | hello = 'world' 2 | -------------------------------------------------------------------------------- /test/defaults.test.ts: -------------------------------------------------------------------------------- 1 | import { defaultParsers, defaultPaths } from '../src/defaults'; 2 | 3 | describe('defaultParsers', () => { 4 | let parsers: string[]; 5 | 6 | beforeAll(() => { 7 | parsers = defaultParsers.map((parser) => parser.extensions()).flat(); 8 | }); 9 | 10 | it('should contain JSON parsers', () => { 11 | expect(parsers).toContain('json'); 12 | expect(parsers).toContain('json5'); 13 | }); 14 | 15 | it('should contain the TOML parser', () => { 16 | expect(parsers).toContain('toml'); 17 | }); 18 | 19 | it('should contain the YAML parser', () => { 20 | expect(parsers).toContain('yaml'); 21 | }); 22 | 23 | it('should contain the INI parser', () => { 24 | expect(parsers).toContain('ini'); 25 | }); 26 | }); 27 | 28 | describe('defaultPaths', () => { 29 | let paths: string[]; 30 | 31 | beforeAll(() => { 32 | process.env.HOME = '/home/jest'; 33 | paths = defaultPaths.map((fn) => fn('test')); 34 | }); 35 | 36 | it('should contain the current working directory', () => { 37 | expect(paths).toContain(process.cwd()); 38 | }); 39 | 40 | it('should contain the .config directory', () => { 41 | expect(paths).toContain(`${process.cwd()}/.config`); 42 | }); 43 | 44 | it('should contain the $HOME directory', () => { 45 | expect(paths).toContain(`/home/jest/.test`); 46 | }); 47 | 48 | it('should contain the `/etc` directory', () => { 49 | expect(paths).toContain(`/etc/test`); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/env.test.ts: -------------------------------------------------------------------------------- 1 | import { parse, toPrefix } from '../src/env'; 2 | 3 | describe('environment parsing', () => { 4 | it('should parse values', () => { 5 | const parsed = parse('TEST_', { 6 | TEST_HELLO: 'hello world', 7 | TEST_HELLOTHERE: 'general kenobi', 8 | }); 9 | 10 | expect(Object.keys(parsed)).toHaveLength(2); 11 | 12 | expect(parsed).toHaveProperty('hello'); 13 | expect(parsed.hello).toEqual('hello world'); 14 | 15 | expect(parsed).toHaveProperty('hellothere'); 16 | expect(parsed.hellothere).toEqual('general kenobi'); 17 | }); 18 | 19 | it('should parse nested values', () => { 20 | const parsed = parse('TEST_', { 21 | TEST_HELLOWORLD: 'hello world', 22 | TEST_HELLO_THERE: 'general kenobi', 23 | }); 24 | 25 | expect(Object.keys(parsed)).toHaveLength(2); 26 | 27 | expect(parsed).toHaveProperty('helloworld'); 28 | expect(parsed.helloworld).toEqual('hello world'); 29 | 30 | expect(parsed).toHaveProperty('hello'); 31 | expect(parsed.hello).toHaveProperty('there'); 32 | expect(parsed.hello.there).toEqual('general kenobi'); 33 | }); 34 | 35 | it('should skip values without prefix', () => { 36 | const parsed = parse('TEST_', { 37 | TEST_HELLO: 'hello world', 38 | NODE_ENV: 'development', 39 | }); 40 | 41 | expect(Object.keys(parsed)).toHaveLength(1); 42 | 43 | expect(parsed).toHaveProperty('hello'); 44 | expect(parsed.hello).toEqual('hello world'); 45 | }); 46 | 47 | it('should default to parsing process.env', () => { 48 | process.env = { 49 | TEST_HELLO: 'hello world', 50 | }; 51 | 52 | const parsed = parse('TEST_'); 53 | 54 | expect(Object.keys(parsed)).toHaveLength(1); 55 | 56 | expect(parsed).toHaveProperty('hello'); 57 | expect(parsed.hello).toEqual('hello world'); 58 | }); 59 | }); 60 | 61 | describe('environment variable prefix', () => { 62 | it('should handle valid names', () => { 63 | expect(toPrefix('example')).toEqual('EXAMPLE_'); 64 | expect(toPrefix('c9h')).toEqual('C9H_'); 65 | }); 66 | 67 | it('should handle extreme names', () => { 68 | expect(toPrefix('fastify-c9h')).toEqual('FASTIFY_C9H_'); 69 | expect(toPrefix('@fastify/c9h')).toEqual('FASTIFY_C9H_'); 70 | }); 71 | 72 | it('should reject erroneous names', () => { 73 | expect(() => toPrefix('fastify c9h')).toThrowError(); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /test/example.toml: -------------------------------------------------------------------------------- 1 | hello = ['world'] 2 | -------------------------------------------------------------------------------- /test/example.yml: -------------------------------------------------------------------------------- 1 | hello: [hi there] 2 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import * as c9h from '../src'; 3 | 4 | describe('c9h sync', () => { 5 | it('should load from files', () => { 6 | const loaded = c9h.loadSync({ 7 | name: 'example', 8 | paths: [() => './test'], 9 | }); 10 | 11 | expect(loaded).toBeDefined(); 12 | expect(loaded).toHaveProperty('hello'); 13 | expect(loaded.hello).toHaveLength(1); 14 | expect(loaded.hello).toContain('world'); 15 | }); 16 | 17 | it('should default to the package.json `name` property if `options.name` is undefined', () => { 18 | process.env.npm_package_name = 'example'; 19 | 20 | const loaded = c9h.loadSync({ 21 | paths: [() => `${process.cwd()}/test`], 22 | }); 23 | 24 | expect(loaded).toBeDefined(); 25 | expect(loaded).toHaveProperty('hello'); 26 | expect(loaded.hello).toHaveLength(1); 27 | expect(loaded.hello).toContain('world'); 28 | }); 29 | 30 | it('should not merge arrays by default', () => { 31 | const loaded = c9h.loadSync({ 32 | name: 'example', 33 | defaults: { 34 | hello: ['general', 'kenobi'], 35 | }, 36 | paths: [() => './test'], 37 | }); 38 | 39 | expect(loaded).toBeDefined(); 40 | expect(loaded).toHaveProperty('hello'); 41 | expect(loaded.hello).toHaveLength(1); 42 | expect(loaded.hello).toContain('world'); 43 | }); 44 | 45 | it('should not merge arrays when options.mergeArray is true', () => { 46 | const loaded = c9h.loadSync({ 47 | name: 'example', 48 | defaults: { 49 | hello: ['general', 'kenobi'], 50 | }, 51 | paths: [() => './test'], 52 | mergeArray: true, 53 | }); 54 | 55 | expect(loaded).toBeDefined(); 56 | expect(loaded).toHaveProperty('hello'); 57 | expect(loaded.hello).toHaveLength(3); 58 | expect(loaded.hello).toContain('world'); 59 | expect(loaded.hello).toContain('general'); 60 | expect(loaded.hello).toContain('kenobi'); 61 | }); 62 | 63 | it('should handle an undefined options object', () => { 64 | const loaded = c9h.loadSync(); 65 | 66 | expect(Object.keys(loaded)).toHaveLength(0); 67 | }); 68 | 69 | it('should handle `options.name` as a function', () => { 70 | const loaded = c9h.loadSync({ 71 | name: () => 'exa' + 'mple', 72 | paths: [() => './test'], 73 | }); 74 | 75 | expect(loaded).toBeDefined(); 76 | expect(loaded).toHaveProperty('hello'); 77 | expect(loaded.hello).toHaveLength(1); 78 | expect(loaded.hello).toContain('world'); 79 | }); 80 | 81 | it('should thrown an error if multiple files are found and `options.mergeFiles` is set to error', () => { 82 | expect(() => 83 | c9h.loadSync({ 84 | name: () => 'example', 85 | paths: [() => './test'], 86 | mergeFiles: 'error', 87 | }), 88 | ).toThrowError(); 89 | }); 90 | 91 | it('should default to `options.name` if `options.filename` is undefined', () => { 92 | const loaded = c9h.loadSync({ 93 | name: 'example', 94 | filename: undefined, 95 | paths: [() => './test'], 96 | }); 97 | 98 | expect(loaded).toBeDefined(); 99 | expect(loaded).toHaveProperty('hello'); 100 | expect(loaded.hello).toHaveLength(1); 101 | expect(loaded.hello).toContain('world'); 102 | }); 103 | }); 104 | 105 | describe('c9h async', () => { 106 | it('should load from files', async () => { 107 | const loaded = await c9h.load({ 108 | name: 'example', 109 | paths: [() => './test'], 110 | }); 111 | 112 | expect(loaded).toBeDefined(); 113 | expect(loaded).toHaveProperty('hello'); 114 | expect(loaded.hello).toHaveLength(1); 115 | expect(loaded.hello).toContain('world'); 116 | }); 117 | 118 | it('should default to the package.json `name` property if `options.name` is undefined', async () => { 119 | process.env.npm_package_name = 'example'; 120 | 121 | const loaded = await c9h.load({ 122 | paths: [() => `${process.cwd()}/test`], 123 | }); 124 | 125 | expect(loaded).toBeDefined(); 126 | expect(loaded).toHaveProperty('hello'); 127 | expect(loaded.hello).toHaveLength(1); 128 | expect(loaded.hello).toContain('world'); 129 | }); 130 | 131 | it('should not merge arrays by default', async () => { 132 | const loaded = await c9h.load({ 133 | name: 'example', 134 | defaults: { 135 | hello: ['general', 'kenobi'], 136 | }, 137 | paths: [() => './test'], 138 | }); 139 | 140 | expect(loaded).toBeDefined(); 141 | expect(loaded).toHaveProperty('hello'); 142 | expect(loaded.hello).toHaveLength(1); 143 | expect(loaded.hello).toContain('world'); 144 | }); 145 | 146 | it('should not merge arrays when options.mergeArray is true', async () => { 147 | const loaded = await c9h.load({ 148 | name: 'example', 149 | defaults: { 150 | hello: ['general', 'kenobi'], 151 | }, 152 | paths: [() => './test'], 153 | mergeArray: true, 154 | }); 155 | 156 | expect(loaded).toBeDefined(); 157 | expect(loaded).toHaveProperty('hello'); 158 | expect(loaded.hello).toHaveLength(3); 159 | expect(loaded.hello).toContain('world'); 160 | expect(loaded.hello).toContain('general'); 161 | expect(loaded.hello).toContain('kenobi'); 162 | }); 163 | 164 | it('should handle an undefined options object', async () => { 165 | const loaded = await c9h.load(); 166 | 167 | expect(Object.keys(loaded)).toHaveLength(0); 168 | }); 169 | 170 | it('should handle `options.name` as a function', async () => { 171 | const loaded = await c9h.load({ 172 | name: () => 'exa' + 'mple', 173 | paths: [() => './test'], 174 | }); 175 | 176 | expect(loaded).toBeDefined(); 177 | expect(loaded).toHaveProperty('hello'); 178 | expect(loaded.hello).toHaveLength(1); 179 | expect(loaded.hello).toContain('world'); 180 | }); 181 | 182 | it('should thrown an error if multiple files are found and `options.mergeFiles` is set to error', async () => { 183 | await expect(() => 184 | c9h.load({ 185 | name: () => 'example', 186 | paths: [() => './test'], 187 | mergeFiles: 'error', 188 | }), 189 | ).rejects.toThrow(); 190 | }); 191 | 192 | it('should default to `options.name` if `options.filename` is undefined', async () => { 193 | const loaded = await c9h.load({ 194 | name: 'example', 195 | filename: undefined, 196 | paths: [() => './test'], 197 | }); 198 | 199 | expect(loaded).toBeDefined(); 200 | expect(loaded).toHaveProperty('hello'); 201 | expect(loaded.hello).toHaveLength(1); 202 | expect(loaded.hello).toContain('world'); 203 | }); 204 | }); 205 | -------------------------------------------------------------------------------- /test/load.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { fileExists, load, loadSync } from '../src/load'; 3 | import toml from '../src/parsers/toml'; 4 | import json5 from '../src/parsers/json5'; 5 | import yaml from '../src/parsers/yaml'; 6 | 7 | describe('the fileExists function', () => { 8 | it('should asynchronously check if a file exists', async () => { 9 | expect(await fileExists('./test/load.test.ts')).toBeTruthy(); 10 | expect(await fileExists('./test/load.testa.ts')).toBeFalsy(); 11 | }); 12 | }); 13 | 14 | describe('asynchronous file loading', () => { 15 | it('should load files', async () => { 16 | const loaded = await load('example', ['./test'], [toml], false); 17 | 18 | expect(loaded).toHaveLength(1); 19 | expect(loaded[0]).toHaveProperty('hello'); 20 | expect(loaded[0].hello).toHaveLength(1); 21 | expect(loaded[0].hello).toContain('world'); 22 | }); 23 | 24 | it('should return the first file found if the many parameter is false', async () => { 25 | const loaded = await load('example', ['./test'], [toml, yaml], false); 26 | 27 | expect(loaded).toHaveLength(1); 28 | expect(loaded[0]).toHaveProperty('hello'); 29 | expect(loaded[0].hello).toHaveLength(1); 30 | expect(loaded[0].hello).toContain('world'); 31 | }); 32 | 33 | it('should return multiple files if the many parameter is true', async () => { 34 | const loaded = await load('example', ['./test'], [toml, yaml], true); 35 | 36 | expect(loaded).toHaveLength(2); 37 | expect(loaded[0]).toHaveProperty('hello'); 38 | expect(loaded[0].hello).toHaveLength(1); 39 | expect(loaded[0].hello).toContain('world'); 40 | expect(loaded[1]).toHaveProperty('hello'); 41 | expect(loaded[1].hello).toHaveLength(1); 42 | expect(loaded[1].hello).toContain('hi there'); 43 | }); 44 | 45 | it('should return an empty array if no files are found', async () => { 46 | expect(await load('example', ['./test'], [json5], false)).toHaveLength(0); 47 | }); 48 | }); 49 | 50 | describe('synchronous file loading', () => { 51 | it('should load files', () => { 52 | const loaded = loadSync('example', ['./test'], [toml], false); 53 | 54 | expect(loaded).toHaveLength(1); 55 | expect(loaded[0]).toHaveProperty('hello'); 56 | expect(loaded[0].hello).toHaveLength(1); 57 | expect(loaded[0].hello).toContain('world'); 58 | }); 59 | 60 | it('should return the first file found if the many parameter is false', () => { 61 | const loaded = loadSync('example', ['./test'], [toml, yaml], false); 62 | 63 | expect(loaded).toHaveLength(1); 64 | expect(loaded[0]).toHaveProperty('hello'); 65 | expect(loaded[0].hello).toHaveLength(1); 66 | expect(loaded[0].hello).toContain('world'); 67 | }); 68 | 69 | it('should return multiple files if the many parameter is true', () => { 70 | const loaded = loadSync('example', ['./test'], [toml, yaml], true); 71 | 72 | expect(loaded).toHaveLength(2); 73 | expect(loaded[0]).toHaveProperty('hello'); 74 | expect(loaded[0].hello).toHaveLength(1); 75 | expect(loaded[0].hello).toContain('world'); 76 | expect(loaded[1]).toHaveProperty('hello'); 77 | expect(loaded[1].hello).toHaveLength(1); 78 | expect(loaded[1].hello).toContain('hi there'); 79 | }); 80 | 81 | it('should return an empty array if no files are found', () => { 82 | expect(loadSync('example', ['./test'], [json5], false)).toHaveLength(0); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /test/merge.test.ts: -------------------------------------------------------------------------------- 1 | import { isObject, merge } from '../src/merge'; 2 | 3 | describe('isObject function', () => { 4 | it('should accept valid arguments', () => { 5 | expect(isObject({ abc: 123 })).toBeTruthy(); 6 | }); 7 | 8 | it('should accept extreme arguments', () => { 9 | expect(isObject({ abc: 123, i: { am: 'nested' } })).toBeTruthy(); 10 | }); 11 | 12 | it('should reject erroneous arguments', () => { 13 | expect(isObject(false)).toBeFalsy(); 14 | expect(isObject(undefined)).toBeFalsy(); 15 | expect(isObject('hello world')).toBeFalsy(); 16 | expect(isObject([123, 456])).toBeFalsy(); 17 | }); 18 | }); 19 | 20 | describe('merging util function', () => { 21 | it('should merge two objects', () => { 22 | const a = { hello: 'world' }; 23 | const b = { abc: 123 }; 24 | 25 | const merged = merge(a, [b]); 26 | 27 | expect(Object.keys(merged)).toHaveLength(2); 28 | 29 | expect(merged).toHaveProperty('hello'); 30 | expect(merged.hello).toEqual('world'); 31 | 32 | expect(merged).toHaveProperty('abc'); 33 | expect(merged.abc).toEqual(123); 34 | }); 35 | 36 | it('should merge objects with nested values', () => { 37 | const a = { abc: { hello: 'world' } }; 38 | const b = { abc: { easy: { as: 123 } } }; 39 | 40 | const merged = merge(a, [b]); 41 | 42 | expect(Object.keys(merged)).toHaveLength(1); 43 | 44 | expect(merged).toHaveProperty('abc'); 45 | expect(merged.abc).toHaveProperty('easy'); 46 | expect(merged.abc.easy).toHaveProperty('as'); 47 | expect(merged.abc.easy.as).toEqual(123); 48 | expect(merged.abc).toHaveProperty('hello'); 49 | expect(merged.abc.hello).toEqual('world'); 50 | }); 51 | 52 | it('should merge array values when options.mergeArray is true', () => { 53 | const a = { hello: ['world'] }; 54 | const b = { hello: ['there'], general: ['kenobi'] }; 55 | 56 | const merged = merge(a, [b], { mergeArray: true }); 57 | 58 | expect(Object.keys(merged)).toHaveLength(2); 59 | 60 | expect(merged).toHaveProperty('hello'); 61 | expect(merged.hello).toHaveLength(2); 62 | expect(merged.hello).toContain('world'); 63 | expect(merged.hello).toContain('there'); 64 | 65 | expect(merged).toHaveProperty('general'); 66 | expect(merged.general).toHaveLength(1); 67 | expect(merged.general).toContain('kenobi'); 68 | }); 69 | 70 | it('should not merge non-object values', () => { 71 | const a = { hello: 'world' }; 72 | const b = [123, 456]; 73 | 74 | const merged = merge(a, [b]); 75 | 76 | expect(Object.keys(merged)).toHaveLength(1); 77 | 78 | expect(merged).toHaveProperty('hello'); 79 | expect(merged.hello).toEqual('world'); 80 | }); 81 | 82 | it('should return the target object if no sources are provided', () => { 83 | const a = { hello: 'world' }; 84 | const merged = merge(a, []); 85 | 86 | expect(Object.keys(merged)).toHaveLength(1); 87 | expect(merged).toHaveProperty('hello'); 88 | expect(merged.hello).toEqual('world'); 89 | }); 90 | 91 | it('should ignore non-object sources', () => { 92 | const a = { hello: 'world' }; 93 | const b = [123, 456]; 94 | 95 | const merged = merge(a, b); 96 | 97 | expect(Object.keys(merged)).toHaveLength(1); 98 | expect(merged).toHaveProperty('hello'); 99 | expect(merged.hello).toEqual('world'); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /test/parsers/ini.test.ts: -------------------------------------------------------------------------------- 1 | import ini from '../../src/parsers/ini'; 2 | 3 | describe('INI parser', () => { 4 | it('should handle `.toml` extensions', () => { 5 | expect(ini.extensions).toBeDefined(); 6 | 7 | const extensions = ini.extensions(); 8 | 9 | expect(extensions).toHaveLength(1); 10 | expect(extensions).toContain('ini'); 11 | }); 12 | 13 | it('should parse valid INI', () => { 14 | const j = ` 15 | hello = 'world' 16 | `; 17 | 18 | const parsed = ini.parse(j); 19 | 20 | expect(parsed).toBeDefined(); 21 | 22 | expect(parsed).toHaveProperty('hello'); 23 | expect(parsed.hello).toEqual('world'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/parsers/json.test.ts: -------------------------------------------------------------------------------- 1 | import json from '../../src/parsers/json'; 2 | 3 | describe('JSON parser', () => { 4 | it('should handle `.json` extensions', () => { 5 | expect(json.extensions).toBeDefined(); 6 | 7 | const extensions = json.extensions(); 8 | 9 | expect(extensions).toHaveLength(1); 10 | expect(extensions).toContain('json'); 11 | }); 12 | 13 | it('should parse valid JSON', () => { 14 | const j = ` 15 | { 16 | "hello": "world" 17 | } 18 | `; 19 | 20 | const parsed = json.parse(j); 21 | 22 | expect(parsed).toBeDefined(); 23 | 24 | expect(parsed).toHaveProperty('hello'); 25 | expect(parsed.hello).toEqual('world'); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/parsers/json5.test.ts: -------------------------------------------------------------------------------- 1 | import json5 from '../../src/parsers/json5'; 2 | 3 | describe('JSON5 parser', () => { 4 | it('should handle `.json5` extensions', () => { 5 | expect(json5.extensions).toBeDefined(); 6 | 7 | const extensions = json5.extensions(); 8 | 9 | expect(extensions).toHaveLength(1); 10 | expect(extensions).toContain('json5'); 11 | }); 12 | 13 | it('should parse valid JSON5', () => { 14 | const j = ` 15 | { 16 | hello: 'world', 17 | } 18 | `; 19 | 20 | const parsed = json5.parse(j); 21 | 22 | expect(parsed).toBeDefined(); 23 | 24 | expect(parsed).toHaveProperty('hello'); 25 | expect(parsed.hello).toEqual('world'); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/parsers/toml.test.ts: -------------------------------------------------------------------------------- 1 | import toml from '../../src/parsers/toml'; 2 | 3 | describe('TOML parser', () => { 4 | it('should handle `.toml` extensions', () => { 5 | expect(toml.extensions).toBeDefined(); 6 | 7 | const extensions = toml.extensions(); 8 | 9 | expect(extensions).toHaveLength(1); 10 | expect(extensions).toContain('toml'); 11 | }); 12 | 13 | it('should parse valid TOML', () => { 14 | const j = ` 15 | [hello] 16 | there = 'general kenobi' 17 | `; 18 | 19 | const parsed = toml.parse(j); 20 | 21 | expect(parsed).toBeDefined(); 22 | 23 | expect(parsed).toHaveProperty('hello'); 24 | expect(parsed.hello).toHaveProperty('there'); 25 | expect(parsed.hello.there).toEqual('general kenobi'); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/parsers/yaml.test.ts: -------------------------------------------------------------------------------- 1 | import yaml from '../../src/parsers/yaml'; 2 | 3 | describe('YAML parser', () => { 4 | it('should handle `.yaml` and `.yml` extensions', () => { 5 | expect(yaml.extensions).toBeDefined(); 6 | 7 | const extensions = yaml.extensions(); 8 | 9 | expect(extensions).toHaveLength(2); 10 | expect(extensions).toContain('yaml'); 11 | expect(extensions).toContain('yml'); 12 | }); 13 | 14 | it('should parse valid YAML', () => { 15 | const j = ` 16 | hello: 17 | there: general kenobi 18 | `; 19 | 20 | const parsed = yaml.parse(j); 21 | 22 | expect(parsed).toBeDefined(); 23 | 24 | expect(parsed).toHaveProperty('hello'); 25 | expect(parsed.hello).toHaveProperty('there'); 26 | expect(parsed.hello.there).toEqual('general kenobi'); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "esModuleInterop": true, 8 | "strict": true, 9 | "declaration": true, 10 | "types": ["node", "jest"] 11 | }, 12 | "include": ["src"] 13 | } 14 | --------------------------------------------------------------------------------