├── .gitignore
├── test
├── basic
│ ├── .env
│ ├── basic.config.js
│ ├── package.json
│ └── test.spec.js
├── context-configs
│ ├── configs
│ │ ├── basic.config.js
│ │ ├── superseder.config.js
│ │ └── usepkgjson.config.js
│ ├── package.json
│ └── context.spec.js
└── context-configs-circular
│ ├── configs
│ ├── circular1.config.js
│ └── circular2.config.js
│ ├── package.json
│ └── context.spec.js
├── configent.png
├── postBuild.js
├── tsconfig.json
├── package.json
├── index.d.ts
├── README.md
└── configent.js
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 | /dist/
3 | .DS_Store
4 | **/.history
--------------------------------------------------------------------------------
/test/basic/.env:
--------------------------------------------------------------------------------
1 | BASIC_fromDotEnv = true
2 | BASIC_fromEnv = false
--------------------------------------------------------------------------------
/configent.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/roxiness/configent/HEAD/configent.png
--------------------------------------------------------------------------------
/test/basic/basic.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | fromConfig: true,
3 | fromDotEnv: false,
4 | fromEnv: false,
5 | }
--------------------------------------------------------------------------------
/test/context-configs/configs/basic.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | name: 'basic',
3 | condition: () => process.env.USE_BASIC,
4 | config: () => ({ fromContext: 'basic' })
5 | }
--------------------------------------------------------------------------------
/postBuild.js:
--------------------------------------------------------------------------------
1 | const {appendFileSync} = require('fs')
2 | const footer = '\n\n---\n\nVintage vector created by macrovector - www.freepik.com'
3 |
4 | appendFileSync('./README.md', footer)
--------------------------------------------------------------------------------
/test/context-configs/configs/superseder.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | name: 'Superseder',
3 | supersedes: ['basic'],
4 | condition: () => process.env.USE_SUPERSEDER,
5 | config: () => ({ fromContext: 'superseder' })
6 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "declaration": true,
4 | "allowJs": true,
5 | "checkJs": true,
6 | "emitDeclarationOnly": true,
7 | "outFile": "index.d.ts"
8 | },
9 | "include": ["configent.js"]
10 | }
11 |
--------------------------------------------------------------------------------
/test/context-configs-circular/configs/circular1.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | name: 'Use package.json',
3 | supersedes: ['circular2'],
4 | condition: ({ pkgjson }) => (process.env.USE_CIRCULAR),
5 | config: () => ({ fromContext: 'circular1' })
6 | }
--------------------------------------------------------------------------------
/test/context-configs-circular/configs/circular2.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | name: 'Use package.json',
3 | supersedes: ['circular1'],
4 | condition: ({ pkgjson }) => (process.env.USE_CIRCULAR),
5 | config: () => ({ fromContext: 'circular2' })
6 | }
--------------------------------------------------------------------------------
/test/context-configs/configs/usepkgjson.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | name: 'Use package.json',
3 | supersedes: ['superseder'],
4 | condition: ({ pkgjson }) => (process.env.USE_PKGJSON && pkgjson.name === 'context-configs'),
5 | config: () => ({ fromContext: 'usepkgjson' })
6 | }
--------------------------------------------------------------------------------
/test/context-configs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "context-configs",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "author": "",
10 | "license": "ISC",
11 | "devDependencies": {
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/test/context-configs-circular/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "context-configs",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "author": "",
10 | "license": "ISC",
11 | "devDependencies": {
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/test/basic/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "basic",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "author": "",
10 | "license": "ISC",
11 | "devDependencies": {
12 | },
13 | "basic": {
14 | "fromPackageJson": true
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/test/context-configs-circular/context.spec.js:
--------------------------------------------------------------------------------
1 | const test = require('ava').default
2 | const { configent } = require('../..')
3 | process.chdir(__dirname) //change cwd to __dirname
4 |
5 | const defaults = { fromContext: false }
6 | const configentOptions = {
7 | useDetectDefaults: true,
8 | cacheConfig: false
9 | }
10 |
11 | test('circular tests create error', async t => {
12 |
13 | const res = await t.try(() => {
14 | configent(defaults, {}, configentOptions)
15 | })
16 | res.discard()
17 |
18 | t.is(res.errors[0].savedError.message,
19 | `Looks like you have circular supersedings ` +
20 | `\ncircular1.config.js supersedes circular2` +
21 | `\ncircular2.config.js supersedes circular1`
22 | )
23 | })
24 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "configent",
3 | "version": "2.2.0",
4 | "description": "Confident configurations",
5 | "repository": {
6 | "type": "git",
7 | "url": "git+https://github.com/roxiness/configent.git"
8 | },
9 | "main": "configent.js",
10 | "scripts": {
11 | "build": "tsc && documentation readme configent.js -f md -s API && node ./postBuild",
12 | "test": "cd test && ava"
13 | },
14 | "keywords": [
15 | "config",
16 | "env",
17 | "package.json"
18 | ],
19 | "author": "jakobrosenberg@gmail.com",
20 | "license": "MIT",
21 | "dependencies": {
22 | "dotenv": "^8.2.0"
23 | },
24 | "devDependencies": {
25 | "ava": "^3.13.0",
26 | "documentation": "^13.1.0",
27 | "typescript": "^4.0.3"
28 | },
29 | "ava": {
30 | "files": [
31 | "test/specs/**",
32 | "test/**/*.spec.*"
33 | ]
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/test/basic/test.spec.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | const test = require('ava').default
4 | const { configent } = require('../..')
5 | process.chdir(__dirname)
6 |
7 |
8 | test('no env', async t => {
9 | const configentOptions = {
10 | useConfig: false,
11 | useDotEnv: false,
12 | useEnv: false,
13 | usePackageConfig: false
14 | }
15 | const defaults = { bar: 123, baz: 456 }
16 | const result = configent(defaults, {}, configentOptions)
17 | t.deepEqual(result, defaults)
18 | })
19 |
20 | test('env', async t => {
21 | console.log('cwd2', process.cwd())
22 | process.env['BASIC_fromEnv'] = 'true'
23 | const defaults = {
24 | fromDefaults: true,
25 | fromDotEnv: false,
26 | fromEnv: false,
27 | fromOptions: false,
28 | fromConfig: false,
29 | fromPackageJson: false,
30 | }
31 | const options = { fromOptions: true }
32 | const result = configent(defaults, options)
33 | t.deepEqual(result, {
34 | fromDefaults: true,
35 | fromDotEnv: 'true',
36 | fromEnv: 'true',
37 | fromConfig: true,
38 | fromPackageJson: true,
39 | fromOptions: true,
40 | })
41 | })
42 |
--------------------------------------------------------------------------------
/test/context-configs/context.spec.js:
--------------------------------------------------------------------------------
1 | const test = require('ava').default
2 | const { configent } = require('../..')
3 | process.chdir(__dirname) //change cwd to __dirname
4 |
5 | const defaults = { fromContext: false }
6 | const configentOptions = {
7 | useDetectDefaults: true,
8 | cacheConfig: false,
9 | cacheDetectedDefaults: false
10 | }
11 |
12 | test('if no context is found, defaults are unchanged', async t => {
13 | const result = configent(defaults, {}, configentOptions)
14 | t.deepEqual(result, { fromContext: false })
15 | })
16 |
17 | test('if context is found, it sets defaults', async t => {
18 | process.env.USE_BASIC = "1"
19 | const result = configent(defaults, {}, configentOptions)
20 | t.deepEqual(result, { fromContext: 'basic' })
21 | })
22 |
23 | test('if multiple contexts are found, superseder wins', async t => {
24 | process.env.USE_SUPERSEDER = "1"
25 | const result = configent(defaults, {}, configentOptions)
26 | t.deepEqual(result, { fromContext: 'superseder' })
27 | })
28 |
29 | test('can read package.json from cwd', async t => {
30 | process.env.USE_PKGJSON = "1"
31 | const result = configent(defaults, {}, configentOptions)
32 | t.deepEqual(result, { fromContext: 'usepkgjson' })
33 | })
34 |
35 | test('circular tests create error', async t => {
36 | process.env.USE_CIRCULAR = "1"
37 | const result = configent(defaults, {}, configentOptions)
38 | t.deepEqual(result, { fromContext: 'usepkgjson' })
39 | })
40 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module "configent" {
2 | /**
3 | * @template {Object.} options
4 | * @param {options} defaults default options
5 | * @param {Partial=} input provided input
6 | * @param {object} [configentOptions] configent options
7 | * @param {string=} [configentOptions.name = ''] name to use for configs. If left empty, name from package.json is used
8 | * @param {boolean=} [configentOptions.cacheConfig = true] calling configent twice with same parameters will return the same instance
9 | * @param {boolean=} [configentOptions.cacheDetectedDefaults = true] calling configent twice from the same module will return the same defaults
10 | * @param {boolean=} [configentOptions.useDotEnv = true] include config from .env files
11 | * @param {boolean=} [configentOptions.useEnv = true] include config from process.env
12 | * @param {boolean=} [configentOptions.usePackageConfig = true] include config from package.json
13 | * @param {boolean=} [configentOptions.useConfig = true] include config from [name].config.js
14 | * @param {boolean=} [configentOptions.useDetectDefaults = true] detect defaults from context (package.json and file stucture)
15 | * @param {string=} [configentOptions.detectDefaultsConfigPath = 'configs'] detect defaults from context (package.json and file stucture)
16 | * @param {function=} [configentOptions.sanitizeEnvValue = str => str.replace(/[-_][a-z]/g, str => str.substr(1).toUpperCase())] sanitize environment values. Convert snake_case to camelCase by default.
17 | * @param {NodeModule} [configentOptions.module] required if multiple modules are using configent
18 | * @returns {options}
19 | */
20 | export function configent(defaults: options, input?: Partial, configentOptions?: {
23 | name?: string | undefined;
24 | cacheConfig?: boolean | undefined;
25 | cacheDetectedDefaults?: boolean | undefined;
26 | useDotEnv?: boolean | undefined;
27 | useEnv?: boolean | undefined;
28 | usePackageConfig?: boolean | undefined;
29 | useConfig?: boolean | undefined;
30 | useDetectDefaults?: boolean | undefined;
31 | detectDefaultsConfigPath?: string | undefined;
32 | sanitizeEnvValue?: Function | undefined;
33 | module?: NodeModule;
34 | }): options;
35 | }
36 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 |
5 | # Configent
6 |
7 | ### Confident configurations
8 |
9 | No fuzz config compilation from (ordered by ascending precedence)
10 |
11 | - defaults
12 | - package.json
13 | - [name].config.js
14 | - .env
15 | - environment
16 | - input
17 |
18 | ```javascript
19 | /**
20 | * package.json {"foobar": {"city": "Portsmouth"}}
21 | * foobar.config.js {lastSeen: 'Liverpool'}
22 | * process.env.foobar_last_seen = London
23 | * options = { name: 'Sherlock Holmes' }
24 | */
25 |
26 | const defaults = { name: 'John Doe', city: 'N/A', lastSeen: 'N/A' }
27 |
28 | const config = configent('foobar', defaults, options)
29 |
30 | /**
31 | * console.log(config)
32 | * {
33 | * name: 'Sherlock Holmes',
34 | * city: 'Portsmouth',
35 | * lastSeen: 'London'
36 | * }
37 | * /
38 | ```
39 |
40 | ### Auto detect defaults
41 |
42 | Configent supports multiple default configs. These are added to `./configs`.
43 |
44 | ```javascript
45 | /** ./configs/routify2.config.js */
46 |
47 | module.exports = {
48 | supersedes: ['svelte'],
49 | condition: ({ pkgjson }) => pkgjson.dependencies['@roxi/routify'],
50 | config: () => ({
51 | /** the config object used as default */
52 | myAppName: 'Routify App'
53 | })
54 | }
55 | ```
56 |
57 | ```javascript
58 | /** ./configs/svelte.config.js */
59 |
60 | module.exports = {
61 | condition: ({ pkgjson }) => pkgjson.dependencies['svelte'],
62 | config: () => ({
63 | /** the config object used as default */
64 | myAppName: 'Svelte App'
65 | })
66 | }
67 | ```
68 |
69 | The first config with a true condition is used. To avoid conflicts, configs using the `supersedes` option, will run before their superseded targets.
70 |
71 | To change the location of default configs, refer to `detectDefaultsConfigPath`.
72 |
73 | ### API
74 |
75 |
76 |
77 | ##### Table of Contents
78 |
79 | - [configent](#configent)
80 | - [Parameters](#parameters)
81 |
82 | #### configent
83 |
84 | ##### Parameters
85 |
86 | - `defaults` **options** default options
87 | - `input` **Partial<options>?** provided input (optional, default `{}`)
88 | - `configentOptions` **[object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)?** configent options
89 | - `configentOptions.name` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** name to use for configs. If left empty, name from package.json is used (optional, default `''`)
90 | - `configentOptions.cacheConfig` **[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** calling configent twice with same parameters will return the same instance (optional, default `true`)
91 | - `configentOptions.cacheDetectedDefaults` **[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** calling configent twice from the same module will return the same defaults (optional, default `true`)
92 | - `configentOptions.useDotEnv` **[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** include config from .env files (optional, default `true`)
93 | - `configentOptions.useEnv` **[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** include config from process.env (optional, default `true`)
94 | - `configentOptions.usePackageConfig` **[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** include config from package.json (optional, default `true`)
95 | - `configentOptions.useConfig` **[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** include config from [name].config.js (optional, default `true`)
96 | - `configentOptions.useDetectDefaults` **[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** detect defaults from context (package.json and file stucture) (optional, default `true`)
97 | - `configentOptions.detectDefaultsConfigPath` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** detect defaults from context (package.json and file stucture) (optional, default `'configs'`)
98 | - `configentOptions.sanitizeEnvValue` **[function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** sanitize environment values. Convert snake_case to camelCase by default. (optional, default `str=>str.replace(/[-_][a-z]/g,str=>str.substr(1).toUpperCase())`)
99 | - `configentOptions.module` **NodeModule?** required if multiple modules are using configent
100 |
101 | Returns **options**
102 |
103 | ####
104 |
105 |
106 | ---
107 |
108 | Vintage vector created by macrovector - www.freepik.com
--------------------------------------------------------------------------------
/configent.js:
--------------------------------------------------------------------------------
1 | const { existsSync, readdirSync, readFileSync } = require('fs')
2 | const { resolve, dirname } = require('path')
3 | let instances = {}
4 | let detectedFromDefaults = {}
5 |
6 | const _defaults = {
7 | name: '',
8 | cacheConfig: true,
9 | cacheDetectedDefaults: true,
10 | useDotEnv: true,
11 | useEnv: true,
12 | usePackageConfig: true,
13 | useConfig: true,
14 | useDetectDefaults: false,
15 | detectDefaultsConfigPath: 'configs',
16 | sanitizeEnvValue: str => str.replace(/[-_][a-z]/g, str => str.substr(1).toUpperCase())
17 | }
18 |
19 | /**
20 | * @template {Object.} options
21 | * @param {options} defaults default options
22 | * @param {Partial=} input provided input
23 | * @param {object} [configentOptions] configent options
24 | * @param {string=} [configentOptions.name = ''] name to use for configs. If left empty, name from package.json is used
25 | * @param {boolean=} [configentOptions.cacheConfig = true] calling configent twice with same parameters will return the same instance
26 | * @param {boolean=} [configentOptions.cacheDetectedDefaults = true] calling configent twice from the same module will return the same defaults
27 | * @param {boolean=} [configentOptions.useDotEnv = true] include config from .env files
28 | * @param {boolean=} [configentOptions.useEnv = true] include config from process.env
29 | * @param {boolean=} [configentOptions.usePackageConfig = true] include config from package.json
30 | * @param {boolean=} [configentOptions.useConfig = true] include config from [name].config.js
31 | * @param {boolean=} [configentOptions.useDetectDefaults = true] detect defaults from context (package.json and file stucture)
32 | * @param {string=} [configentOptions.detectDefaultsConfigPath = 'configs'] detect defaults from context (package.json and file stucture)
33 | * @param {function=} [configentOptions.sanitizeEnvValue = str => str.replace(/[-_][a-z]/g, str => str.substr(1).toUpperCase())] sanitize environment values. Convert snake_case to camelCase by default.
34 | * @param {NodeModule} [configentOptions.module] required if multiple modules are using configent
35 | * @returns {options}
36 | */
37 | function configent(defaults, input = {}, configentOptions) {
38 | configentOptions = { ..._defaults, ...configentOptions }
39 | const getParentModuleDir = createGetParentModuleDir(configentOptions)
40 | configentOptions.name = configentOptions.name || require(resolve(getParentModuleDir(), 'package.json')).name
41 |
42 | const {
43 | name,
44 | cacheConfig,
45 | cacheDetectedDefaults,
46 | useDotEnv,
47 | sanitizeEnvValue,
48 | useConfig,
49 | useEnv,
50 | usePackageConfig,
51 | useDetectDefaults,
52 | detectDefaultsConfigPath
53 | } = configentOptions
54 | const upperCaseRE = new RegExp(`^${name.toUpperCase()}_`)
55 |
56 | return buildConfig()
57 |
58 | function buildConfig() {
59 | delete (configentOptions.module)
60 | const hash = JSON.stringify({ name, defaults, input, configentOptions })
61 | if (!instances[hash] || !cacheConfig) {
62 | instances[hash] = {
63 | ...defaults,
64 | ...useDetectDefaults && getDetectDefaults(),
65 | ...usePackageConfig && getPackageConfig(),
66 | ...useConfig && getUserConfig(),
67 | ...useEnv && getEnvConfig(),
68 | ...input
69 | }
70 | }
71 | return instances[hash]
72 | }
73 |
74 | function getEnvConfig() {
75 | let env = process.env
76 | if (useDotEnv) {
77 | try {
78 | const envPath = resolve(process.cwd(), '.env')
79 | const envContents = readFileSync(envPath, 'utf8')
80 | env = {
81 | ...require('dotenv').parse(envContents),
82 | ...env
83 | }
84 | } catch (e) {
85 | // ignore
86 | }
87 | }
88 |
89 | const entries = Object.entries(env)
90 | .filter(([key]) => key.match(upperCaseRE))
91 | .map(parseField)
92 |
93 | if (entries.length)
94 | return entries.reduce((prev, { key, value }) => ({ ...prev, [key]: value }), {})
95 |
96 | function parseField([key, value]) {
97 | const shouldParseValue = k => typeof defaults[k] === 'object'
98 |
99 | key = sanitizeEnvValue(key.replace(upperCaseRE, ''))
100 | value = shouldParseValue(key) ? JSON.parse(value) : value
101 | return { key, value }
102 | }
103 | }
104 |
105 | function getUserConfig() {
106 | const path_js = resolve(process.cwd(), `${name}.config.js`)
107 | const path_cjs = resolve(process.cwd(), `${name}.config.cjs`)
108 |
109 | if (existsSync(path_js)) {
110 | return require(path_js);
111 | } else if (existsSync(path_cjs)) {
112 | return require(path_cjs);
113 | } else {
114 | return {};
115 | }
116 | }
117 |
118 | function getPackageConfig() {
119 | const path = resolve(process.cwd(), 'package.json')
120 | return existsSync(path) && require(path)[name]
121 | }
122 |
123 | function getDetectDefaults() {
124 | const hash = JSON.stringify({ name, path: module['parent'].path })
125 |
126 | // we only want to detect the defaults for any given module once
127 | if (!detectedFromDefaults[hash] || !cacheDetectedDefaults) {
128 | const pkgjson = { dependencies: {}, devDependencies: {} };
129 | if (existsSync('package.json')) {
130 | Object.assign(pkgjson, require(resolve(process.cwd(), 'package.json')));
131 | }
132 |
133 | Object.assign(pkgjson.dependencies, pkgjson.devDependencies)
134 |
135 | const unsortedConfigTemplates = readdirSync(resolve(getParentModuleDir(), detectDefaultsConfigPath))
136 | .map(file => ({
137 | file,
138 | ...require(resolve(getParentModuleDir(), detectDefaultsConfigPath, file))
139 | }))
140 | const configTemplates = sortBySupersedings(unsortedConfigTemplates)
141 | .filter(configTemplate => configTemplate.condition({ pkgjson }))
142 | .reverse()
143 | if (configTemplates) {
144 | if (configTemplates.length > 1) // we don't care about the default template
145 | console.log(`[%s] detected defaults from %s`, name, configTemplates.filter(template => template.file !== 'default.config.js').map(template => template.name).join(', '))
146 | detectedFromDefaults[hash] = Object.assign({}, ...configTemplates.map(template => template.config({ pkgjson })))
147 | }
148 | }
149 | return detectedFromDefaults[hash]
150 | }
151 | }
152 |
153 | module.exports = { configent }
154 |
155 | function sortBySupersedings(arr) {
156 | // clone the array
157 | arr = [...arr]
158 | const sorted = []
159 |
160 | while (arr.length) {
161 | let foundMatch = false
162 | const supersedings = [].concat(...arr.map(entry => entry.supersedes || []))
163 | for (const [index, entry] of arr.entries()) {
164 | const file = entry.file.replace(/\.config\.js/, '')
165 | if (!supersedings.includes(file)) {
166 | sorted.push(...arr.splice(index, 1))
167 | foundMatch = true
168 | break
169 | }
170 | }
171 | // each iteration should find and pluck one match
172 | if (!foundMatch) throw Error('Looks like you have circular supersedings \n' + arr.map(f => `${f.file} supersedes ${f.supersedes}`).join('\n'))
173 | }
174 |
175 | return sorted
176 | }
177 |
178 | function createGetParentModuleDir(options) {
179 | const { module } = options
180 | let parentModuleDir
181 | return () => {
182 | parentModuleDir = parentModuleDir || _getParentModuleDir(module && module.path)
183 | return parentModuleDir
184 | }
185 | }
186 |
187 | // walk through parents till we find a package.json
188 | function _getParentModuleDir(path) {
189 | if (!path) {
190 | const modules = Object.values(require.cache)
191 | /** @ts-ignore */
192 | .filter((m) => m.children.includes(module))
193 | if (modules.length >= 2) missingModuleError(modules)
194 | else path = modules[0].path
195 | }
196 |
197 | return (existsSync(resolve(path, 'package.json'))) ?
198 | path : _getParentModuleDir(dirname(path))
199 | }
200 |
201 | function missingModuleError(modules) {
202 | const paths = modules.map(m => _getParentModuleDir(m.path))
203 | throw new Error([
204 | `if multiple packages are using configent, they all need to provide the module.`,
205 | `Packages using configent: `,
206 | ...paths.map(p => '- ' + p),
207 | `Updating the packages may fix the problem.`, ''
208 | ].join('\n'))
209 | }
--------------------------------------------------------------------------------