@keeex/projectconfig 4.2.1 → 5.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
@@ -43,10 +43,55 @@ The same can be done for secrets with `--secrets`.
43
43
 
44
44
  ### Accessing the configuration
45
45
 
46
- The loaded configuration is available as the default export of the library:
46
+ The loaded configuration can be obtained by calling the function `loadConfig()`.
47
+
48
+ ```typescript
49
+ import {makeProfilePredicate} from "@keeex/utils/types/record.js";
50
+ import * as projectConfig from "@keeex/projectconfig";
51
+
52
+ interface Config {
53
+ port: number;
54
+ debug: boolean;
55
+ }
56
+
57
+ const isConfig = makeProfilePredicate<Config>({
58
+ port: "number";
59
+ debug: "boolean";
60
+ });
61
+
62
+ interface Secrets {
63
+ login: string;
64
+ password: string;
65
+ }
66
+
67
+ const isSecrets = makeProfilePredicate<Secrets>({
68
+ login: "string";
69
+ password: "string";
70
+ });
71
+
72
+ const getConfig = (): Promise<projectConfig.ProjectConfig<Config, Secrets>> =>
73
+ projectConfig.loadConfig({
74
+ configPredicate: isConfig,
75
+ secretsPredicate: isSecrets,
76
+ });
77
+
78
+ export default getConfig();
79
+ ```
80
+
81
+ In the above exemple, we define a module that export the fully-typed configuration, to be used throughout a project.
82
+ Using this module would look something like this:
47
83
 
48
- ```JavaScript
49
- import config from "@keeex/projectconfig";
84
+ ```typescript
85
+ import getConfig from "./services/config.js";
50
86
 
