@keeex/projectconfig 4.1.2 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -18
- package/lib/env.d.ts +2 -6
- package/lib/env.js +2 -6
- package/lib/index.d.ts +2 -2
- package/lib/index.js +80 -145
- package/lib/types.d.ts +1 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,31 +4,30 @@ Load project for a project from the following places, whichever works first:
|
|
|
4
4
|
|
|
5
5
|
- Command line argument, as a JSON string
|
|
6
6
|
- Command line argument, as a path to a JSON/JS file
|
|
7
|
-
- From a file at the root of the project
|
|
7
|
+
- From a file at the root of the project, or in the parent directory of the root of the project
|
|
8
8
|
|
|
9
|
-
Additionaly, it will automatically sideload a "secrets" file, for settings that
|
|
10
|
-
should not remain in a shared configuration file.
|
|
11
|
-
The secrets file is expected to be named "secrets.js[on]" at the root of the
|
|
12
|
-
project.
|
|
13
|
-
It can be a symlink.
|
|
14
|
-
If no secret file is found at the root of the project, this library will try to
|
|
15
|
-
load it one directory above the project.
|
|
9
|
+
Additionaly, it will automatically sideload a "secrets" file, for settings that should not remain in a shared configuration file.
|
|
16
10
|
|
|
17
11
|
## Usage
|
|
18
12
|
|
|
19
13
|
### Configuration location
|
|
20
14
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
When loading configuration from files without providing the path to the file, the following locations/names are checked:
|
|
16
|
+
|
|
17
|
+
- `<projectRoot>/config.*`
|
|
18
|
+
- `<projectRoot>/../config.*`
|
|
19
|
+
|
|
20
|
+
The file extensions `.js`, `.json`, `.ts`, `.cjs`, `.mjs`, `.cts`, `.mts` are checked, the first one found is used.
|
|
21
|
+
For JavaScript/TypeScript files, the default export is expected to contain the configuration.
|
|
22
|
+
If there is no default export, all other exports are handled as if they were a top-level object.
|
|
23
|
+
|
|
24
|
+
Secrets file can be placed in the same location (project root or one directory above), and follow the same rules regarding file extensions.
|
|
24
25
|
|
|
25
26
|
#### Overriding default file names
|
|
26
27
|
|
|
27
28
|
For testing purpose it can be useful to override the file names locally.
|
|
28
|
-
If `NODE_ENV` is set to `test`, file names will be `config-test
|
|
29
|
-
`
|
|
30
|
-
Alternatively, if `KEEEX_CONFIG_SUFFIX` is set, it will be appended to the base
|
|
31
|
-
names (with no dash).
|
|
29
|
+
If `NODE_ENV` is set to `test`, file names will be `config-test.*` and `secrets-test.*`.
|
|
30
|
+
Alternatively, if `KEEEX_CONFIG_SUFFIX` is set, it will be appended to the base names (with no dash).
|
|
32
31
|
The `KEEEX_CONFIG_SUFFIX` option takes precedence over `NODE_ENV`.
|
|
33
32
|
|
|
34
33
|
In any case, the default names (without any suffix) will be used as fallback.
|
|
@@ -38,10 +37,9 @@ In any case, the default names (without any suffix) will be used as fallback.
|
|
|
38
37
|
It is possible to load a different configuration from command line.
|
|
39
38
|
Passing `--config <some json>` will load that JSON instead of the config file.
|
|
40
39
|
|
|
41
|
-
One can also pass `--config <path to json file>` to load a different config from
|
|
42
|
-
another file.
|
|
40
|
+
One can also pass `--config <path to json file>` to load a different config from another file.
|
|
43
41
|
|
|
44
|
-
|
|
42
|
+
The same can be done for secrets with `--secrets`.
|
|
45
43
|
|
|
46
44
|
### Accessing the configuration
|
|
47
45
|
|
package/lib/env.d.ts
CHANGED
|
@@ -25,11 +25,7 @@
|
|
|
25
25
|
* <mailto: contact@keeex.net>
|
|
26
26
|
*
|
|
27
27
|
*/
|
|
28
|
-
/**
|
|
29
|
-
* Return the config suffix from env value
|
|
30
|
-
*/
|
|
28
|
+
/** Return the config suffix from env value */
|
|
31
29
|
export declare const getConfigSuffix: () => string | undefined;
|
|
32
|
-
/**
|
|
33
|
-
* Return the current NODE_ENV value (default to `production`)
|
|
34
|
-
*/
|
|
30
|
+
/** Return the current NODE_ENV value (default to `production`) */
|
|
35
31
|
export declare const getNodeEnv: () => string;
|
package/lib/env.js
CHANGED
|
@@ -25,11 +25,7 @@
|
|
|
25
25
|
* <mailto: contact@keeex.net>
|
|
26
26
|
*
|
|
27
27
|
*/
|
|
28
|
-
/**
|
|
29
|
-
* Return the config suffix from env value
|
|
30
|
-
*/
|
|
28
|
+
/** Return the config suffix from env value */
|
|
31
29
|
export const getConfigSuffix = () => process.env.KEEEX_CONFIG_SUFFIX;
|
|
32
|
-
/**
|
|
33
|
-
* Return the current NODE_ENV value (default to `production`)
|
|
34
|
-
*/
|
|
30
|
+
/** Return the current NODE_ENV value (default to `production`) */
|
|
35
31
|
export const getNodeEnv = () => process.env.NODE_ENV ?? "production";
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -25,178 +25,113 @@
|
|
|
25
25
|
* <mailto: contact@keeex.net>
|
|
26
26
|
*
|
|
27
27
|
*/
|
|
28
|
-
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
* from `${CWD}/secrets.js[on]`.
|
|
36
|
-
*/
|
|
37
|
-
import { existsSync } from "fs";
|
|
38
|
-
import { readFile } from "fs/promises";
|
|
39
|
-
import { join, resolve, extname } from "path";
|
|
40
|
-
import { getConfigSuffix, getNodeEnv } from "./env.js";
|
|
41
|
-
const defaultCliArgName = "--config";
|
|
42
|
-
const defaultConfigBasename = "config";
|
|
43
|
-
const defaultSecretsBasename = "secrets";
|
|
44
|
-
const acceptableExtensions = [".js", ".json", ".ts"];
|
|
45
|
-
const configSuffix = (useSuffix) => {
|
|
46
|
-
if (!useSuffix) {
|
|
47
|
-
return "";
|
|
48
|
-
}
|
|
49
|
-
const suffixFromEnv = getConfigSuffix();
|
|
50
|
-
if (suffixFromEnv !== undefined) {
|
|
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)
|
|
51
35
|
return suffixFromEnv;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (nodeEnvValue === "test") {
|
|
55
|
-
return "-test";
|
|
56
|
-
}
|
|
57
|
-
return "";
|
|
36
|
+
const nodeEnvValue = env.getNodeEnv();
|
|
37
|
+
return nodeEnvValue === "test" ? "-test" : null;
|
|
58
38
|
};
|
|
59
|
-
/**
|
|
60
|
-
|
|
61
|
-
*/
|
|
62
|
-
const
|
|
63
|
-
/**
|
|
64
|
-
|
|
65
|
-
*/
|
|
66
|
-
const secretsBasename = (
|
|
67
|
-
/**
|
|
68
|
-
|
|
69
|
-
*/
|
|
70
|
-
const projectRoot = process.cwd();
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
*/
|
|
74
|
-
const getFirstFileFound = (basepath, basename, extensions) => {
|
|
75
|
-
for (const extension of extensions) {
|
|
76
|
-
const candidate = join(basepath, `${basename}${extension}`);
|
|
77
|
-
if (existsSync(candidate)) {
|
|
78
|
-
return candidate;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
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, `${basestr}${suffix}`] : [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];
|
|
81
53
|
};
|
|
82
|
-
/**
|
|
83
|
-
|
|
84
|
-
*/
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
+
}
|
|
91
65
|
}
|
|
92
|
-
return configPathNoSuffix;
|
|
93
|
-
}
|
|
94
|
-
return configPath;
|
|
95
|
-
};
|
|
96
|
-
/**
|
|
97
|
-
* Return path to the secrets file
|
|
98
|
-
*/
|
|
99
|
-
const getSecretsPath = () => {
|
|
100
|
-
const secretsPathWithSuffix = getFirstFileFound(projectRoot, secretsBasename(true), acceptableExtensions);
|
|
101
|
-
if (secretsPathWithSuffix) {
|
|
102
|
-
return secretsPathWithSuffix;
|
|
103
|
-
}
|
|
104
|
-
const secretsPathNoSuffix = getFirstFileFound(projectRoot, secretsBasename(false), acceptableExtensions);
|
|
105
|
-
if (secretsPathNoSuffix) {
|
|
106
|
-
return secretsPathNoSuffix;
|
|
107
|
-
}
|
|
108
|
-
const secretsPathAbove = getFirstFileFound(resolve(projectRoot, ".."), secretsBasename(false), acceptableExtensions);
|
|
109
|
-
if (secretsPathAbove) {
|
|
110
|
-
return secretsPathAbove;
|
|
111
66
|
}
|
|
112
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);
|
|
113
72
|
const loadFile = async (path) => {
|
|
114
|
-
const
|
|
115
|
-
if ([".js", ".ts"].includes(
|
|
73
|
+
const fileSuffix = nodePath.extname(path);
|
|
74
|
+
if ([".js", ".ts", ".cjs", ".mjs", ".cts", ".mts"].includes(fileSuffix)) {
|
|
116
75
|
const module = (await import(path));
|
|
117
|
-
if (module.default)
|
|
76
|
+
if (module.default)
|
|
118
77
|
return module.default;
|
|
119
|
-
}
|
|
120
78
|
return module;
|
|
121
79
|
}
|
|
122
|
-
if (
|
|
123
|
-
return JSON.parse(await readFile(path, "utf8"));
|
|
124
|
-
}
|
|
125
|
-
throw new Error(`Unexpected suffix: ${suffix}`);
|
|
80
|
+
if (fileSuffix === ".json")
|
|
81
|
+
return JSON.parse(await nodeFs.promises.readFile(path, "utf8"));
|
|
82
|
+
throw new Error(`Unexpected suffix: ${fileSuffix}`);
|
|
126
83
|
};
|
|
127
|
-
/**
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const getSecrets = async () => {
|
|
131
|
-
const secretsPath = getSecretsPath();
|
|
132
|
-
if (secretsPath === undefined) {
|
|
84
|
+
/** Return the content of a file */
|
|
85
|
+
const getFromFile = async (filepath) => {
|
|
86
|
+
if (filepath === undefined)
|
|
133
87
|
return;
|
|
134
|
-
|
|
135
|
-
return loadFile(secretsPath);
|
|
88
|
+
return loadFile(filepath);
|
|
136
89
|
};
|
|
137
90
|
/**
|
|
138
|
-
*
|
|
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.
|
|
139
97
|
*/
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
catch {
|
|
153
|
-
configObj = JSON.parse(await readFile(configFromArgs, "utf8"));
|
|
154
|
-
}
|
|
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);
|
|
155
110
|
return configObj;
|
|
156
111
|
}
|
|
157
|
-
return
|
|
112
|
+
return JSON.parse(await nodeFs.promises.readFile(value, "utf8"));
|
|
158
113
|
};
|
|
159
|
-
/**
|
|
160
|
-
|
|
161
|
-
*/
|
|
162
|
-
const
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
* Return the configuration object from the first available source
|
|
168
|
-
*/
|
|
169
|
-
const getBaseProjectConfig = async () => {
|
|
170
|
-
const configFromCLI = await getConfigFromCLI();
|
|
171
|
-
if (configFromCLI !== null) {
|
|
172
|
-
return configFromCLI;
|
|
173
|
-
}
|
|
174
|
-
return getConfigFromFile();
|
|
175
|
-
};
|
|
176
|
-
/**
|
|
177
|
-
* Merge the base configuration and the secrets
|
|
178
|
-
*/
|
|
179
|
-
const getProjectConfig = async () => {
|
|
180
|
-
const baseConfig = await getBaseProjectConfig();
|
|
181
|
-
const secrets = await getSecrets();
|
|
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);
|
|
182
122
|
if (secrets) {
|
|
183
|
-
const merged = {
|
|
184
|
-
...baseConfig,
|
|
185
|
-
secrets,
|
|
186
|
-
};
|
|
123
|
+
const merged = { ...baseConfig, secrets };
|
|
187
124
|
return merged;
|
|
188
125
|
}
|
|
189
126
|
return baseConfig;
|
|
190
127
|
};
|
|
191
|
-
/**
|
|
192
|
-
* Cache of the loaded configuration
|
|
193
|
-
*/
|
|
128
|
+
/** Cache of the loaded configuration */
|
|
194
129
|
let config = null;
|
|
195
130
|
/** Load the project configuration */
|
|
196
|
-
const loadConfig = () => {
|
|
131
|
+
const loadConfig = (argv) => {
|
|
197
132
|
config ??= (async () => {
|
|
198
133
|
try {
|
|
199
|
-
const cfg = await getProjectConfig();
|
|
134
|
+
const cfg = await getProjectConfig(argv);
|
|
200
135
|
return cfg;
|
|
201
136
|
}
|
|
202
137
|
catch (error) {
|
|
@@ -206,4 +141,4 @@ const loadConfig = () => {
|
|
|
206
141
|
})();
|
|
207
142
|
return config;
|
|
208
143
|
};
|
|
209
|
-
export default await loadConfig();
|
|
144
|
+
export default await loadConfig(process.argv);
|
package/lib/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"@keeex/projectconfig","version":"4.
|
|
1
|
+
{"name":"@keeex/projectconfig","version":"4.2.0","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"}
|