configate 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -45,6 +45,7 @@ Use the `loadConfig` function once, to load and merge configurations from specif
45
45
  With parameters you can define:
46
46
  - `configDirs`: (Default: `['${current-working-directory}/config']`) An array of directories to load configurations from
47
47
  - `environment`: (Default: `process.env.NODE_ENV`) The environment to load specific configurations for
48
+ - `variant`: (Default: `undefined`) Allows to define 2nd dimension of configs above environment. Will load configs matching `*-{variant}` pattern
48
49
  - `fileExtensions`: (Default: `['ts', 'js']`) An array of file extensions to load configurations from
49
50
  - `throwOnUndefinedProp`: (Default: `true`) If true, throws an error when accessing undefined properties
50
51
  - `freezeConfig`: (Default: `true`) If true, freezes the configuration object to make it immutable
@@ -123,8 +124,10 @@ Do the same for other environments if you need to override more properties for t
123
124
  When you need to define secrets in configuration, they should be defined via environment variables.
124
125
 
125
126
  Create `custom-environment-variables.ts` file in `config` directory and export a `config` object.
127
+
126
128
  - use `process.env` to read environment variables and assign to property in your configuration
127
- - this file will be merged with other configs as last one so environment variables always have priority
129
+ - this file is merged last within its directory, so its defined values override other files in that directory
130
+ - when loading multiple directories, later directories can override these values.
128
131
 
129
132
  ```ts
130
133
  // config/custom-environment-variables.ts
@@ -153,14 +156,54 @@ Add this to `.gitignore` to prevent committing local configuration files:
153
156
  **/config/local*
154
157
  ```
155
158
 
159
+ ### Second layer of variant configurations
160
+
161
+ Some applications may need another layer of overrides besides environments (for example: regions, brands, customers etc).
162
+ Pass the `variant` option to `loadConfig` and create files that follow the `*-{variant}` naming convention to scope those overrides.
163
+
164
+ When `variant` is set, Configate looks for these optional files on top of the usual ones:
165
+ - `default-{variant}.ext`
166
+ - `{environment}-{variant}.ext`
167
+ - `local-{variant}.ext`
168
+ - `local-{environment}-{variant}.ext`
169
+
170
+ Example setup:
171
+
172
+ ```text
173
+ config/
174
+ default.ts
175
+ default-customerA.ts
176
+ default-customerB.ts
177
+ production.ts
178
+ production-customerA.ts
179
+ ```
180
+
181
+ ```ts
182
+ // src/config.ts
183
+ import { loadConfig } from 'configate';
184
+
185
+ export const { config } = await loadConfig<AppConfig>({
186
+ environment: 'production',
187
+ variant: 'customerA',
188
+ });
189
+ ```
190
+
191
+ This keeps the base config reusable while still letting you target per-variant overrides without duplicating entire files.
192
+
156
193
  ### Order of loading configuration files
157
194
 
195
+ The following order applies separately within each configuration directory.
196
+
158
197
  ```
159
- default.ext
160
- {environment}.ext
161
- local.ext
162
- local-{environment}.ext
163
- custom-environment-variables.ext
198
+ 1. default.ext
199
+ 2. default-{variant}.ext
200
+ 3. {environment}.ext
201
+ 4. {environment}-{variant}.ext
202
+ 5. local.ext
203
+ 6. local-{variant}.ext
204
+ 7. local-{environment}.ext
205
+ 8. local-{environment}-{variant}.ext
206
+ 9. custom-environment-variables.ext
164
207
  ```
165
208
 
166
209
  ### Using unsecure config
@@ -211,6 +254,11 @@ const host = config.database.host;
211
254
 
212
255
  #### Loading configurations from multiple directories
213
256
 
257
+ Directories are fully loaded and merged in `configDirs` order.
258
+ Values from later directories override matching properties from earlier directories.
259
+
260
+ For example, a value in the second directory's `default.ts` can override the same property from the first directory's `custom-environment-variables.ts`.
261
+
214
262
  ```ts
215
263
  import { loadConfig } from 'configate';
216
264
 
@@ -221,4 +269,3 @@ export const { config } = await loadConfig<TestConfig>({
221
269
  ],
222
270
  });
223
271
  ```
224
-
@@ -4,7 +4,7 @@ import { isObject } from "./common.js";
4
4
  */
