├── .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 | configent 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 | } --------------------------------------------------------------------------------