@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 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
- The main configuration should be placed at the root of the project, in a file
22
- named `config.js` or `config.json`.
23
- Same for the secrets, using a file named `secrets.js` or `secrets.json`.
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.js[on]` and
29
- `secrets-test.js[on]`.
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
- It is not possible to specify a different secret file from command line.
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
@@ -25,6 +25,6 @@
25
25
  * <mailto: contact@keeex.net>
26
26
  *
27
27
  */
28
- import { type BaseConfig } from "./types.js";
29
- declare const _default: BaseConfig;
28
+ import type * as types from "./types.js";
29
+ declare const _default: types.BaseConfig;
30
30
  export default _default;
package/lib/index.js CHANGED
@@ -25,178 +25,113 @@
25
25
  * <mailto: contact@keeex.net>
26
26
  *
27
27
  */
28
- /**
29
- * Load configuration from three places, whichever works first:
30
- * - command line, as a JSON after "--config"
31
- * - command line, as the path to a JSON file after "--config"
32
- * - config file, `${CWD}/config.js`
33
- *
34
- * It will automatically append a "secrets" property to the config object read
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
- const nodeEnvValue = getNodeEnv();
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
- * Config basename with suffix
61
- */
62
- const configBasename = (withSuffix) => `${defaultConfigBasename}${configSuffix(withSuffix)}`;
63
- /**
64
- * Secrets basename with suffix
65
- */
66
- const secretsBasename = (withSuffix) => `${defaultSecretsBasename}${configSuffix(withSuffix)}`;
67
- /**
68
- * Initial directory of the app. Hopefully the project root.
69
- */
70
- const projectRoot = process.cwd();
71
- /**
72
- * Try multiple suffix for a file path, return the first one that exist.
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
- * Return path to the config file
84
- */
85
- const getConfigPath = () => {
86
- const configPath = getFirstFileFound(projectRoot, configBasename(true), acceptableExtensions);
87
- if (configPath === undefined) {
88
- const configPathNoSuffix = getFirstFileFound(projectRoot, configBasename(false), acceptableExtensions);
89
- if (configPathNoSuffix === undefined) {
90
- throw new Error("No configuration file found");
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 suffix = extname(path);
115
- if ([".js", ".ts"].includes(suffix)) {
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 (suffix === ".json") {
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
- * Return the secrets data
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
- * Return the configuration object from the command line arguments
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 getConfigFromCLI = async () => {
141
- const cliArgName = defaultCliArgName;
142
- const configArgIndex = process.argv.indexOf(cliArgName);
143
- if (configArgIndex !== -1) {
144
- const configFromArgs = process.argv[configArgIndex + 1];
145
- const valuesExtracted = 2;
146
- process.argv.splice(configArgIndex, valuesExtracted);
147
- // eslint-disable-next-line @typescript-eslint/init-declarations
148
- let configObj;
149
- try {
150
- configObj = JSON.parse(configFromArgs);
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 null;
112
+ return JSON.parse(await nodeFs.promises.readFile(value, "utf8"));
158
113
  };
159
- /**
160
- * Load the project configuration from a file
161
- */
162
- const getConfigFromFile = async () => {
163
- const filePath = getConfigPath();
164
- return loadFile(filePath);
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
@@ -25,7 +25,5 @@
25
25
  * <mailto: contact@keeex.net>
26
26
  *
27
27
  */
28
- /**
29
- * Base for any returned configuration
30
- */
28
+ /** Base for any returned configuration */
31
29
  export type BaseConfig = Record<string, any>;
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name":"@keeex/projectconfig","version":"4.1.2","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":"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"}