Skip to content

Commit 917f651

Browse files
authored
Merge pull request #124 from bluecadet/feat/js-config
Add support for JS configs
2 parents 3f0f586 + 196ce9b commit 917f651

File tree

4 files changed

+130
-115
lines changed

4 files changed

+130
-115
lines changed

.changeset/chilled-eyes-kick.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@bluecadet/launchpad-dashboard": minor
3+
"@bluecadet/launchpad": minor
4+
"@bluecadet/launchpad-scaffold": minor
5+
"@bluecadet/launchpad-content": minor
6+
"@bluecadet/launchpad-monitor": minor
7+
"@bluecadet/launchpad-utils": minor
8+
---
9+
10+
support js configs

packages/launchpad/README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ graph LR
3131
## Getting Started
3232

3333
1. Install launchpad: `npm i @bluecadet/launchpad`
34-
2. Create a `launchpad.json` config (see [configuration](#configuration))
34+
2. Create a `launchpad.config.js` config (see [configuration](#configuration))
3535
3. *Optional: [Bootstrap](/packages/scaffold) your PC with `npx launchpad scaffold`*
3636
4. Run `npx launchpad`
3737

@@ -43,10 +43,10 @@ Run `npx launchpad --help` to see all available commands.
4343

4444
## Configuration
4545

46-
Each [launchpad package](#packages) is configured via its own section in `launchpad.json`. Below is a simple example that uses the [`content`](/packages/content) package to download JSON and images from Flickr and [`monitor`](/packages/monitor) to launch a single app:
46+
Each [launchpad package](#packages) is configured via its own section in `launchpad.config.js`. Below is a simple example that uses the [`content`](/packages/content) package to download JSON and images from Flickr and [`monitor`](/packages/monitor) to launch a single app:
4747

48-
```json
49-
{
48+
```js
49+
export default {
5050
"content": {
5151
"sources": [
5252
{
@@ -91,7 +91,7 @@ All available config settings across packages can be found in the links below:
9191

9292
### Config Loading
9393

94-
- By default, Launchpad looks for `launchpad.json` or `config.json` at the cwd (where you ran `npx launchpad`/`launchpad` from)
94+
- By default, Launchpad looks for `launchpad.config.js`, `launchpad.config.mjs`, `launchpad.json` or `config.json` at the cwd (where you ran `npx launchpad`/`launchpad` from)
9595
- You can change the default path with `--config=<YOUR_FILE_PATH>` (e.g. `npx launchpad --config=../settings/my-config.json`)
9696
- If no config is found, Launchpad will traverse up directories (up to 64) to find one
9797
- All config values can be overridden via `--foo=bar` (e.g. `--logging.level=debug`)

packages/utils/lib/config-manager.js

Lines changed: 96 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,17 @@
1-
import yargs from 'yargs';
2-
import { hideBin } from 'yargs/helpers';
31
import fs from 'fs-extra';
42
import path from 'path';
53
import url from 'url';
64
import stripJsonComments from 'strip-json-comments';
75
import chalk from 'chalk';
86

9-
export class ConfigManagerOptions {
10-
static DEFAULT_CONFIG_PATHS = ['launchpad.json', 'config.json'];
11-
12-
constructor({
13-
configPaths = ConfigManagerOptions.DEFAULT_CONFIG_PATHS,
14-
...rest
15-
} = {}) {
16-
/**
17-
* The path where to load the config from.
18-
* If an array of paths is passed, all found configs will be merged in that order.
19-
* @type {string|Array<string>}
20-
*/
21-
this.configPaths = configPaths;
22-
23-
// Allows for additional properties to be inherited
24-
Object.assign(this, rest);
25-
}
26-
}
7+
const DEFAULT_CONFIG_PATHS = ['launchpad.config.js', 'launchpad.config.mjs', 'launchpad.json', 'config.json'];
278

9+
/**
10+
* @template T config type
11+
*/
2812
export class ConfigManager {
29-
/** @type {ConfigManager | null} */
30-
static _instance = null;
31-
32-
/** @returns {ConfigManager} */
33-
static getInstance() {
34-
if (this._instance === null) {
35-
this._instance = new ConfigManager();
36-
}
37-
return this._instance;
38-
}
39-
40-
/** @type {ConfigManagerOptions} */
41-
_config = new ConfigManagerOptions();
13+
/** @type {Partial<T>} */
14+
_config = {};
4215

4316
/** @type {boolean} */
4417
_isLoaded = false;
@@ -55,9 +28,10 @@ export class ConfigManager {
5528
/**
5629
* Imports a JS config from a set of paths. The JS files have to export
5730
* its config as the default export. Will return the first config found.
58-
* @param {Array.<string>} paths
31+
* @template T
32+
* @param {Array<string>} paths
5933
* @param {ImportMeta?} importMeta The import.meta property of the file at your base directory.
60-
* @returns {Promise<object | null>} The parsed config object or null if none can be found
34+
* @returns {Promise<T | null>} The parsed config object or null if none can be found
6135
*/
6236
static async importJsConfig(paths, importMeta = null) {
6337
const __dirname = ConfigManager.getProcessDirname(importMeta);
@@ -77,47 +51,50 @@ export class ConfigManager {
7751

7852
/**
7953
* Loads the config in the following order of overrides:
80-
* defaults < json < user < argv
54+
* defaults < js/json < user
8155
*
82-
* @param {ConfigManagerOptions|object?} userConfig Optional config overrides
83-
* @param {((conf: import("yargs").Argv) => import("yargs").Argv)?} yargsCallback Optional function to further configure yargs startup options.
84-
* @returns {object} A promise with the current config.
56+
* @param {Partial<T>?} userConfig Optional config overrides
57+
* @param {string} [configPath] Optional path to a config file, relative to the current working directory.
58+
* @returns {Promise<Partial<T>>} A promise with the current config.
8559
*/
86-
loadConfig(userConfig = null, yargsCallback = null) {
60+
async loadConfig(userConfig = null, configPath) {
61+
if (configPath) {
62+
// if config is manually specified, load it without searching parent directories,
63+
// and fail if it doesn't exist
64+
const resolved = path.resolve(configPath);
65+
if (!fs.existsSync(resolved)) {
66+
throw new Error(`Could not find config at '${resolved}'`);
67+
}
68+
69+
this._config = { ...this._config, ...ConfigManager._loadConfigFromFile(resolved) };
70+
} else {
71+
// if no config is specified, search current and parent directories for default config files.
72+
// Only the first found config will be loaded.
73+
for (const defaultPath of DEFAULT_CONFIG_PATHS) {
74+
const resolved = ConfigManager._findFirstFileRecursive(defaultPath);
75+
76+
if (resolved) {
77+
console.warn(`Found config at '${chalk.white(resolved)}'`);
78+
this._config = { ...this._config, ...(await ConfigManager._loadConfigFromFile(resolved)) };
79+
break;
80+
}
81+
82+
console.warn(`Could not find config with name '${chalk.white(defaultPath)}'`);
83+
}
84+
}
85+
86+
// user config overrides js/json config
8787
if (userConfig) {
8888
this._config = { ...this._config, ...userConfig };
8989
}
9090

91-
let argv = yargs(hideBin(process.argv))
92-
.parserConfiguration({
93-
// See https://github.com/yargs/yargs-parser#camel-case-expansion
94-
'camel-case-expansion': false
95-
})
96-
.config('config', 'Path to your config file. Can contain comments.', this._loadConfigFromFile.bind(this));
97-
98-
if (yargsCallback) {
99-
argv = yargsCallback(argv);
100-
}
101-
102-
const parsedArgv = argv.help().parse();
103-
104-
this._config = { ...this._config, ...parsedArgv };
105-
106-
// console.log(this._config);
107-
108-
if (!('config' in parsedArgv)) {
109-
for (const configPath of this._config.configPaths) {
110-
this._config = { ...this._config, ...this._loadConfigFromFile(configPath) };
111-
}
112-
}
113-
11491
this._isLoaded = true;
11592
return this._config;
11693
}
11794

11895
/**
11996
* Retrieves the current config object.
120-
* @returns {ConfigManagerOptions}
97+
* @returns {Partial<T>}
12198
*/
12299
getConfig() {
123100
return this._config;
@@ -137,63 +114,74 @@ export class ConfigManager {
137114
isLoaded() {
138115
return this._isLoaded;
139116
}
117+
118+
/**
119+
* @param {string} filePath
120+
* @returns {string | null} The absolute path to the file or null if it doesn't exist.
121+
* @private
122+
*/
123+
static _findFirstFileRecursive(filePath) {
124+
const maxDepth = 64;
125+
126+
let absPath = filePath;
127+
128+
if (process.env.INIT_CWD) {
129+
absPath = path.resolve(process.env.INIT_CWD, filePath);
130+
} else {
131+
absPath = path.resolve(filePath);
132+
}
133+
134+
for (let i = 0; i < maxDepth; i++) {
135+
if (fs.existsSync(absPath)) {
136+
return absPath;
137+
}
138+
139+
const dirPath = path.dirname(absPath);
140+
const filePath = path.basename(absPath);
141+
const parentPath = path.resolve(dirPath, '..', filePath);
142+
143+
if (absPath === parentPath) {
144+
// Can't navigate any more levels up
145+
break;
146+
}
147+
148+
absPath = parentPath;
149+
}
150+
151+
return null;
152+
}
140153

141154
/**
155+
* @template T
142156
* @param {string} configPath
157+
* @returns {Promise<Partial<T>>}
158+
* @private
143159
*/
144-
_loadConfigFromFile(configPath) {
160+
static async _loadConfigFromFile(configPath) {
145161
if (!configPath) {
146162
return {};
147163
}
148164

149-
let absPath = configPath;
150-
151-
if (process.env.INIT_CWD && !fs.existsSync(configPath)) {
152-
absPath = path.resolve(process.env.INIT_CWD, configPath);
153-
}
154-
155165
try {
156-
const maxLevels = 64;
157-
for (let i = 0; i < maxLevels; i++) {
158-
const resolvedPath = path.resolve(absPath);
159-
160-
// console.debug(chalk.gray(`Trying to load config from ${chalk.white(resolvedPath)}`));
161-
162-
if (fs.existsSync(resolvedPath)) {
163-
absPath = resolvedPath;
164-
console.info(chalk.gray(`Loading config from ${chalk.white(absPath)}`));
165-
break;
166-
} else if (i >= maxLevels) {
167-
throw new Error(`No config found at '${chalk.white(configPath)}'.`);
168-
} else {
169-
const dirPath = path.dirname(absPath);
170-
const filePath = path.basename(absPath);
171-
const parentPath = path.resolve(dirPath, '..', filePath);
172-
173-
if (absPath === parentPath) {
174-
// Can't navigate any more levels up
175-
throw new Error(`No config found at '${chalk.white(configPath)}'.`);
176-
}
177-
178-
absPath = parentPath;
166+
// if suffix is json, parse as json
167+
if (configPath.endsWith('.json')) {
168+
console.warn(chalk.yellow('JSON config files are deprecated. Please use JS config files instead.'));
169+
170+
const configStr = fs.readFileSync(configPath, 'utf8');
171+
const config = JSON.parse(stripJsonComments(configStr));
172+
173+
if (!config) {
174+
throw new Error(`Could not parse config from '${chalk.white(configPath)}'`);
179175
}
176+
177+
return config;
178+
} else {
179+
// otherwise, parse as js
180+
return (await import(configPath)).default;
180181
}
181-
182-
const configStr = fs.readFileSync(absPath, 'utf8');
183-
const config = JSON.parse(stripJsonComments(configStr));
184-
185-
if (!config) {
186-
throw new Error(`Could not parse config from '${chalk.white(configPath)}'`);
187-
}
188-
189-
return config;
190182
} catch (err) {
191-
if (err instanceof Error) {
192-
console.warn(`${err.message}`);
193-
}
183+
throw new Error(`Unable to load config file '${chalk.white(configPath)}'`, { cause: err });
194184
}
195-
196-
return {};
197185
}
198186
}
199187

packages/utils/lib/launch-from-cli.js

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import url from 'url';
22

33
import ConfigManager from './config-manager.js';
44
import LogManager from './log-manager.js';
5+
import yargs from 'yargs';
6+
import { hideBin } from 'yargs/helpers';
57

68
/**
79
* Resolves with a promise including the current config if your
@@ -31,10 +33,25 @@ export const launchFromCli = async (importMeta, {
3133
// eslint-disable-next-line prefer-promise-reject-errors
3234
return Promise.reject();
3335
}
36+
37+
let argv = yargs(hideBin(process.argv))
38+
.parserConfiguration({
39+
// See https://github.com/yargs/yargs-parser#camel-case-expansion
40+
'camel-case-expansion': false
41+
})
42+
.option('config', { alias: 'c', describe: 'Path to your JS or JSON config file.', type: 'string' }).help();
43+
44+
if (yargsCallback) {
45+
argv = yargsCallback(argv);
46+
}
47+
48+
const parsedArgv = await argv.parse();
49+
50+
const configManager = new ConfigManager();
3451

35-
ConfigManager.getInstance().loadConfig(userConfig, yargsCallback);
52+
await configManager.loadConfig({ ...userConfig, ...parsedArgv }, parsedArgv.config);
3653
/** @type {any} TODO: figure out where to add this 'logging' property */
37-
const config = ConfigManager.getInstance().getConfig();
54+
const config = configManager.getConfig();
3855
LogManager.getInstance(config.logging || config);
3956

4057
return Promise.resolve(config);

0 commit comments

Comments
 (0)