A very simple configuration management for your projects. It takes care of loading, activating, switching and merging configuration files.
⚠️ This package is only for Node.
- ⚙️ Examples
- 🤘 Development
Let's say your app tree looks like this:
myApp/
├── config/
│ ├── development.js
│ └── production.js
└── app/
└── index.js
And you execute your app with something like:
node app
If you don't have anything managing the configurations, you would probably have something like this:
const config = (
await (process.NODE_ENV === 'production'
? import('../config/production')
: import('../config/development'))
).default;
And the, development.js
probably imports production
, and has some deep merge logic, to avoid re writing the whole thing.
Maybe is different, but we can simplify it a little bit with simpleConfig
:
import { simpleConfig } from '@homer0/simple-config';
const config = simpleConfig({
filenameFormat: '[name].js',
path: 'config',
});
// ... sometime later, because there's not top-level await yet.
await config.load(process.NODE_ENV || 'development');
Now you can go to development
and just add a key extends
with the value production
, and simpleConfig
will take care of loading the file you need, and deep merge the one for development.
The reason the default options needed to be changed was because:
-
filenameFormat
: To avoid changing the name of the files, since the service looks for[configName].[name].config.js
by default. -
path
: To avoid creating another directory, since the service looks for files inconfig/[configName]
by default.
const config = simpleConfig({
name: 'myApp',
});
The name
option is used to generate the default values of multiple options:
-
defaultConfigFilename
:myApp.config.js
. -
envVarName
:MY_APP_CONFIG
. -
path
:config/myApp
. -
filenameFormat
:myApp.[name].config.js
.
In the first example, we went directly to load a configuration file, but the service can also be used with the static default configuration:
const config = simpleConfig({
defaultConfig: {
foo: 'bar',
},
});
const foo = config.get<string>('foo');
Since the service can hold many configurations, you can also specify the name of the default one with the defaultConfigName
option:
const config = simpleConfig({
defaultConfigName: 'fooBar',
defaultConfig: {
foo: 'bar',
},
});
And, if you want the default configuration to be loaded from a specific file, you can specify its name with the defaultConfigFilename
option:
const config = simpleConfig({
defaultConfigFilename: 'fooBar.config.js',
});
// ...
await config.loadFromFile();
In the first example, we were using the value of NODE_ENV
to decide which configuration to load, but we could also use a specific env var to tell the service which configuration to load:
APP_CONFIG=development node app
Then...
const config = simpleConfig();
// ...
await config.loadFromEnv();
Using the defaults, the service will look at APP_CONFIG
, and try to load config/app.development.config.js
, if it exists.
By default, and using the name
option, the service will look for the files in config/[name]
, relative to the project root; this can be changed with the path
option, by sending another path, also relative to the project root:
const config = simpleConfig({
path: '.config',
defaultConfigFilename: 'config.js',
});
// ...
await config.loadFromFile();
With that, it will look for .config/config.js
instead of config/app/app.config.js
.
While developing an app, or when debugging it, it may be useful to switch between different configurations, and that's why the service gives you the possiblity of enabling this feature:
const config = simpleConfig({
allowConfigSwitch: true,
});
// ...
await config.switch('anotherConfig');
With that, the service will try to switch the active configuration to the one specified, and if it's not loaded, it will try to load it from a file.
The service has another option relevant to this feature, which is the allowConfigSwitchSetting
: When this setting is enabled, which it is by default, whenever the service loads a configuration, it will check if it has a boolean allowConfigSwitch
property, and if it does, it will set the service property to the new value.
When calling the load*
methods, the service ignores the allowConfigSwitch
option, but if the implementation uses .switch
, you can specify allowConfigSwitch: false
on your configuration file, so once the file is loaded, the feature will be disabled.
By default, all the configurations extend from the default one (sent when the service is initialized), but you can include an extends
key on the configurations with the name of the one you want to extend.
Whenever the service tries to load a configuration that extends from one that is not registered, it will attempt to load it from a file.
The service is not only about managing files, it also comes with utility methods to read and write on the configurations:
Get a single setting:
const value = config.get('some-setting');
Get multiple settings:
const { settingOne, settingTwo } = config.get(['settingOne', 'settingTwo']);
Get multiple settings as an array:
const [settingOne, settingTwo] = config.get(['settingOne', 'settingTwo'], true);
And since the service uses my own object-utils
, you can also use the get
method with a dot-notation:
const value = config.get('some.setting');
// or
const [settingOne, settingTwo] = config.get(
['one.group.setting', 'two.something.else'],
true,
);
Write a single setting:
config.set('some-setting', 'some-value');
Write multiple settings:
config.set({
settingOne: 'valueOne',
settingTwo: 'valueTwo',
});
And, like get
, you can use dot notation:
config.set('some.setting', 'some-value');
// or
config.set({
'one.group.setting': 'valueOne',
'two.something.else': 'valueTwo',
});
A special feature of the set
method is that, by default, if the new value and the old values are both objects, instead of overwriting the old one, it will deep merge the new one into the old one:
config.set('person', {
name: 'Rosario',
birthday: '',
});
// ...
config.set('person', {
birthday: '25-09-2015',
});
/**
* The current value of the setting would be:
* {
* name: 'Rosario',
* birthday: '25-09-2015',
* }
*/
You can disable this functionality by sending a third parameter to the set
method:
config.set(
'person',
{
birthday: '25-09-2015',
},
false,
);
/**
* The current value of the setting would be:
* {
* birthday: '25-09-2015',
* }
*/
If your app uses a Jimple container, you can register SimpleConfig
as the config
service by using its provider:
import { simpleConfigProvider } from '@homer0/simple-config';
// ...
container.register(simpleConfigProvider);
// ...
const config = container.get('config');
And since the provider is a "provider creator" (created with my custom version of Jimple), you can customize its service name, and options:
container.register(
simpleConfigProvider({
serviceName: 'myConfig',
path: '.config',
defaultConfigFilename: 'config.js',
}),
);
SimpleConfig
depends on the following services, and when used with Jimple, it will try to find them in the container, otherwise, it will create new instances:
-
@homer0/env-utils
, with the nameenvUtils
. Used to get the check the environment variable. -
@homer0/root-file
, with the namerootFile
. Used to get import the configuration files. -
@homer0/path-utils
, with the namepathUtils
. Needed byrootFile
to generate the paths relative to the project root.
If you already implement the dependencies, but with a different name, you can specify them in the provider:
container.register(
simpleConfigProvider({
services: {
envUtils: 'myEnvUtils',
rootFile: 'myRootFile',
pathUtils: 'myPathUtils',
},
}),
);
As this project is part of the packages
monorepo, some of the tooling, like lint-staged
and husky
, are installed on the root's package.json
.
Task | Description |
---|---|
lint |
Lints the package. |
test |
Runs the unit tests. |
build |
Transpiles the project. |
types:check |
Validates the TypeScript types. |