@keeex/projectconfig 5.0.1 → 6.0.1

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
@@ -1,12 +1,16 @@
1
1
  # Project configuration loader
2
2
 
3
- Load project for a project from the following places, whichever works first:
3
+ Load project for a project from the following places, later levels taking precedence.
4
+ Each of the following "places" are loaded and merged with the previous one.
4
5
 
6
+ - From default values provided programatically
7
+ - From a file at the root of the project, or in the parent directory of the root of the project
8
+ - From environment variables
5
9
  - Command line argument, as a JSON string
6
10
  - Command line argument, as a path to a JSON/JS file
7
- - From a file at the root of the project, or in the parent directory of the root of the project
8
11
 
9
- Additionaly, it will automatically sideload a "secrets" file, for settings that should not remain in a shared configuration file.
12
+ In addition, it will automatically sideload a "secrets" file, for settings that should not remain in a shared configuration file.
13
+ Loading secrets file follows the same rules as the general config.
10
14
 
11
15
  ## Usage
12
16
 
@@ -32,6 +36,26 @@ The `KEEEX_CONFIG_SUFFIX` option takes precedence over `NODE_ENV`.
32
36
 
33
37
  In any case, the default names (without any suffix) will be used as fallback.
34
38
 
39
+ ### Environment variables
40
+
41
+ Some properties can be provided using environment variable.
42
+ Three types of values are supported:
43
+
44
+ - `string`: provides the string as-is
45
+ - `boolean`: parse true-like values as `true` ("1", "true", "TRUE", "on", "ON", "enable", "ENABLED")
46
+ - `number`: any value parsed by `Number.parseFloat(value)`
47
+ - `JSON`: takes a string and passes it as-is to `JSON.parse()`
48
+ - Arrays: there is no specific option for arrays, use `JSON`. Note that values of different levels overwrite the previous, so if a value is set in the config file then defined in the environment variable, it will be replaced.
49
+
50
+ The program must provide a list of fields to load, and environment variable names will be derived from there.
51
+
52
+ Fields are provided as JSON path, from the root of the configuration (or the root of the secret).
53
+ The environment variable key is build by converting the path to capital case and replacing the dot with underscores.
54
+ It is then prefixed with `KEEEX_CFG_${name}` (or `KEEEX_SECRET_${name}` for secrets), which is used as the base name below.
55
+
56
+ For example, assuming a property `config.paths.storage` that is a string, the config JSON path is `paths.storage` and the environment key basename is `KEEEX_CFG_PATHS_STORAGE`.
57
+ The value can be provided either by setting `KEEEX_CFG_PATHS_STORAGE`, or by putting the path to a file in `KEEEX_CFG_PATHS_STORAGE__FILE` which will be loaded as a string (stripping whitespaces at the end).
58
+
35
59
  ### Command-line configuration
36
60
 
37
61
  It is possible to load a different configuration from command line.
@@ -41,6 +65,11 @@ One can also pass `--config <path to json file>` to load a different config from
41
65
 
42
66
  The same can be done for secrets with `--secrets`.
43
67
 
68
+ Specific values can be set if they're configured to be available through environment variables.
69
+
70
+ In the above example, `config.paths.storage` could be set using `--config.paths.storage <value>` or `--config.paths.storage-file <file path>`.
71
+ For secrets, the argument would be `--secrets.db.user` for `config.secrets.db.user`.
72
+
44
73
  ### Accessing the configuration
45
74
 
46
75
  The loaded configuration can be obtained by calling the function `loadConfig()`.
@@ -49,14 +78,26 @@ The loaded configuration can be obtained by calling the function `loadConfig()`.
49
78
  import {makeProfilePredicate} from "@keeex/utils/types/record.js";
50
79
  import * as projectConfig from "@keeex/projectconfig";
51
80
 
81
+ interface Paths {
82
+ importantDir: string;
83
+ webRoot: string;
84
+ }
85
+
86
+ const isPaths = makeProfilePredicate<Paths>({
87
+ importantDir: "string",
88
+ webRoot: "string",
89
+ });
90
+
52
91
  interface Config {
53
- port: number;
54
92
  debug: boolean;
93
+ paths: Paths;
94
+ port: number;
55
95
  }
56
96
 
57
97
  const isConfig = makeProfilePredicate<Config>({
58
- port: "number";
59
- debug: "boolean";
98
+ debug: "boolean",
99
+ paths: isPaths,
100
+ port: "number",
60
101
  });
61
102
 
