configate 1.0.0 → 1.1.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
@@ -153,14 +154,52 @@ Add this to `.gitignore` to prevent committing local configuration files:
153
154
  **/config/local*
154
155
  ```
155
156
 
157
+ ### Second layer of variant configurations
158
+
159
+ Some applications may need another layer of overrides besides environments (for example: regions, brands, customers etc).
160
+ Pass the `variant` option to `loadConfig` and create files that follow the `*-{variant}` naming convention to scope those overrides.
161
+
162
+ When `variant` is set, Configate looks for these optional files on top of the usual ones:
163
+ - `default-{variant}.ext`
164
+ - `{environment}-{variant}.ext`
165
+ - `local-{variant}.ext`
166
+ - `local-{environment}-{variant}.ext`
167
+
168
+ Example setup:
169
+
170
+ ```text
171
+ config/
172
+ default.ts
173
+ default-customerA.ts
174
+ default-customerB.ts
175
+ production.ts
176
+ production-customerA.ts
177
+ ```
178
+
179
+ ```ts
180
+ // src/config.ts
181
+ import { loadConfig } from 'configate';
182
+
183
+ export const { config } = await loadConfig<AppConfig>({
184
+ environment: 'production',
185
+ variant: 'customerA',
186
+ });
187
+ ```
188
+
189
+ This keeps the base config reusable while still letting you target per-variant overrides without duplicating entire files.
190
+
156
191
  ### Order of loading configuration files
157
192
 
158
193
  ```
159
- default.ext
160
- {environment}.ext
161
- local.ext
162
- local-{environment}.ext
163
- custom-environment-variables.ext
194
+ 1. default.ext
195
+ 2. default-{variant}.ext
196
+ 3. {environment}.ext
197
+ 4. {environment}-{variant}.ext
198
+ 5. local.ext
199
+ 6. local-{variant}.ext
200
+ 7. local-{environment}.ext
201
+ 8. local-{environment}-{variant}.ext
202
+ 9. custom-environment-variables.ext
164
203
  ```
165
204
 
166
205
  ### Using unsecure config
@@ -221,4 +260,3 @@ export const { config } = await loadConfig<TestConfig>({
221
260
  ],
222
261
  });
223
262
  ```
224
-
@@ -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({
@@ -41,5 +65,5 @@ export async function importConfigFiles({ configDir, environment, fileExtensions
41
65
  filePath: `${configDir}/custom-environment-variables`,
42
66
  fileExtensions: fileExtensions.filter((ext) => ext !== 'json'), // JSON not supported because it cannot use `process.env`
43
67
  });
44
- return deepMerge(structuredClone(defaultConfig), structuredClone(envConfig ?? {}), structuredClone(localConfig), structuredClone(localEnvConfig), structuredClone(customEnvVarsConfig));
68
+ return deepMerge(structuredClone(defaultConfig), structuredClone(defaultVariantConfig ?? {}), structuredClone(environmentConfig ?? {}), structuredClone(environmentVariantConfig ?? {}), structuredClone(localConfig), structuredClone(localVariantConfig ?? {}), structuredClone(localEnvConfig), structuredClone(customEnvVarsConfig));
45
69
  }
@@ -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.1.0",
4
4
  "type": "module",
5
5
  "description": "Configuration helper for TypeScript applications",
6
6
  "main": "dist/index.js",