configate 0.1.2 → 0.1.3
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/dist/common.d.ts +7 -0
- package/dist/common.js +3 -0
- package/dist/deepFreezeConfig.d.ts +5 -0
- package/dist/deepFreezeConfig.js +12 -0
- package/dist/deepMerge.d.ts +9 -0
- package/dist/deepMerge.js +31 -0
- package/dist/importConfigFile.d.ts +6 -0
- package/dist/importConfigFile.js +45 -0
- package/dist/importConfigFiles.d.ts +11 -0
- package/dist/importConfigFiles.js +45 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/loadConfig.d.ts +39 -0
- package/dist/loadConfig.js +52 -0
- package/dist/makeSecureDeepProxy.d.ts +6 -0
- package/dist/makeSecureDeepProxy.js +35 -0
- package/package.json +45 -41
package/dist/common.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type DefaultConfig = Record<string, any>;
|
|
2
|
+
export type Environment = 'development' | 'staging' | 'production' | 'test' | string;
|
|
3
|
+
export type FileExtension = 'ts' | 'mts' | 'js' | 'mjs' | 'cjs' | 'json';
|
|
4
|
+
export type DeepPartial<T> = T extends object ? {
|
|
5
|
+
[P in keyof T]?: DeepPartial<T[P]>;
|
|
6
|
+
} : T | undefined;
|
|
7
|
+
export declare function isObject(item: unknown): item is object;
|
package/dist/common.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { isObject } from "./common.js";
|
|
2
|
+
/**
|
|
3
|
+
* Use Object.freeze to make the config object immutable
|
|
4
|
+
*/
|
|
5
|
+
export function deepFreezeConfig(config) {
|
|
6
|
+
for (const key in config) {
|
|
7
|
+
if (isObject(config[key])) {
|
|
8
|
+
config[key] = deepFreezeConfig(config[key]);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return Object.freeze(config);
|
|
12
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type DefaultConfig } from './common.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Custom implementation of deepMerge to avoid using an external dependency
|
|
4
|
+
*
|
|
5
|
+
* - it merges objects deeply
|
|
6
|
+
* - it overrides array, doesn't merge array elements
|
|
7
|
+
* - it ignores properties with `undefined` values
|
|
8
|
+
*/
|
|
9
|
+
export declare function deepMerge<T extends DefaultConfig>(target: T, ...sources: Array<Partial<T>>): T;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { isObject } from "./common.js";
|
|
2
|
+
/**
|
|
3
|
+
* Custom implementation of deepMerge to avoid using an external dependency
|
|
4
|
+
*
|
|
5
|
+
* - it merges objects deeply
|
|
6
|
+
* - it overrides array, doesn't merge array elements
|
|
7
|
+
* - it ignores properties with `undefined` values
|
|
8
|
+
*/
|
|
9
|
+
export function deepMerge(target, ...sources) {
|
|
10
|
+
if (!sources.length) {
|
|
11
|
+
return target;
|
|
12
|
+
}
|
|
13
|
+
const source = sources.shift();
|
|
14
|
+
if (isObject(target) && isObject(source)) {
|
|
15
|
+
for (const key in source) {
|
|
16
|
+
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
17
|
+
if (key === '__proto__' || key === 'constructor') {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const sourceValue = source[key];
|
|
21
|
+
if (isObject(sourceValue) && isObject(target[key])) {
|
|
22
|
+
target[key] = deepMerge(target[key], sourceValue);
|
|
23
|
+
}
|
|
24
|
+
else if (sourceValue !== undefined) {
|
|
25
|
+
target[key] = sourceValue;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return deepMerge(target, ...sources);
|
|
31
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { DefaultConfig, FileExtension } from './common.ts';
|
|
2
|
+
export declare function importConfigFile<Config extends DefaultConfig>({ filePath, fileExtensions, shouldThrowError, }: {
|
|
3
|
+
filePath: string;
|
|
4
|
+
fileExtensions: FileExtension[];
|
|
5
|
+
shouldThrowError?: boolean;
|
|
6
|
+
}): Promise<Config>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import fs from 'node:fs/promises';
|
|
10
|
+
export async function importConfigFile({ filePath, fileExtensions, shouldThrowError = false, }) {
|
|
11
|
+
let selectedExtension;
|
|
12
|
+
for (const ext of fileExtensions) {
|
|
13
|
+
try {
|
|
14
|
+
// Quickly check if the file exists to avoid unnecessary parsing in import()
|
|
15
|
+
await fs.access(`${filePath}.${ext}`, fs.constants.R_OK);
|
|
16
|
+
selectedExtension = ext;
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
catch (_error) { }
|
|
20
|
+
}
|
|
21
|
+
if (!selectedExtension) {
|
|
22
|
+
if (shouldThrowError) {
|
|
23
|
+
throw new Error(`Config file "${filePath}" not found with any extension: ${fileExtensions.join(', ')}`);
|
|
24
|
+
}
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
const importOptions = selectedExtension === 'json' ? { with: { type: 'json' } } : undefined;
|
|
28
|
+
try {
|
|
29
|
+
const configFile = await import(__rewriteRelativeImportExtension(`${filePath}.${selectedExtension}`), importOptions);
|
|
30
|
+
// Named export of `config` variable is preferred over default export
|
|
31
|
+
if (configFile.config) {
|
|
32
|
+
return configFile.config;
|
|
33
|
+
}
|
|
34
|
+
if (configFile.default) {
|
|
35
|
+
return configFile.default;
|
|
36
|
+
}
|
|
37
|
+
throw new Error(`Config file "${filePath}" has no default export and does not have export "config"`);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (shouldThrowError) {
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DefaultConfig, Environment, FileExtension } from './common.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Import multiple config files in defined order and merge them deeply together
|
|
4
|
+
*/
|
|
5
|
+
export declare function importConfigFiles<Config extends DefaultConfig>({ configDir, environment, fileExtensions, }: ImportConfigFilesOptions): Promise<DefaultConfig>;
|
|
6
|
+
type ImportConfigFilesOptions = {
|
|
7
|
+
configDir: string;
|
|
8
|
+
environment?: Environment;
|
|
9
|
+
fileExtensions: FileExtension[];
|
|
10
|
+
};
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { deepMerge } from "./deepMerge.js";
|
|
2
|
+
import { importConfigFile } from "./importConfigFile.js";
|
|
3
|
+
/**
|
|
4
|
+
* Import multiple config files in defined order and merge them deeply together
|
|
5
|
+
*/
|
|
6
|
+
export async function importConfigFiles({ configDir, environment, fileExtensions, }) {
|
|
7
|
+
/** default.ext **/
|
|
8
|
+
const defaultConfig = await importConfigFile({
|
|
9
|
+
filePath: `${configDir}/default`,
|
|
10
|
+
fileExtensions,
|
|
11
|
+
shouldThrowError: true,
|
|
12
|
+
});
|
|
13
|
+
/** {environment}.ext **/
|
|
14
|
+
let envConfig;
|
|
15
|
+
if (environment) {
|
|
16
|
+
try {
|
|
17
|
+
envConfig = await importConfigFile({
|
|
18
|
+
filePath: `${configDir}/${environment}`,
|
|
19
|
+
fileExtensions,
|
|
20
|
+
shouldThrowError: true,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
catch (_error) {
|
|
24
|
+
console.warn(`Environment ${environment} defined but no config file found`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** local.ext **/
|
|
28
|
+
const localConfig = await importConfigFile({
|
|
29
|
+
filePath: `${configDir}/local`,
|
|
30
|
+
fileExtensions,
|
|
31
|
+
});
|
|
32
|
+
/** local-{environment}.ext **/
|
|
33
|
+
const localEnvConfig = environment
|
|
34
|
+
? await importConfigFile({
|
|
35
|
+
filePath: `${configDir}/local-${environment}`,
|
|
36
|
+
fileExtensions,
|
|
37
|
+
})
|
|
38
|
+
: {};
|
|
39
|
+
/** custom-environment-variables.ext **/
|
|
40
|
+
const customEnvVarsConfig = await importConfigFile({
|
|
41
|
+
filePath: `${configDir}/custom-environment-variables`,
|
|
42
|
+
fileExtensions: fileExtensions.filter((ext) => ext !== 'json'), // JSON not supported because it cannot use `process.env`
|
|
43
|
+
});
|
|
44
|
+
return deepMerge(structuredClone(defaultConfig), structuredClone(envConfig ?? {}), structuredClone(localConfig), structuredClone(localEnvConfig), structuredClone(customEnvVarsConfig));
|
|
45
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { loadConfig } from "./loadConfig.js";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { DefaultConfig, Environment, FileExtension } from './common.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Config library to load configuration files based on the environment
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Load config files in order:
|
|
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)
|
|
12
|
+
* - Throw an error if a property is accessed that is not defined in the config
|
|
13
|
+
* - Freeze the config object to prevent modifications
|
|
14
|
+
* - Supported config file formats: TypeScript (.ts, .mts), JavaScript (.js, .mjs), JSON (.json)
|
|
15
|
+
* - Config file can `export default {}` or `export const config: Config = { ... }`.
|
|
16
|
+
* - named export is recommended in TS for type safety of the config object
|
|
17
|
+
*
|
|
18
|
+
* @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
|
+
* @param environment - the environment to load the config for. Default: process.env.NODE_ENV
|
|
20
|
+
* @param fileExtensions - an array of file extensions to use when looking for config files. Configs are loaded in this order. Default: ['ts', 'js']
|
|
21
|
+
* @param throwOnUndefinedProp - throw an error if a property is accessed that is not defined in the config. Default: true
|
|
22
|
+
* @param freezeConfig - freeze the config object to prevent modifications. Default: true
|
|
23
|
+
*
|
|
24
|
+
* @returns a promise that resolves to an object with 2 properties:
|
|
25
|
+
* - `config` - a secure, frozen object with the loaded config. Throws an error if an undefined property is accessed
|
|
26
|
+
* - `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
|
+
*/
|
|
28
|
+
export declare function loadConfig<Config extends DefaultConfig>({ configDirs, environment, fileExtensions, throwOnUndefinedProp, freezeConfig, }?: LoadConfigOptions): Promise<{
|
|
29
|
+
config: Config;
|
|
30
|
+
unsecureConfig: Config;
|
|
31
|
+
}>;
|
|
32
|
+
type LoadConfigOptions = {
|
|
33
|
+
configDirs?: string[];
|
|
34
|
+
environment?: Environment;
|
|
35
|
+
fileExtensions?: FileExtension[];
|
|
36
|
+
throwOnUndefinedProp?: boolean;
|
|
37
|
+
freezeConfig?: boolean;
|
|
38
|
+
};
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { deepFreezeConfig } from "./deepFreezeConfig.js";
|
|
2
|
+
import { deepMerge } from "./deepMerge.js";
|
|
3
|
+
import { importConfigFiles } from "./importConfigFiles.js";
|
|
4
|
+
import { makeSecureDeepProxy } from "./makeSecureDeepProxy.js";
|
|
5
|
+
/**
|
|
6
|
+
* Config library to load configuration files based on the environment
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Load config files in order:
|
|
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)
|
|
15
|
+
* - Throw an error if a property is accessed that is not defined in the config
|
|
16
|
+
* - Freeze the config object to prevent modifications
|
|
17
|
+
* - Supported config file formats: TypeScript (.ts, .mts), JavaScript (.js, .mjs), JSON (.json)
|
|
18
|
+
* - Config file can `export default {}` or `export const config: Config = { ... }`.
|
|
19
|
+
* - named export is recommended in TS for type safety of the config object
|
|
20
|
+
*
|
|
21
|
+
* @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
|
+
* @param environment - the environment to load the config for. Default: process.env.NODE_ENV
|
|
23
|
+
* @param fileExtensions - an array of file extensions to use when looking for config files. Configs are loaded in this order. Default: ['ts', 'js']
|
|
24
|
+
* @param throwOnUndefinedProp - throw an error if a property is accessed that is not defined in the config. Default: true
|
|
25
|
+
* @param freezeConfig - freeze the config object to prevent modifications. Default: true
|
|
26
|
+
*
|
|
27
|
+
* @returns a promise that resolves to an object with 2 properties:
|
|
28
|
+
* - `config` - a secure, frozen object with the loaded config. Throws an error if an undefined property is accessed
|
|
29
|
+
* - `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
|
+
*/
|
|
31
|
+
export async function loadConfig({ configDirs = [`${process.cwd()}/config`], environment = process.env.NODE_ENV, fileExtensions = ['ts', 'js'], throwOnUndefinedProp = true, freezeConfig = true, } = {}) {
|
|
32
|
+
let mergedConfig = {};
|
|
33
|
+
for (const configDir of configDirs) {
|
|
34
|
+
const config = await importConfigFiles({
|
|
35
|
+
configDir,
|
|
36
|
+
environment,
|
|
37
|
+
fileExtensions,
|
|
38
|
+
});
|
|
39
|
+
mergedConfig = deepMerge(mergedConfig, config);
|
|
40
|
+
}
|
|
41
|
+
const unsecureConfig = structuredClone(mergedConfig);
|
|
42
|
+
if (throwOnUndefinedProp) {
|
|
43
|
+
mergedConfig = makeSecureDeepProxy(mergedConfig);
|
|
44
|
+
}
|
|
45
|
+
if (freezeConfig) {
|
|
46
|
+
mergedConfig = deepFreezeConfig(mergedConfig);
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
config: mergedConfig,
|
|
50
|
+
unsecureConfig: unsecureConfig,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type DefaultConfig } from './common.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Use Proxy to add custom behavior when accessing properties of the config object
|
|
4
|
+
* Throw an error if the property is not defined in the config
|
|
5
|
+
*/
|
|
6
|
+
export declare function makeSecureDeepProxy<T extends DefaultConfig>(config: T): T;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { isObject } from "./common.js";
|
|
2
|
+
/**
|
|
3
|
+
* Use Proxy to add custom behavior when accessing properties of the config object
|
|
4
|
+
* Throw an error if the property is not defined in the config
|
|
5
|
+
*/
|
|
6
|
+
export function makeSecureDeepProxy(config) {
|
|
7
|
+
for (const prop in config) {
|
|
8
|
+
if (isObject(config[prop])) {
|
|
9
|
+
// If it's an object, recursively wrap it
|
|
10
|
+
config[prop] = makeSecureDeepProxy(config[prop]);
|
|
11
|
+
}
|
|
12
|
+
else if (Array.isArray(config[prop])) {
|
|
13
|
+
// If it's an array, wrap each element and the array itself
|
|
14
|
+
config[prop] = makeSecureProxy(config[prop].map((item) => isObject(item) || Array.isArray(item)
|
|
15
|
+
? makeSecureDeepProxy(item)
|
|
16
|
+
: item));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return makeSecureProxy(config);
|
|
20
|
+
}
|
|
21
|
+
function makeSecureProxy(targetObject) {
|
|
22
|
+
return new Proxy(targetObject, {
|
|
23
|
+
get(target, prop) {
|
|
24
|
+
// Handle Symbol.toStringTag and Array properties
|
|
25
|
+
if (prop === Symbol.toStringTag ||
|
|
26
|
+
(Array.isArray(target) && Number.isNaN(Number(prop)))) {
|
|
27
|
+
return Reflect.get(target, prop);
|
|
28
|
+
}
|
|
29
|
+
if (!(prop in target)) {
|
|
30
|
+
throw new Error(`Property ${String(prop)} is not defined in the config`);
|
|
31
|
+
}
|
|
32
|
+
return Reflect.get(target, prop);
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
package/package.json
CHANGED
|
@@ -1,43 +1,47 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
2
|
+
"name": "configate",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Configuration helper for TypeScript applications",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"module": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"default": "./dist/index.js",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"ci": "node --run test && npm run typecheck && node --run lint",
|
|
19
|
+
"lint": "biome check",
|
|
20
|
+
"test": "node --test-isolation=none --test-reporter=spec --experimental-test-coverage --test-coverage-exclude='src/{testConfigDirs/**/*,**/*.test.ts}' --test 'src/**/*.test.ts'",
|
|
21
|
+
"typecheck": "tsc --noEmit"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">= 20"
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/mdrobny/configate.git"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"config"
|
|
35
|
+
],
|
|
36
|
+
"author": "Michal Drobniak",
|
|
37
|
+
"license": "Apache-2.0",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/mdrobny/configate/issues"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/mdrobny/configate#readme",
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@biomejs/biome": "1.9.4",
|
|
44
|
+
"@types/node": "^22.10.6",
|
|
45
|
+
"typescript": "^5.7.3"
|
|
46
|
+
}
|
|
43
47
|
}
|