62
103
  interface Secrets {
@@ -71,6 +112,17 @@ const isSecrets = makeProfilePredicate<Secrets>({
71
112
 
72
113
  const getConfig = (): Promise<projectConfig.ProjectConfig<Config, Secrets>> =>
73
114
  projectConfig.loadConfig({
115
+ configDefault: {
116
+ port: 3000,
117
+ debug: false,
118
+ paths: {webRoot: "./dist"},
119
+ },
120
+ configEnvironmentVars: {
121
+ "port": projectConfig.TypeForEnv.number,
122
+ "debug": projectConfig.TypeForEnv.boolean,
123
+ "paths.importantDir": projectConfig.TypeForEnv.string,
124
+ "paths.webRoot": projectConfig.TypesForEnv.string,
125
+ },
74
126
  configPredicate: isConfig,
75
127
  secretsPredicate: isSecrets,
76
128
  });
@@ -89,6 +141,8 @@ const config = await getConfig();
89
141
  console.log(`
90
142
  Port: ${config.port}
91
143
  Debug: ${config.debug}
144
+ ImportantDir: ${config.paths.importantDir}
145
+ WebRoot: ${config.paths.webRoot}
92
146
  User: ${config.secrets.login}
93
147
  Password: ${"*".repeat(config.secrets.password.length)}
94
148
  `);
package/lib/index.d.ts CHANGED
@@ -25,5 +25,5 @@
25
25
  * <mailto: contact@keeex.net>
26
26
  *
27
27
  */
28
- export { loadConfig } from "./loader.js";
29
- export type { ProjectConfig } from "./types.js";
28
+ export { loadConfig } from "./services/loader.js";
29
+ export type { ProjectConfig, TypeForEnv } from "./services/types.js";
package/lib/index.js CHANGED
@@ -25,4 +25,4 @@
25
25
  * <mailto: contact@keeex.net>
26
26
  *
27
27
  */
28
- export { loadConfig } from "./loader.js";
28
+ export { loadConfig } from "./services/loader.js";
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @license
3
+ * @preserve
4
+ *
5
+ * KeeeX SAS Public code
6
+ * https://keeex.me
7
+ * Copyright 2013-2026 KeeeX All Rights Reserved.
8
+ *
9
+ * These computer program listings and specifications, herein,
10
+ * are and remain the property of KeeeX SAS. The intellectual
11
+ * and technical concepts herein are proprietary to KeeeX SAS
12
+ * and may be covered by EU and foreign patents,
13
+ * patents in process, trade secrets and copyright law.
14
+ *
15
+ * These listings are published as a way to provide third party
16
+ * with the ability to process KeeeX data.
17
+ * As such, support for public inquiries is limited.
18
+ * They are provided "as-is", without warrany of any kind.
19
+ *
20
+ * They shall not be reproduced or copied or used in whole or
21
+ * in part as the basis for manufacture or sale of items unless
22
+ * prior written permission is obtained from KeeeX SAS.
23
+ *
24
+ * For a license agreement, please contact:
25
+ * <mailto: contact@keeex.net>
26
+ *
27
+ */
28
+ import type * as types from "./types.js";
29
+ import type { JSONObject } from "@keeex/utils/json.js";
30
+ /**
31
+ * Get config from the CLI.
32
+ *
33
+ * Check if `--{config,secret}` is present, and if so, tries to interpret it as JSON or as a file path to load.
34
+ *
35
+ * @returns
36
+ * The loaded content, or `null` if the argument is not present.
37
+ */
38
+ export declare const getFromCliBase: (argv: Array<string>, baseDataConfig: types.BaseDataConfig<unknown>) => Promise<JSONObject | null>;
39
+ /** Get a config value from CLI args */
40
+ export declare const getCfgValue: (argv: Array<string>, argType: types.ArgType | string, valuePath: string, file: boolean) => string | undefined;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * @license
3
+ * @preserve
4
+ *
5
+ * KeeeX SAS Public code
6
+ * https://keeex.me
7
+ * Copyright 2013-2026 KeeeX All Rights Reserved.
8
+ *
9
+ * These computer program listings and specifications, herein,
10
+ * are and remain the property of KeeeX SAS. The intellectual
11
+ * and technical concepts herein are proprietary to KeeeX SAS
12
+ * and may be covered by EU and foreign patents,
13
+ * patents in process, trade secrets and copyright law.
14
+ *
15
+ * These listings are published as a way to provide third party
16
+ * with the ability to process KeeeX data.
17
+ * As such, support for public inquiries is limited.
18
+ * They are provided "as-is", without warrany of any kind.
19
+ *
20
+ * They shall not be reproduced or copied or used in whole or
21
+ * in part as the basis for manufacture or sale of items unless
22
+ * prior written permission is obtained from KeeeX SAS.
23
+ *
24
+ * For a license agreement, please contact:
25
+ * <mailto: contact@keeex.net>
26
+ *
27
+ */
28
+ import * as nodeFs from "node:fs";
29
+ import { consumeArgv } from "@keeex/utils-node/argv.js";
30
+ import { getArgName } from "./consts.js";
31
+ /**
32
+ * Get config from the CLI.
33
+ *
34
+ * Check if `--{config,secret}` is present, and if so, tries to interpret it as JSON or as a file path to load.
35
+ *
36
+ * @returns
37
+ * The loaded content, or `null` if the argument is not present.
38
+ */
39
+ export const getFromCliBase = async (argv, baseDataConfig) => {
40
+ const argName = getArgName(baseDataConfig.argType);
41
+ const configValue = consumeArgv(argv, argName, true);
42
+ if (configValue === undefined) {
43
+ baseDataConfig.loadedFrom.push(`CLI: no --${argName}`);
44
+ return null;
45
+ }
46
+ if (configValue.startsWith("{"))
47
+ return JSON.parse(configValue);
48
+ return JSON.parse(await nodeFs.promises.readFile(configValue, "utf8"));
49
+ };
50
+ /** Get a config value from CLI args */
51
+ export const getCfgValue = (argv, argType, valuePath, file) => {
52
+ const baseKey = `${getArgName(argType)}.${valuePath}`;
53
+ const key = file ? `${baseKey}-file` : baseKey;
54
+ const cliValue = consumeArgv(argv, key, true);
55
+ return cliValue;
56
+ };
@@ -25,4 +25,5 @@
25
25
  * <mailto: contact@keeex.net>
26
26
  *
27
27
  */
28
- export {};
28
+ import { ArgType } from "./types.js";
29
+ export declare const getArgName: (argType: ArgType | string) => string;
@@ -25,7 +25,14 @@
25
25
  * <mailto: contact@keeex.net>
26
26
  *
27
27
  */
28
- /** Return the config suffix from env value */
29
- export declare const getConfigSuffix: () => string | undefined;
30
- /** Return the current NODE_ENV value (default to `production`) */
31
- export declare const getNodeEnv: () => string;
28
+ import { ArgType } from "./types.js";
29
+ export const getArgName = (argType) => {
30
+ switch (argType) {
31
+ case ArgType.config:
32
+ return "config";
33
+ case ArgType.secrets:
34
+ return "secrets";
35
+ default:
36
+ return argType;
37
+ }
38
+ };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @license
3
+ * @preserve
4
+ *
5
+ * KeeeX SAS Public code
6
+ * https://keeex.me
7
+ * Copyright 2013-2026 KeeeX All Rights Reserved.
8
+ *
9
+ * These computer program listings and specifications, herein,
10
+ * are and remain the property of KeeeX SAS. The intellectual
11
+ * and technical concepts herein are proprietary to KeeeX SAS
12
+ * and may be covered by EU and foreign patents,
13
+ * patents in process, trade secrets and copyright law.
14
+ *
15
+ * These listings are published as a way to provide third party
16
+ * with the ability to process KeeeX data.
17
+ * As such, support for public inquiries is limited.
18
+ * They are provided "as-is", without warrany of any kind.
19
+ *
20
+ * They shall not be reproduced or copied or used in whole or
21
+ * in part as the basis for manufacture or sale of items unless
22
+ * prior written permission is obtained from KeeeX SAS.
23
+ *
24
+ * For a license agreement, please contact:
25
+ * <mailto: contact@keeex.net>
26
+ *
27
+ */
28
+ import * as types from "./types.js";
29
+ import type { JSONValueType } from "@keeex/utils/json.js";
30
+ export type SysEnv = Partial<Record<string, string>>;
31
+ /** Return the config suffix from env value */
32
+ export declare const getConfigSuffix: (env: SysEnv) => string | undefined;
33
+ /** Return the current NODE_ENV value (default to `production`) */
34
+ export declare const getNodeEnv: (env: SysEnv) => string;
35
+ /** Get a config value from environment variable */
36
+ export declare const getCfgValue: (env: SysEnv, argType: types.ArgType | string, valuePath: string, file: boolean) => string | undefined;
37
+ /** Interpret a value from a string */
38
+ export declare const parseValueFromEnvString: (envValue: string, type: types.TypeForEnv) => JSONValueType;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * @license
3
+ * @preserve
4
+ *
5
+ * KeeeX SAS Public code
6
+ * https://keeex.me
7
+ * Copyright 2013-2026 KeeeX All Rights Reserved.
8
+ *
9
+ * These computer program listings and specifications, herein,
10
+ * are and remain the property of KeeeX SAS. The intellectual
11
+ * and technical concepts herein are proprietary to KeeeX SAS
12
+ * and may be covered by EU and foreign patents,
13
+ * patents in process, trade secrets and copyright law.
14
+ *
15
+ * These listings are published as a way to provide third party
16
+ * with the ability to process KeeeX data.
17
+ * As such, support for public inquiries is limited.
18
+ * They are provided "as-is", without warrany of any kind.
19
+ *
20
+ * They shall not be reproduced or copied or used in whole or
21
+ * in part as the basis for manufacture or sale of items unless
22
+ * prior written permission is obtained from KeeeX SAS.
23
+ *
24
+ * For a license agreement, please contact:
25
+ * <mailto: contact@keeex.net>
26
+ *
27
+ */
28
+ import * as types from "./types.js";
29
+ /** Return the config suffix from env value */
30
+ export const getConfigSuffix = (env) => env.KEEEX_CONFIG_SUFFIX;
31
+ /** Return the current NODE_ENV value (default to `production`) */
32
+ export const getNodeEnv = (env) => env.NODE_ENV ?? "production";
33
+ const getEnvPrefix = (argType) => {
34
+ switch (argType) {
35
+ case types.ArgType.config:
36
+ return "CFG";
37
+ case types.ArgType.secrets:
38
+ return "SECRET";
39
+ default:
40
+ return argType.toLocaleUpperCase();
41
+ }
42
+ };
43
+ /**
44
+ * Convert a path (with dot as separator) to an environment variable name (uppercase with underscore
45
+ * separators).
46
+ */
47
+ const convertPathToEnv = (propPath) => propPath.toUpperCase().split(".").join("_");
48
+ /** Get a config value from environment variable */
49
+ export const getCfgValue = (env, argType, valuePath, file) => {
50
+ const baseKey = `KEEEX_${getEnvPrefix(argType)}_${convertPathToEnv(valuePath)}`;
51
+ const key = file ? `${baseKey}__FILE` : baseKey;
52
+ return env[key];
53
+ };
54
+ /** Interpret a value from a string */
55
+ export const parseValueFromEnvString = (envValue, type) => {
56
+ switch (type) {
57
+ case types.TypeForEnv.string:
58
+ return envValue;
59
+ case types.TypeForEnv.boolean:
60
+ if (envValue.trim().length === 0)
61
+ throw new Error("Invalid argument for boolean");
62
+ return ["1", "TRUE", "ON", "ENABLED"].includes(envValue.toUpperCase());
63
+ case types.TypeForEnv.number: {
64
+ const res = Number.parseFloat(envValue);
65
+ if (Number.isNaN(res))
66
+ throw new Error(`Invalid number: ${envValue}`);
67
+ return res;
68
+ }
69
+ case types.TypeForEnv.json:
70
+ return JSON.parse(envValue);
71
+ }
72
+ };
@@ -25,7 +25,8 @@
25
25
  * <mailto: contact@keeex.net>
26
26
  *
27
27
  */
28
+ import type * as envSrv from "./env.js";
28
29
  import type * as types from "./types.js";
29
- /** Load the project configuration */
30
- export declare const loadConfig: <ConfigType, SecretsType = never>(loadingConfig?: types.ProjectLoadingConfig<ConfigType, SecretsType>) => Promise<types.ProjectConfig<ConfigType, SecretsType>>;
31
- export default loadConfig;
30
+ import type { JSONObject } from "@keeex/utils/json.js";
31
+ /** Return the content of a file, if provided, otherwise returns `null` */
32
+ export declare const getFromFile: (env: envSrv.SysEnv, baseDataConfig: types.BaseDataConfig<unknown>) => Promise<JSONObject | null>;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @license
3
+ * @preserve
4
+ *
5
+ * KeeeX SAS Public code
6
+ * https://keeex.me
7
+ * Copyright 2013-2026 KeeeX All Rights Reserved.
8
+ *
9
+ * These computer program listings and specifications, herein,
10
+ * are and remain the property of KeeeX SAS. The intellectual
11
+ * and technical concepts herein are proprietary to KeeeX SAS
12
+ * and may be covered by EU and foreign patents,
13
+ * patents in process, trade secrets and copyright law.
14
+ *
15
+ * These listings are published as a way to provide third party
16
+ * with the ability to process KeeeX data.
17
+ * As such, support for public inquiries is limited.
18
+ * They are provided "as-is", without warrany of any kind.
19
+ *
20
+ * They shall not be reproduced or copied or used in whole or
21
+ * in part as the basis for manufacture or sale of items unless
22
+ * prior written permission is obtained from KeeeX SAS.
23
+ *
24
+ * For a license agreement, please contact:
25
+ * <mailto: contact@keeex.net>
26
+ *
27
+ */
28
+ import * as nodeFs from "node:fs";
29
+ import * as nodePath from "node:path";
30
+ import { asError } from "@keeex/utils/error.js";
31
+ import { getArgName } from "./consts.js";
32
+ import * as filePathSrv from "./filepath.js";
33
+ /** Load a source/json file as a config object */
34
+ const loadFile = async (path) => {
35
+ const fileSuffix = nodePath.extname(path);
36
+ if ([".js", ".ts", ".cjs", ".mjs", ".cts", ".mts"].includes(fileSuffix)) {
37
+ const module = (await import(path));
38
+ if (module.default)
39
+ return module.default;
40
+ return module;
41
+ }
42
+ if (fileSuffix === ".json") {
43
+ return JSON.parse(await nodeFs.promises.readFile(path, "utf8"));
44
+ }
45
+ throw new Error(`Unexpected suffix: ${fileSuffix}`);
46
+ };
47
+ /** Return the content of a file, if provided, otherwise returns `null` */
48
+ export const getFromFile = async (env, baseDataConfig) => {
49
+ const basename = getArgName(baseDataConfig.argType);
50
+ const filepath = filePathSrv.getFirstFileFound(env, basename, baseDataConfig.loadedFrom);
51
+ if (filepath === undefined)
52
+ return null;
53
+ try {
54
+ return await loadFile(filepath);
55
+ }
56
+ catch (error) {
57
+ throw new Error(`Failed to load data from ${JSON.stringify(filepath)}`, {
58
+ cause: asError(error),
59
+ });
60
+ }
61
+ };
@@ -25,7 +25,7 @@
25
25
  * <mailto: contact@keeex.net>
26
26
  *
27
27
  */
28
- /** Return the config suffix from env value */
29
- export const getConfigSuffix = () => process.env.KEEEX_CONFIG_SUFFIX;
30
- /** Return the current NODE_ENV value (default to `production`) */
31
- export const getNodeEnv = () => process.env.NODE_ENV ?? "production";
28
+ import * as envSrv from "./env.js";
29
+ /** Returns the first file found in `checkDirs`, filenames, and configured extensions */
30
+ export declare const getFirstFileFound: (env: envSrv.SysEnv, baseName: string, loadedFrom: Array<string>) => string | undefined;
31
+ export declare const clearCache: () => void;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @license
3
+ * @preserve
4
+ *
5
+ * KeeeX SAS Public code
6
+ * https://keeex.me
7
+ * Copyright 2013-2026 KeeeX All Rights Reserved.
8
+ *
9
+ * These computer program listings and specifications, herein,
10
+ * are and remain the property of KeeeX SAS. The intellectual
11
+ * and technical concepts herein are proprietary to KeeeX SAS
12
+ * and may be covered by EU and foreign patents,
13
+ * patents in process, trade secrets and copyright law.
14
+ *
15
+ * These listings are published as a way to provide third party
16
+ * with the ability to process KeeeX data.
17
+ * As such, support for public inquiries is limited.
18
+ * They are provided "as-is", without warrany of any kind.
19
+ *
20
+ * They shall not be reproduced or copied or used in whole or
21
+ * in part as the basis for manufacture or sale of items unless
22
+ * prior written permission is obtained from KeeeX SAS.
23
+ *
24
+ * For a license agreement, please contact:
25
+ * <mailto: contact@keeex.net>
26
+ *
27
+ */
28
+ import * as nodeFs from "node:fs";
29
+ import * as nodePath from "node:path";
30
+ import * as envSrv from "./env.js";
31
+ let suffixCache = null;
32
+ /** Return the suffix to use, if any */
33
+ const getConfigSuffix = (env) => {
34
+ if (suffixCache === null) {
35
+ const suffixFromEnv = envSrv.getConfigSuffix(env);
36
+ if (suffixFromEnv === undefined) {
37
+ const nodeEnvValue = envSrv.getNodeEnv(env);
38
+ suffixCache = nodeEnvValue === "test" ? "-test" : "";
39
+ }
40
+ else {
41
+ suffixCache = suffixFromEnv;
42
+ }
43
+ }
44
+ return suffixCache;
45
+ };
46
+ /** If a suffix is present, return both the name without and with the suffix */
47
+ const applySuffix = (env, basestr) => {
48
+ const suffix = getConfigSuffix(env);
49
+ return suffix ? [`${basestr}${suffix}`, basestr] : [basestr];
50
+ };
51
+ let defaultDirsCache = null;
52
+ /** Compute the default directories to look through */
53
+ const getDefaultDirs = () => {
54
+ if (defaultDirsCache === null) {
55
+ /** Initial directory of the app. Hopefully the project root */
56
+ const projectRoot = process.cwd();
57
+ const parentDir = nodePath.resolve(projectRoot, "..");
58
+ defaultDirsCache =
59
+ parentDir !== projectRoot && parentDir ? [projectRoot, parentDir] : [projectRoot];
60
+ }
61
+ return defaultDirsCache;
62
+ };
63
+ /** Returns the first file found in `checkDirs`, filenames, and configured extensions */
64
+ export const getFirstFileFound = (env, baseName, loadedFrom) => {
65
+ const filenames = applySuffix(env, baseName);
66
+ const actuallyChecked = [];
67
+ for (const filepath of getDefaultDirs()) {
68
+ for (const filename of filenames) {
69
+ for (const extension of [".js", ".json", ".ts", ".cjs", ".mjs", ".cts", ".mts"]) {
70
+ const candidate = nodePath.resolve(filepath, `${filename}${extension}`);
71
+ if (nodeFs.existsSync(candidate)) {
72
+ loadedFrom.push(`FILE: loading from ${JSON.stringify(candidate)}`);
73
+ return candidate;
74
+ }
75
+ actuallyChecked.push(candidate);
76
+ }
77
+ }
78
+ }
79
+ const triedFiles = actuallyChecked.map((c) => JSON.stringify(c)).join(",");
80
+ loadedFrom.push(`FILE: not found (tried ${triedFiles})`);
81
+ };
82
+ export const clearCache = () => {
83
+ suffixCache = null;
84
+ defaultDirsCache = null;
85
+ };
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @license
3
+ * @preserve
4
+ *
5
+ * KeeeX SAS Public code
6
+ * https://keeex.me
7
+ * Copyright 2013-2026 KeeeX All Rights Reserved.
8
+ *
9
+ * These computer program listings and specifications, herein,
10
+ * are and remain the property of KeeeX SAS. The intellectual
11
+ * and technical concepts herein are proprietary to KeeeX SAS
12
+ * and may be covered by EU and foreign patents,
13
+ * patents in process, trade secrets and copyright law.
14
+ *
15
+ * These listings are published as a way to provide third party
16
+ * with the ability to process KeeeX data.
17
+ * As such, support for public inquiries is limited.
18
+ * They are provided "as-is", without warrany of any kind.
19
+ *
20
+ * They shall not be reproduced or copied or used in whole or
21
+ * in part as the basis for manufacture or sale of items unless
22
+ * prior written permission is obtained from KeeeX SAS.
23
+ *
24
+ * For a license agreement, please contact:
25
+ * <mailto: contact@keeex.net>
26
+ *
27
+ */
28
+ import * as envSrv from "./env.js";
29
+ import * as types from "./types.js";
30
+ /**
31
+ * Load the project configuration.
32
+ *
33
+ * The `argv` and `env` arguments defaults to the process values, and providing custom values will
34
+ * invalidate using cache, which can cause issues if used wrong.
35
+ * Notably, loading config from CLI arguments will consume them.
36
+ *
37
+ * @param argv - Custom value of argv
38
+ * @param env - Custom value of env
39
+ *
40
+ * @returns
41
+ * The config object (cached, if applicable)
42
+ */
43
+ export declare const loadConfig: <ConfigType, SecretsType = never>(loadingConfig?: types.ProjectLoadingConfig<ConfigType, SecretsType>, argv?: Array<string>, env?: envSrv.SysEnv) => Promise<types.ProjectConfig<ConfigType, SecretsType>>;
44
+ /**
45
+ * Internal function used for testing purpose.
46
+ *
47
+ * Clear all loaded cache.
48
+ */
49
+ export declare const clearConfigCache: () => void;
@@ -0,0 +1,173 @@
1
+ /**
2
+ * @license
3
+ * @preserve
4
+ *
5
+ * KeeeX SAS Public code
6
+ * https://keeex.me
7
+ * Copyright 2013-2026 KeeeX All Rights Reserved.
8
+ *
9
+ * These computer program listings and specifications, herein,
10
+ * are and remain the property of KeeeX SAS. The intellectual
11
+ * and technical concepts herein are proprietary to KeeeX SAS
12
+ * and may be covered by EU and foreign patents,
13
+ * patents in process, trade secrets and copyright law.
14
+ *
15
+ * These listings are published as a way to provide third party
16
+ * with the ability to process KeeeX data.
17
+ * As such, support for public inquiries is limited.
18
+ * They are provided "as-is", without warrany of any kind.
19
+ *
20
+ * They shall not be reproduced or copied or used in whole or
21
+ * in part as the basis for manufacture or sale of items unless
22
+ * prior written permission is obtained from KeeeX SAS.
23
+ *
24
+ * For a license agreement, please contact:
25
+ * <mailto: contact@keeex.net>
26
+ *
27
+ */
28
+ import * as nodeFs from "node:fs";
29
+ import { asError } from "@keeex/utils/error.js";
30
+ import * as utilsJson from "../utils/json.js";
31
+ import * as cliSrv from "./cli.js";
32
+ import { getArgName } from "./consts.js";
33
+ import * as envSrv from "./env.js";
34
+ import { getFromFile } from "./file.js";
35
+ import * as filePathSrv from "./filepath.js";
36
+ import * as types from "./types.js";
37
+ /**
38
+ * Extract config values using key-value getters.
39
+ *
40
+ * @param getterDirect - Getter that should return the value string if available
41
+ * @param getterFile - Getter that should return the path to the file containing the value if
42
+ * available
43
+ * @param loadedFromName - Used in the log in case of error to know what loader failed
44
+ */
45
+ const getFromRecords = async (baseDataConfig, getterDirect, getterFile, loadedFromName) => {
46
+ const result = {};
47
+ let anyValueRead = false;
48
+ await Promise.all(Object.entries(baseDataConfig.environmentValues).map(async ([propPath, type]) => {
49
+ try {
50
+ const directArgValue = await getterDirect(propPath);
51
+ if (directArgValue !== undefined) {
52
+ baseDataConfig.loadedFrom.push(`${loadedFromName}: ${propPath}`);
53
+ anyValueRead = true;
54
+ utilsJson.setPathValue(result, propPath, envSrv.parseValueFromEnvString(directArgValue, type));
55
+ return;
56
+ }
57
+ const fileArgValue = await getterFile(propPath);
58
+ if (fileArgValue !== undefined) {
59
+ const fileData = (await nodeFs.promises.readFile(fileArgValue, "utf8")).trimEnd();
60
+ baseDataConfig.loadedFrom.push(`${loadedFromName}: ${propPath} (from file)`);
61
+ anyValueRead = true;
62
+ utilsJson.setPathValue(result, propPath, envSrv.parseValueFromEnvString(fileData, type));
63
+ }
64
+ }
65
+ catch (error) {
66
+ throw new Error(`Parsing argument for ${propPath}`, { cause: asError(error) });
67
+ }
68
+ }));
69
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
70
+ if (anyValueRead)
71
+ return result;
72
+ baseDataConfig.loadedFrom.push("ENV: no env. variables used");
73
+ return null;
74
+ };
75
+ const getFromEnv = async (env, baseDataConfig) => {
76
+ const fromEnvDirect = (propPath) => envSrv.getCfgValue(env, baseDataConfig.argType, propPath, false);
77
+ const fromEnvFile = (propPath) => envSrv.getCfgValue(env, baseDataConfig.argType, propPath, true);
78
+ return getFromRecords(baseDataConfig, fromEnvDirect, fromEnvFile, "ENV");
79
+ };
80
+ const getFromCliArgs = async (argv, baseDataConfig) => {
81
+ const fromCliDirect = (propPath) => cliSrv.getCfgValue(argv, baseDataConfig.argType, propPath, false);
82
+ const fromCliFile = (propPath) => cliSrv.getCfgValue(argv, baseDataConfig.argType, propPath, true);
83
+ return getFromRecords(baseDataConfig, fromCliDirect, fromCliFile, "CLIARGS");
84
+ };
85
+ /** Build the specific object based on the available sources */
86
+ const getBaseData = async (argv, env, baseDataConfig) => {
87
+ const [fromFile, fromEnv, fromCliBase, fromCliArgs] = await Promise.all([
88
+ getFromFile(env, baseDataConfig),
89
+ getFromEnv(env, baseDataConfig),
90
+ cliSrv.getFromCliBase(argv, baseDataConfig),
91
+ getFromCliArgs(argv, baseDataConfig),
92
+ ]);
93
+ const result = utilsJson.mergeJsonDeep(baseDataConfig.defaultValues, fromFile, fromEnv, fromCliBase, fromCliArgs);
94
+ try {
95
+ baseDataConfig.predicate?.(result, true);
96
+ }
97
+ catch (error) {
98
+ throw new Error(`Error while loading ${getArgName(baseDataConfig.argType)} data`, {
99
+ cause: error,
100
+ });
101
+ }
102
+ return result;
103
+ };
104
+ /** Build the full configuration object on demand */
105
+ const getProjectConfig = async (argv, env, loadingConfig) => {
106
+ const loadedFrom = [];
107
+ try {
108
+ const baseConfig = await getBaseData(argv, env, {
109
+ argType: types.ArgType.config,
110
+ defaultValues: loadingConfig.configDefault ?? {},
111
+ environmentValues: loadingConfig.configEnvironmentVars ?? {},
112
+ loadedFrom,
113
+ predicate: loadingConfig.configPredicate,
114
+ });
115
+ const secrets = await getBaseData(argv, env, {
116
+ argType: types.ArgType.secrets,
117
+ defaultValues: loadingConfig.secretsDefault ?? {},
118
+ environmentValues: loadingConfig.secretEnvironmentVars ?? {},
119
+ loadedFrom,
120
+ predicate: loadingConfig.secretsPredicate,
121
+ });
122
+ if (secrets) {
123
+ const merged = { ...baseConfig, secrets };
124
+ return merged;
125
+ }
126
+ return baseConfig;
127
+ }
128
+ catch (error) {
129
+ throw new Error(`Failure to load config (tried:${loadedFrom.map((c) => JSON.stringify(c)).join(",")})`, { cause: error });
130
+ }
131
+ };
132
+ /** Cache of the loaded configuration */
133
+ let configCache = null;
134
+ const loadConfigCached = async (loadingConfig, argv, env) => {
135
+ try {
136
+ return await getProjectConfig(argv, env, loadingConfig);
137
+ }
138
+ catch (error) {
139
+ configCache = null;
140
+ throw error;
141
+ }
142
+ };
143
+ /**
144
+ * Load the project configuration.
145
+ *
146
+ * The `argv` and `env` arguments defaults to the process values, and providing custom values will
147
+ * invalidate using cache, which can cause issues if used wrong.
148
+ * Notably, loading config from CLI arguments will consume them.
149
+ *
150
+ * @param argv - Custom value of argv
151
+ * @param env - Custom value of env
152
+ *
153
+ * @returns
154
+ * The config object (cached, if applicable)
155
+ */
156
+ export const loadConfig = (loadingConfig, argv, env) => {
157
+ const realArgv = argv ?? process.argv;
158
+ const realEnv = env ?? process.env;
159
+ const realConfig = loadingConfig ?? {};
160
+ if (argv || env)
161
+ return getProjectConfig(realArgv, realEnv, realConfig);
162
+ configCache ??= loadConfigCached(realConfig, realArgv, realEnv);
163
+ return configCache;
164
+ };
165
+ /**
166
+ * Internal function used for testing purpose.
167
+ *
168
+ * Clear all loaded cache.
169
+ */
170
+ export const clearConfigCache = () => {
171
+ filePathSrv.clearCache();
172
+ configCache = null;
173
+ };
@@ -25,13 +25,41 @@
25
25
  * <mailto: contact@keeex.net>
26
26
  *
27
27
  */
28
- /** Base for any returned configuration */
28
+ import type { TypePredicate } from "@keeex/utils/types/types.js";
29
29
  export type BaseConfig = Record<string, any>;
30
- /** Generic type predicate */
31
- export type TypePredicate<Type> = (obj: unknown, throwOnError?: boolean) => obj is Type;
30
+ /** Separate config from secret inputs */
31
+ export declare enum ArgType {
32
+ config = 0,
33
+ secrets = 1
34
+ }
35
+ export declare enum TypeForEnv {
36
+ string = 0,
37
+ boolean = 1,
38
+ number = 2,
39
+ json = 3
40
+ }
41
+ type DeepPartial<T> = {
42
+ [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
43
+ };
44
+ type EnvVarsList = Record<string, TypeForEnv>;
45
+ export interface BaseDataConfig<Type> {
46
+ argType: ArgType | string;
47
+ defaultValues: DeepPartial<Type>;
48
+ environmentValues: EnvVarsList;
49
+ loadedFrom: Array<string>;
50
+ predicate?: TypePredicate<Type>;
51
+ }
32
52
  export interface ProjectLoadingConfig<ConfigType, SecretsType = never> {
53
+ /** Default values to be used as the base of all configs */
54
+ configDefault?: DeepPartial<ConfigType>;
55
+ /** List properties that can be read from environment variables for the config */
56
+ configEnvironmentVars?: EnvVarsList;
33
57
  /** Type predicate for the base configuration (without secrets) */
34
58
  configPredicate?: TypePredicate<ConfigType>;
59
+ /** Default values to be used as the base of all secrets */
60
+ secretsDefault?: DeepPartial<SecretsType>;
61
+ /** List properties that can be read from environment variables for the secret */
62
+ secretEnvironmentVars?: EnvVarsList;
35
63
  /** Type predicate for the secrets only */
36
64
  secretsPredicate?: SecretsType extends never ? never : TypePredicate<SecretsType>;
37
65
  }
@@ -40,3 +68,4 @@ export type ProjectConfig<ConfigType, SecretsType = never> = SecretsType extends
40
68
  } & {
41
69
  secrets: SecretsType;
42
70
  };
71
+ export {};
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @license
3
+ * @preserve
4
+ *
5
+ * KeeeX SAS Public code
6
+ * https://keeex.me
7
+ * Copyright 2013-2026 KeeeX All Rights Reserved.
8
+ *
9
+ * These computer program listings and specifications, herein,
10
+ * are and remain the property of KeeeX SAS. The intellectual
11
+ * and technical concepts herein are proprietary to KeeeX SAS
12
+ * and may be covered by EU and foreign patents,
13
+ * patents in process, trade secrets and copyright law.
14
+ *
15
+ * These listings are published as a way to provide third party
16
+ * with the ability to process KeeeX data.
17
+ * As such, support for public inquiries is limited.
18
+ * They are provided "as-is", without warrany of any kind.
19
+ *
20
+ * They shall not be reproduced or copied or used in whole or
21
+ * in part as the basis for manufacture or sale of items unless
22
+ * prior written permission is obtained from KeeeX SAS.
23
+ *
24
+ * For a license agreement, please contact:
25
+ * <mailto: contact@keeex.net>
26
+ *
27
+ */
28
+ /** Separate config from secret inputs */
29
+ export var ArgType;
30
+ (function (ArgType) {
31
+ ArgType[ArgType["config"] = 0] = "config";
32
+ ArgType[ArgType["secrets"] = 1] = "secrets";
33
+ })(ArgType || (ArgType = {}));
34
+ Object.freeze(ArgType);
35
+ export var TypeForEnv;
36
+ (function (TypeForEnv) {
37
+ TypeForEnv[TypeForEnv["string"] = 0] = "string";
38
+ TypeForEnv[TypeForEnv["boolean"] = 1] = "boolean";
39
+ TypeForEnv[TypeForEnv["number"] = 2] = "number";
40
+ TypeForEnv[TypeForEnv["json"] = 3] = "json";
41
+ })(TypeForEnv || (TypeForEnv = {}));
42
+ Object.freeze(TypeForEnv);
@@ -0,0 +1,58 @@
1
+ /**
2
+ * @license
3
+ * @preserve
4
+ *
5
+ * KeeeX SAS Public code
6
+ * https://keeex.me
7
+ * Copyright 2013-2026 KeeeX All Rights Reserved.
8
+ *
9
+ * These computer program listings and specifications, herein,
10
+ * are and remain the property of KeeeX SAS. The intellectual
11
+ * and technical concepts herein are proprietary to KeeeX SAS
12
+ * and may be covered by EU and foreign patents,
13
+ * patents in process, trade secrets and copyright law.
14
+ *
15
+ * These listings are published as a way to provide third party
16
+ * with the ability to process KeeeX data.
17
+ * As such, support for public inquiries is limited.
18
+ * They are provided "as-is", without warrany of any kind.
19
+ *
20
+ * They shall not be reproduced or copied or used in whole or
21
+ * in part as the basis for manufacture or sale of items unless
22
+ * prior written permission is obtained from KeeeX SAS.
23
+ *
24
+ * For a license agreement, please contact:
25
+ * <mailto: contact@keeex.net>
26
+ *
27
+ */
28
+ import * as utilsJson from "@keeex/utils/json.js";
29
+ /**
30
+ * Set the value in a JSON object's path.
31
+ *
32
+ * This function does very little typechecking; if a value in the path exists, it is assumed to be
33
+ * assignable to the next path entry.
34
+ * If a value in the path does not exist, a new object is created.
35
+ *
36
+ * @param obj - The object to update
37
+ * @param path - The JSON path, using dot as separator
38
+ * @param value - The value to write. `undefined` will delete the key. In arrays, it will remove the
39
+ * index and not leave an empty element.
40
+ */
41
+ export declare const setPathValue: (obj: utilsJson.JSONObject, path: string, value: utilsJson.JSONValueType | undefined) => void;
42
+ /**
43
+ * Merge all provided JSON, left to right.
44
+ *
45
+ * This is relatively loose; properties common between the current object and the next are merged
46
+ * using this algorithm:
47
+ *
48
+ * - if there is no old value, always use the new value
49
+ * - if the new value is a primitive type or an array, it is always used
50
+ * - if both the old and new value are objects, they are merged using this function
51
+ * - if the new value is an object but the old value isn't, the new value is used and the old value is lost
52
+ *
53
+ * @param operands - The values to merge, in order. `undefined` and `null` are skipped.
54
+ *
55
+ * @returns
56
+ * The final object
57
+ */
58
+ export declare const mergeJsonDeep: (...operands: Array<utilsJson.JSONObject | undefined | null>) => utilsJson.JSONObject;
@@ -0,0 +1,147 @@
1
+ /**
2
+ * @license
3
+ * @preserve
4
+ *
5
+ * KeeeX SAS Public code
6
+ * https://keeex.me
7
+ * Copyright 2013-2026 KeeeX All Rights Reserved.
8
+ *
9
+ * These computer program listings and specifications, herein,
10
+ * are and remain the property of KeeeX SAS. The intellectual
11
+ * and technical concepts herein are proprietary to KeeeX SAS
12
+ * and may be covered by EU and foreign patents,
13
+ * patents in process, trade secrets and copyright law.
14
+ *
15
+ * These listings are published as a way to provide third party
16
+ * with the ability to process KeeeX data.
17
+ * As such, support for public inquiries is limited.
18
+ * They are provided "as-is", without warrany of any kind.
19
+ *
20
+ * They shall not be reproduced or copied or used in whole or
21
+ * in part as the basis for manufacture or sale of items unless
22
+ * prior written permission is obtained from KeeeX SAS.
23
+ *
24
+ * For a license agreement, please contact:
25
+ * <mailto: contact@keeex.net>
26
+ *
27
+ */
28
+ import * as utilsJson from "@keeex/utils/json.js";
29
+ /** Check if a value is traversable by path */
30
+ const assertsTraversableType = (value) => {
31
+ if (utilsJson.isJsonPrimitiveType(value)) {
32
+ throw new TypeError("value is not traversable");
33
+ }
34
+ };
35
+ /**
36
+ * Get the next object by path.
37
+ *
38
+ * @returns
39
+ * Only an object or an array. Anything else will throw.
40
+ * If the requested property does not exists, it is added and an empty object is used.
41
+ */
42
+ const getNextObjectInPath = (currentObject, propertyName) => {
43
+ if (Array.isArray(currentObject)) {
44
+ const index = Number.parseInt(propertyName);
45
+ if (index in currentObject) {
46
+ const nextObject = currentObject[index];
47
+ assertsTraversableType(nextObject);
48
+ return nextObject;
49
+ }
50
+ const nextObject = {};
51
+ currentObject[index] = nextObject;
52
+ return nextObject;
53
+ }
54
+ if (propertyName in currentObject) {
55
+ const nextObject = currentObject[propertyName];
56
+ assertsTraversableType(nextObject);
57
+ return nextObject;
58
+ }
59
+ const nextObject = {};
60
+ currentObject[propertyName] = nextObject;
61
+ return nextObject;
62
+ };
63
+ /**
64
+ * Define the value on the given property name.
65
+ *
66
+ * @param currentObject - The object on which a property will be set
67
+ * @param value - The value to set, or undefined to delete an etry
68
+ */
69
+ const setValueInProperty = (currentObject, propertyName, value) => {
70
+ if (Array.isArray(currentObject)) {
71
+ const index = Number.parseInt(propertyName);
72
+ if (value === undefined) {
73
+ currentObject.splice(index, 1);
74
+ }
75
+ else {
76
+ currentObject[index] = value;
77
+ }
78
+ }
79
+ else if (value === undefined) {
80
+ if (propertyName in currentObject) {
81
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
82
+ delete currentObject[propertyName];
83
+ }
84
+ }
85
+ else {
86
+ currentObject[propertyName] = value;
87
+ }
88
+ };
89
+ /**
90
+ * Set the value in a JSON object's path.
91
+ *
92
+ * This function does very little typechecking; if a value in the path exists, it is assumed to be
93
+ * assignable to the next path entry.
94
+ * If a value in the path does not exist, a new object is created.
95
+ *
96
+ * @param obj - The object to update
97
+ * @param path - The JSON path, using dot as separator
98
+ * @param value - The value to write. `undefined` will delete the key. In arrays, it will remove the
99
+ * index and not leave an empty element.
100
+ */
101
+ export const setPathValue = (obj, path, value) => {
102
+ const pathParts = path.split(".");
103
+ const lastPart = pathParts.pop();
104
+ if (lastPart === undefined)
105
+ throw new Error("Invalid path");
106
+ let currentObject = obj;
107
+ for (const pathPart of pathParts)
108
+ currentObject = getNextObjectInPath(currentObject, pathPart);
109
+ setValueInProperty(currentObject, lastPart, value);
110
+ };
111
+ /**
112
+ * Merge all provided JSON, left to right.
113
+ *
114
+ * This is relatively loose; properties common between the current object and the next are merged
115
+ * using this algorithm:
116
+ *
117
+ * - if there is no old value, always use the new value
118
+ * - if the new value is a primitive type or an array, it is always used
119
+ * - if both the old and new value are objects, they are merged using this function
120
+ * - if the new value is an object but the old value isn't, the new value is used and the old value is lost
121
+ *
122
+ * @param operands - The values to merge, in order. `undefined` and `null` are skipped.
123
+ *
124
+ * @returns
125
+ * The final object
126
+ */
127
+ export const mergeJsonDeep = (...operands) => {
128
+ const result = {};
129
+ for (const operand of operands) {
130
+ if (operand === undefined || operand === null)
131
+ continue;
132
+ for (const [key, newValue] of Object.entries(operand)) {
133
+ if (!(key in result) || utilsJson.isJsonPrimitiveType(newValue) || Array.isArray(newValue)) {
134
+ result[key] = newValue;
135
+ continue;
136
+ }
137
+ // newValue is a JSONObject at this point
138
+ const oldValue = result[key];
139
+ if (utilsJson.isJsonPrimitiveType(oldValue) || Array.isArray(oldValue)) {
140
+ result[key] = newValue;
141
+ continue;
142
+ }
143
+ result[key] = mergeJsonDeep(oldValue, newValue);
144
+ }
145
+ }
146
+ return result;
147
+ };
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name":"@keeex/projectconfig","version":"5.0.1","description":"Load project configuration with separate secrets","main":"./lib/index.js","scripts":{},"type":"module","author":"KeeeX SAS","contributors":[{"email":"gabriel@keeex.net","name":"Gabriel Paul \"Cley Faye\" Risterucci"}],"license":"SEE LICENSE IN LICENSE","files":["lib"],"exports":{".":"./lib/index.js","./env.js":"./lib/env.js","./index.js":"./lib/index.js","./lib/env.js":"./lib/env.js","./lib/index.js":"./lib/index.js","./lib/loader.js":"./lib/loader.js","./lib/types.js":"./lib/types.js","./loader.js":"./lib/loader.js","./types.js":"./lib/types.js"},"homepage":"https://keeex.me/oss"}
1
+ {"name":"@keeex/projectconfig","version":"6.0.1","description":"Load project configuration with separate secrets","main":"./lib/index.js","scripts":{},"type":"module","author":"KeeeX SAS","contributors":[{"email":"gabriel@keeex.net","name":"Gabriel Paul \"Cley Faye\" Risterucci"}],"license":"SEE LICENSE IN LICENSE","files":["lib"],"exports":{".":"./lib/index.js","./index.js":"./lib/index.js"},"dependencies":{"@keeex/utils":"^7.6.2","@keeex/utils-node":"^6.8.2"},"homepage":"https://keeex.me/oss"}
package/lib/loader.js DELETED
@@ -1,184 +0,0 @@
1
- /**
2
- * @license
3
- * @preserve
4
- *
5
- * KeeeX SAS Public code
6
- * https://keeex.me
7
- * Copyright 2013-2026 KeeeX All Rights Reserved.
8
- *
9
- * These computer program listings and specifications, herein,
10
- * are and remain the property of KeeeX SAS. The intellectual
11
- * and technical concepts herein are proprietary to KeeeX SAS
12
- * and may be covered by EU and foreign patents,
13
- * patents in process, trade secrets and copyright law.
14
- *
15
- * These listings are published as a way to provide third party
16
- * with the ability to process KeeeX data.
17
- * As such, support for public inquiries is limited.
18
- * They are provided "as-is", without warrany of any kind.
19
- *
20
- * They shall not be reproduced or copied or used in whole or
21
- * in part as the basis for manufacture or sale of items unless
22
- * prior written permission is obtained from KeeeX SAS.
23
- *
24
- * For a license agreement, please contact:
25
- * <mailto: contact@keeex.net>
26
- *
27
- */
28
- import * as nodeFs from "node:fs";
29
- import * as nodePath from "node:path";
30
- import * as env from "./env.js";
31
- let suffixCache = null;
32
- /** Return the suffix to use, if any */
33
- const getConfigSuffix = () => {
34
- if (suffixCache === null) {
35
- const suffixFromEnv = env.getConfigSuffix();
36
- if (suffixFromEnv === undefined) {
37
- const nodeEnvValue = env.getNodeEnv();
38
- suffixCache = nodeEnvValue === "test" ? "-test" : "";
39
- }
40
- else {
41
- suffixCache = suffixFromEnv;
42
- }
43
- }
44
- return suffixCache;
45
- };
46
- /** If a suffix is present, return both the name without and with the suffix */
47
- const applySuffix = (basestr) => {
48
- const suffix = getConfigSuffix();
49
- return suffix ? [`${basestr}${suffix}`, basestr] : [basestr];
50
- };
51
- let defaultDirsCache = null;
52
- /** Compute the default directories to look through */
53
- const getDefaultDirs = () => {
54
- if (defaultDirsCache === null) {
55
- /** Initial directory of the app. Hopefully the project root */
56
- const projectRoot = process.cwd();
57
- const parentDir = nodePath.resolve(projectRoot, "..");
58
- defaultDirsCache =
59
- parentDir !== projectRoot && parentDir ? [projectRoot, parentDir] : [projectRoot];
60
- }
61
- return defaultDirsCache;
62
- };
63
- /** Returns the first file found in `checkDirs`, filenames, and configured extensions */
64
- const getFirstFileFound = (filenames, candidates) => {
65
- for (const filepath of getDefaultDirs()) {
66
- for (const filename of filenames) {
67
- for (const extension of [".js", ".json", ".ts", ".cjs", ".mjs", ".cts", ".mts"]) {
68
- const candidate = nodePath.resolve(filepath, `${filename}${extension}`);
69
- if (nodeFs.existsSync(candidate)) {
70
- candidates.push(`found: ${candidate}`);
71
- return candidate;
72
- }
73
- candidates.push(`missing: ${candidate}`);
74
- }
75
- }
76
- }
77
- };
78
- /** Load a source/json file as a config object */
79
- const loadFile = async (path) => {
80
- const fileSuffix = nodePath.extname(path);
81
- if ([".js", ".ts", ".cjs", ".mjs", ".cts", ".mts"].includes(fileSuffix)) {
82
- const module = (await import(path));
83
- if (module.default)
84
- return module.default;
85
- return module;
86
- }
87
- if (fileSuffix === ".json")
88
- return JSON.parse(await nodeFs.promises.readFile(path, "utf8"));
89
- throw new Error(`Unexpected suffix: ${fileSuffix}`);
90
- };
91
- /** Return the content of a file, if provided, otherwise returns `null` */
92
- const getFromFile = async (basename, candidates) => {
93
- const filepath = getFirstFileFound(applySuffix(basename), candidates);
94
- if (filepath === undefined)
95
- return null;
96
- return loadFile(filepath);
97
- };
98
- /**
99
- * Get config from the CLI.
100
- *
101
- * Check if `--${argName}` is present, and if so, tries to interpret it as JSON or as a file path to load.
102
- *
103
- * @returns
104
- * The loaded content, or `null` if the argument is not present.
105
- */
106
- const getFromCli = async (argv, argName, candidates) => {
107
- const configArgIndex = argv.indexOf(`--${argName}`);
108
- if (configArgIndex === -1) {
109
- candidates.push(`missing: --${argName} on CLI`);
110
- return null;
111
- }
112
- const valueIndex = configArgIndex + 1;
113
- if (argv.length <= valueIndex) {
114
- candidates.push(`incomplete: --${argName} on CLI requires an argument`);
115
- throw new Error(`--${argName} requires a string argument`);
116
- }
117
- const value = argv[valueIndex];
118
- const valuesExtracted = 2;
119
- argv.splice(configArgIndex, valuesExtracted);
120
- if (value.startsWith("{")) {
121
- try {
122
- return JSON.parse(value);
123
- }
124
- catch (error) {
125
- candidates.push(`error: --${argName} JSON on CLI`);
126
- throw error;
127
- }
128
- }
129
- try {
130
- return JSON.parse(await nodeFs.promises.readFile(value, "utf8"));
131
- }
132
- catch (error) {
133
- candidates.push(`error: --${argName} as file path`);
134
- throw error;
135
- }
136
- };
137
- /** Return the specified object from the first available source */
138
- const getBaseData = async (argv, argName, candidates, predicate) => {
139
- const result = (await getFromCli(argv, argName, candidates)) ?? getFromFile(argName, candidates);
140
- if (predicate) {
141
- try {
142
- predicate(result, true);
143
- }
144
- catch (error) {
145
- throw new Error(`Error while loading ${argName} data`, { cause: error });
146
- }
147
- }
148
- return result;
149
- };
150
- /** Build the full configuration object on demand */
151
- const getProjectConfig = async (argv, loadingConfig) => {
152
- const candidates = [];
153
- try {
154
- const baseConfig = await getBaseData(argv, "config", candidates, loadingConfig.configPredicate);
155
- if (!baseConfig)
156
- throw new Error("Missing base project configuration");
157
- const secrets = await getBaseData(argv, "secrets", candidates, loadingConfig.secretsPredicate);
158
- if (secrets) {
159
- const merged = { ...baseConfig, secrets };
160
- return merged;
161
- }
162
- return baseConfig;
163
- }
164
- catch (error) {
165
- throw new Error(`Failure to load config (tried:${candidates.map((c) => JSON.stringify(c)).join(",")})`, { cause: error });
166
- }
167
- };
168
- /** Cache of the loaded configuration */
169
- let configCache = null;
170
- const loadConfigCached = async (loadingConfig) => {
171
- try {
172
- return await getProjectConfig(process.argv, loadingConfig);
173
- }
174
- catch (error) {
175
- configCache = null;
176
- throw error;
177
- }
178
- };
179
- /** Load the project configuration */
180
- export const loadConfig = (loadingConfig) => {
181
- configCache ??= loadConfigCached(loadingConfig ?? {});
182
- return configCache;
183
- };
184
- export default loadConfig;