51
- console.log(config.something);
87
+ const config = await getConfig();
88
+
89
+ console.log(`
90
+ Port: ${config.port}
91
+ Debug: ${config.debug}
92
+ User: ${config.secrets.login}
93
+ Password: ${"*".repeat(config.secrets.password.length)}
94
+ `);
52
95
  ```
96
+
97
+ The loaded configuration is cached after the first call, so it is safe to recall this function multiple times from multiple places if needed.
package/lib/index.d.ts CHANGED
@@ -25,6 +25,5 @@
25
25
  * <mailto: contact@keeex.net>
26
26
  *
27
27
  */
28
- import type * as types from "./types.js";
29
- declare const _default: types.BaseConfig;
30
- export default _default;
28
+ export { loadConfig } from "./loader.js";
29
+ export type { ProjectConfig } from "./types.js";
package/lib/index.js CHANGED
@@ -25,120 +25,4 @@
25
25
  * <mailto: contact@keeex.net>
26
26
  *
27
27
  */
28
- import * as nodeFs from "node:fs";
29
- import * as nodePath from "node:path";
30
- import * as env from "./env.js";
31
- /** Return the suffix to use, if any */
32
- const configSuffix = () => {
33
- const suffixFromEnv = env.getConfigSuffix();
34
- if (suffixFromEnv !== undefined)
35
- return suffixFromEnv;
36
- const nodeEnvValue = env.getNodeEnv();
37
- return nodeEnvValue === "test" ? "-test" : null;
38
- };
39
- /** Suffix to use for default file name */
40
- const suffix = configSuffix();
41
- /** If a suffix is present, return both the name without and with the suffix */
42
- const applySuffix = (basestr) => suffix ? [`${basestr}${suffix}`, basestr] : [basestr];
43
- /** Basename for config file */
44
- const configBasename = applySuffix("config");
45
- /** Basename for secret file */
46
- const secretsBasename = applySuffix("secrets");
47
- /** Compute the default directories to look through */
48
- const getDefaultDirs = () => {
49
- /** Initial directory of the app. Hopefully the project root */
50
- const projectRoot = process.cwd();
51
- const parentDir = nodePath.resolve(projectRoot, "..");
52
- return parentDir !== projectRoot && parentDir ? [projectRoot, parentDir] : [projectRoot];
53
- };
54
- /** Directories to search for config/secrets into */
55
- const checkDirs = getDefaultDirs();
56
- /** Returns the first file found in `checkDirs`, filenames, and configured extensions */
57
- const getFirstFileFound = (filenames) => {
58
- for (const filepath of checkDirs) {
59
- for (const filename of filenames) {
60
- for (const extension of [".js", ".json", ".ts", ".cjs", ".mjs", ".cts", ".mts"]) {
61
- const candidate = nodePath.resolve(filepath, `${filename}${extension}`);
62
- if (nodeFs.existsSync(candidate))
63
- return candidate;
64
- }
65
- }
66
- }
67
- };
68
- /** Path to the default config file */
69
- const configPath = getFirstFileFound(configBasename);
70
- /** Path to the default secrets file */
71
- const secretsPath = getFirstFileFound(secretsBasename);
72
- const loadFile = async (path) => {
73
- const fileSuffix = nodePath.extname(path);
74
- if ([".js", ".ts", ".cjs", ".mjs", ".cts", ".mts"].includes(fileSuffix)) {
75
- const module = (await import(path));
76
- if (module.default)
77
- return module.default;
78
- return module;
79
- }
80
- if (fileSuffix === ".json")
81
- return JSON.parse(await nodeFs.promises.readFile(path, "utf8"));
82
- throw new Error(`Unexpected suffix: ${fileSuffix}`);
83
- };
84
- /** Return the content of a file */
85
- const getFromFile = async (filepath) => {
86
- if (filepath === undefined)
87
- return;
88
- return loadFile(filepath);
89
- };
90
- /**
91
- * Get config from the CLI.
92
- *
93
- * Check if `--${argName}` is present, and if so, tries to interpret it as JSON or as a file path to load.
94
- *
95
- * @returns
96
- * The loaded content, or `null` if the argument is not present.
97
- */
98
- const getFromCli = async (argv, argName) => {
99
- const configArgIndex = argv.indexOf(`--${argName}`);
100
- if (configArgIndex === -1)
101
- return null;
102
- const valueIndex = configArgIndex + 1;
103
- if (argv.length <= valueIndex)
104
- throw new Error(`--${argName} requires a string argument`);
105
- const value = argv[valueIndex];
106
- const valuesExtracted = 2;
107
- argv.splice(configArgIndex, valuesExtracted);
108
- if (value.startsWith("{")) {
109
- const configObj = JSON.parse(value);
110
- return configObj;
111
- }
112
- return JSON.parse(await nodeFs.promises.readFile(value, "utf8"));
113
- };
114
- /** Return the specified object from the first available source */
115
- const getBaseData = async (argv, argName, filepath) => (await getFromCli(argv, argName)) ?? getFromFile(filepath);
116
- /** Merge the base configuration and the secrets */
117
- const getProjectConfig = async (argv) => {
118
- const baseConfig = await getBaseData(argv, "config", configPath);
119
- if (!baseConfig)
120
- throw new Error("Missing base project configuration");
121
- const secrets = await getBaseData(argv, "secrets", secretsPath);
122
- if (secrets) {
123
- const merged = { ...baseConfig, secrets };
124
- return merged;
125
- }
126
- return baseConfig;
127
- };
128
- /** Cache of the loaded configuration */
129
- let config = null;
130
- /** Load the project configuration */
131
- const loadConfig = (argv) => {
132
- config ??= (async () => {
133
- try {
134
- const cfg = await getProjectConfig(argv);
135
- return cfg;
136
- }
137
- catch (error) {
138
- config = null;
139
- throw error;
140
- }
141
- })();
142
- return config;
143
- };
144
- export default await loadConfig(process.argv);
28
+ export { loadConfig } from "./loader.js";
@@ -0,0 +1,31 @@
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
+ /** 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;
package/lib/loader.js ADDED
@@ -0,0 +1,184 @@
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;
package/lib/types.d.ts CHANGED
@@ -27,3 +27,16 @@
27
27
  */
28
28
  /** Base for any returned configuration */
29
29
  export type BaseConfig = Record<string, any>;
30
+ /** Generic type predicate */
31
+ export type TypePredicate<Type> = (obj: unknown, throwOnError?: boolean) => obj is Type;
32
+ export interface ProjectLoadingConfig<ConfigType, SecretsType = never> {
33
+ /** Type predicate for the base configuration (without secrets) */
34
+ configPredicate?: TypePredicate<ConfigType>;
35
+ /** Type predicate for the secrets only */
36
+ secretsPredicate?: SecretsType extends never ? never : TypePredicate<SecretsType>;
37
+ }
38
+ export type ProjectConfig<ConfigType, SecretsType = never> = SecretsType extends never ? ConfigType : {
39
+ [key in keyof ConfigType]: ConfigType[key];
40
+ } & {
41
+ secrets: SecretsType;
42
+ };
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name":"@keeex/projectconfig","version":"4.2.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/types.js":"./lib/types.js","./types.js":"./lib/types.js"},"homepage":"https://keeex.me/oss"}
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"}