5
5
  export function deepFreezeConfig(config) {
6
6
  for (const key in config) {
7
- if (isObject(config[key])) {
7
+ if (isObject(config[key]) || Array.isArray(config[key])) {
8
8
  config[key] = deepFreezeConfig(config[key]);
9
9
  }
10
10
  }
@@ -2,10 +2,11 @@ import type { DefaultConfig, Environment, FileExtension } from './common.ts';
2
2
  /**
3
3
  * Import multiple config files in defined order and merge them deeply together
4
4
  */
5
- export declare function importConfigFiles<Config extends DefaultConfig>({ configDir, environment, fileExtensions, }: ImportConfigFilesOptions): Promise<DefaultConfig>;
5
+ export declare function importConfigFiles<Config extends DefaultConfig>({ configDir, environment, variant, fileExtensions, }: ImportConfigFilesOptions): Promise<DefaultConfig>;
6
6
  type ImportConfigFilesOptions = {
7
7
  configDir: string;
8
8
  environment?: Environment;
9
+ variant?: string;
9
10
  fileExtensions: FileExtension[];
10
11
  };
11
12
  export {};
@@ -3,18 +3,27 @@ import { importConfigFile } from "./importConfigFile.js";
3
3
  /**
4
4
  * Import multiple config files in defined order and merge them deeply together
5
5
  */
6
- export async function importConfigFiles({ configDir, environment, fileExtensions, }) {
6
+ export async function importConfigFiles({ configDir, environment, variant, fileExtensions, }) {
7
7
  /** default.ext **/
8
8
  const defaultConfig = await importConfigFile({
9
9
  filePath: `${configDir}/default`,
10
10
  fileExtensions,
11
11
  shouldThrowError: true,
12
12
  });
13
+ /** default-{variant}.ext **/
14
+ let defaultVariantConfig;
15
+ if (variant) {
16
+ defaultVariantConfig = await importConfigFile({
17
+ filePath: `${configDir}/default-${variant}`,
18
+ fileExtensions,
19
+ });
20
+ }
13
21
  /** {environment}.ext **/
14
- let envConfig;
22
+ let environmentConfig;
23
+ let environmentVariantConfig;
15
24
  if (environment) {
16
25
  try {
17
- envConfig = await importConfigFile({
26
+ environmentConfig = await importConfigFile({
18
27
  filePath: `${configDir}/${environment}`,
19
28
  fileExtensions,
20
29
  shouldThrowError: true,
@@ -23,12 +32,27 @@ export async function importConfigFiles({ configDir, environment, fileExtensions
23
32
  catch (_error) {
24
33
  console.warn(`Environment ${environment} defined but no config file found`);
25
34
  }
35
+ /** {environment}-{variant}.ext **/
36
+ if (variant) {
37
+ environmentVariantConfig = await importConfigFile({
38
+ filePath: `${configDir}/${environment}-${variant}`,
39
+ fileExtensions,
40
+ });
41
+ }
26
42
  }
27
43
  /** local.ext **/
28
44
  const localConfig = await importConfigFile({
29
45
  filePath: `${configDir}/local`,
30
46
  fileExtensions,
31
47
  });
48
+ /** local-{variant}.ext **/
49
+ let localVariantConfig;
50
+ if (variant) {
51
+ localVariantConfig = await importConfigFile({
52
+ filePath: `${configDir}/local-${variant}`,
53
+ fileExtensions,
54
+ });
55
+ }
32
56
  /** local-{environment}.ext **/
33
57
  const localEnvConfig = environment
34
58
  ? await importConfigFile({
@@ -36,10 +60,17 @@ export async function importConfigFiles({ configDir, environment, fileExtensions
36
60
  fileExtensions,
37
61
  })
38
62
  : {};
63
+ /** local-{environment}-{variant}.ext **/
64
+ const localEnvVariantConfig = environment && variant
65
+ ? await importConfigFile({
66
+ filePath: `${configDir}/local-${environment}-${variant}`,
67
+ fileExtensions,
68
+ })
69
+ : {};
39
70
  /** custom-environment-variables.ext **/
40
71
  const customEnvVarsConfig = await importConfigFile({
41
72
  filePath: `${configDir}/custom-environment-variables`,
42
73
  fileExtensions: fileExtensions.filter((ext) => ext !== 'json'), // JSON not supported because it cannot use `process.env`
43
74
  });
44
- return deepMerge(structuredClone(defaultConfig), structuredClone(envConfig ?? {}), structuredClone(localConfig), structuredClone(localEnvConfig), structuredClone(customEnvVarsConfig));
75
+ return deepMerge(structuredClone(defaultConfig), structuredClone(defaultVariantConfig ?? {}), structuredClone(environmentConfig ?? {}), structuredClone(environmentVariantConfig ?? {}), structuredClone(localConfig), structuredClone(localVariantConfig ?? {}), structuredClone(localEnvConfig), structuredClone(localEnvVariantConfig), structuredClone(customEnvVarsConfig));
45
76
  }
@@ -5,10 +5,14 @@ import type { DefaultConfig, Environment, FileExtension } from './common.ts';
5
5
  * Features:
6
6
  * - Load config files in order:
7
7
  * 1. default.ext
8
- * 2. {environment}.ext
9
- * 4. local.ext
10
- * 3. local-{environment}.ext
11
- * 5. custom-environment-variables.ext (only for .ts and .js files)
8
+ * 2. default-{variant}.ext
9
+ * 3. {environment}.ext
10
+ * 4. {environment}-{variant}.ext
11
+ * 5. local.ext
12
+ * 6. local-{variant}.ext
13
+ * 7. local-{environment}.ext
14
+ * 8. local-{environment}-{variant}.ext
15
+ * 9. custom-environment-variables.ext (only for .ts and .js files)
12
16
  * - Throw an error if a property is accessed that is not defined in the config
13
17
  * - Freeze the config object to prevent modifications
14
18
  * - Supported config file formats: TypeScript (.ts, .mts), JavaScript (.js, .mjs), JSON (.json)
@@ -17,6 +21,7 @@ import type { DefaultConfig, Environment, FileExtension } from './common.ts';
17
21
  *
18
22
  * @param configDirs - paths to directories where the config files are located. Relative or absolute. Directories are merged in the array order. Default: ['${current-working-directory}/config']
19
23
  * @param environment - the environment to load the config for. Default: process.env.NODE_ENV
24
+ * @param variant - allows to define 2nd dimension of configs above environment. Default: undefined
20
25
  * @param fileExtensions - an array of file extensions to use when looking for config files. Configs are loaded in this order. Default: ['ts', 'js']
21
26
  * @param throwOnUndefinedProp - throw an error if a property is accessed that is not defined in the config. Default: true
22
27
  * @param freezeConfig - freeze the config object to prevent modifications. Default: true
@@ -25,13 +30,14 @@ import type { DefaultConfig, Environment, FileExtension } from './common.ts';
25
30
  * - `config` - a secure, frozen object with the loaded config. Throws an error if an undefined property is accessed
26
31
  * - `unsecureConfig` - an unsecure config. May be needed if config is passed directly to some module that wants to read undefined props or modify it
27
32
  */
28
- export declare function loadConfig<Config extends DefaultConfig>({ configDirs, environment, fileExtensions, throwOnUndefinedProp, freezeConfig, }?: LoadConfigOptions): Promise<{
33
+ export declare function loadConfig<Config extends DefaultConfig>({ configDirs, environment, variant, fileExtensions, throwOnUndefinedProp, freezeConfig, }?: LoadConfigOptions): Promise<{
29
34
  config: Config;
30
35
  unsecureConfig: Config;
31
36
  }>;
32
37
  type LoadConfigOptions = {
33
38
  configDirs?: string[];
34
39
  environment?: Environment;
40
+ variant?: string;
35
41
  fileExtensions?: FileExtension[];
36
42
  throwOnUndefinedProp?: boolean;
37
43
  freezeConfig?: boolean;
@@ -8,10 +8,14 @@ import { makeSecureDeepProxy } from "./makeSecureDeepProxy.js";
8
8
  * Features:
9
9
  * - Load config files in order:
10
10
  * 1. default.ext
11
- * 2. {environment}.ext
12
- * 4. local.ext
13
- * 3. local-{environment}.ext
14
- * 5. custom-environment-variables.ext (only for .ts and .js files)
11
+ * 2. default-{variant}.ext
12
+ * 3. {environment}.ext
13
+ * 4. {environment}-{variant}.ext
14
+ * 5. local.ext
15
+ * 6. local-{variant}.ext
16
+ * 7. local-{environment}.ext
17
+ * 8. local-{environment}-{variant}.ext
18
+ * 9. custom-environment-variables.ext (only for .ts and .js files)
15
19
  * - Throw an error if a property is accessed that is not defined in the config
16
20
  * - Freeze the config object to prevent modifications
17
21
  * - Supported config file formats: TypeScript (.ts, .mts), JavaScript (.js, .mjs), JSON (.json)
@@ -20,6 +24,7 @@ import { makeSecureDeepProxy } from "./makeSecureDeepProxy.js";
20
24
  *
21
25
  * @param configDirs - paths to directories where the config files are located. Relative or absolute. Directories are merged in the array order. Default: ['${current-working-directory}/config']
22
26
  * @param environment - the environment to load the config for. Default: process.env.NODE_ENV
27
+ * @param variant - allows to define 2nd dimension of configs above environment. Default: undefined
23
28
  * @param fileExtensions - an array of file extensions to use when looking for config files. Configs are loaded in this order. Default: ['ts', 'js']
24
29
  * @param throwOnUndefinedProp - throw an error if a property is accessed that is not defined in the config. Default: true
25
30
  * @param freezeConfig - freeze the config object to prevent modifications. Default: true
@@ -28,12 +33,13 @@ import { makeSecureDeepProxy } from "./makeSecureDeepProxy.js";
28
33
  * - `config` - a secure, frozen object with the loaded config. Throws an error if an undefined property is accessed
29
34
  * - `unsecureConfig` - an unsecure config. May be needed if config is passed directly to some module that wants to read undefined props or modify it
30
35
  */
31
- export async function loadConfig({ configDirs = [`${process.cwd()}/config`], environment = process.env.NODE_ENV, fileExtensions = ['ts', 'js'], throwOnUndefinedProp = true, freezeConfig = true, } = {}) {
36
+ export async function loadConfig({ configDirs = [`${process.cwd()}/config`], environment = process.env.NODE_ENV, variant, fileExtensions = ['ts', 'js'], throwOnUndefinedProp = true, freezeConfig = true, } = {}) {
32
37
  let mergedConfig = {};
33
38
  for (const configDir of configDirs) {
34
39
  const config = await importConfigFiles({
35
40
  configDir,
36
41
  environment,
42
+ variant,
37
43
  fileExtensions,
38
44
  });
39
45
  mergedConfig = deepMerge(mergedConfig, config);
@@ -23,8 +23,10 @@ function makeSecureProxy(targetObject) {
23
23
  get(target, prop) {
24
24
  // Handle Symbol and Array properties
25
25
  if (typeof prop === 'symbol' ||
26
- // Don't throw error when using array methods
27
- (Array.isArray(target) && Number.isNaN(Number(prop)))) {
26
+ /** Don't throw error when using array methods */
27
+ (Array.isArray(target) && Number.isNaN(Number(prop))) ||
28
+ /** Allow stringifying proxied config (because JSON.stringify calls this method) */
29
+ prop === 'toJSON') {
28
30
  return Reflect.get(target, prop);
29
31
  }
30
32
  if (!(prop in target)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "configate",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "description": "Configuration helper for TypeScript applications",
6
6
  "main": "dist/index.js",
@@ -30,6 +30,10 @@
30
30
  "type": "git",
31
31
  "url": "git+https://github.com/mdrobny/configate.git"
32
32
  },
33
+ "publishConfig": {
34
+ "registry": "https://registry.npmjs.org",
35
+ "provenance": true
36
+ },
33
37
  "keywords": [
34
38
  "config"
35
39
  ],
@@ -40,8 +44,18 @@
40
44
  },
41
45
  "homepage": "https://github.com/mdrobny/configate#readme",
42
46
  "devDependencies": {
43
- "@biomejs/biome": "2.3.8",
47
+ "@biomejs/biome": "2.5.13",
44
48
  "@types/node": "^22.10.6",
45
49
  "typescript": "^5.7.3"
50
+ },
51
+ "devEngines": {
52
+ "runtime": {
53
+ "name": "node",
54
+ "onFail": "error"
55
+ },
56
+ "packageManager": {
57
+ "name": "npm",
58
+ "onFail": "error"
59
+ }
46
60
  }
47
61
  }