c12 1.3.0 → 1.4.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 +44 -3
- package/dist/index.cjs +82 -0
- package/dist/index.d.ts +27 -1
- package/dist/index.mjs +82 -1
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ Smart Configuration Loader.
|
|
|
16
16
|
- Reads config from the nearest `package.json` file
|
|
17
17
|
- [Extends configurations](https://github.com/unjs/c12#extending-configuration) from multiple local or git sources
|
|
18
18
|
- Overwrite with [environment-specific configuration](#environment-specific-configuration)
|
|
19
|
+
- Config watcher with auto-reload and HMR support
|
|
19
20
|
|
|
20
21
|
## Usage
|
|
21
22
|
|
|
@@ -36,10 +37,10 @@ Import:
|
|
|
36
37
|
|
|
37
38
|
```js
|
|
38
39
|
// ESM
|
|
39
|
-
import { loadConfig } from "c12";
|
|
40
|
+
import { loadConfig, watchConfig } from "c12";
|
|
40
41
|
|
|
41
42
|
// CommonJS
|
|
42
|
-
const { loadConfig } = require("c12");
|
|
43
|
+
const { loadConfig, watchConfig } = require("c12");
|
|
43
44
|
```
|
|
44
45
|
|
|
45
46
|
Load configuration:
|
|
@@ -59,7 +60,7 @@ c12 merged config sources with [unjs/defu](https://github.com/unjs/defu) by belo
|
|
|
59
60
|
1. Config overrides passed by options
|
|
60
61
|
2. Config file in CWD
|
|
61
62
|
3. RC file in CWD
|
|
62
|
-
4. Global RC file in user's home directory
|
|
63
|
+
4. Global RC file in the user's home directory
|
|
63
64
|
5. Config from `package.json`
|
|
64
65
|
6. Default config passed by options
|
|
65
66
|
7. Extended config layers
|
|
@@ -234,6 +235,46 @@ c12 tries to match [`envName`](#envname) and override environment config if spec
|
|
|
234
235
|
}
|
|
235
236
|
```
|
|
236
237
|
|
|
238
|
+
## Watching Configuration
|
|
239
|
+
|
|
240
|
+
you can use `watchConfig` instead of `loadConfig` to load config and watch for changes, add and removals in all expected configuration paths and auto reload with new config.
|
|
241
|
+
|
|
242
|
+
### Lifecycle hooks
|
|
243
|
+
|
|
244
|
+
- `onWatch`: This function is always called when config is updated, added, or removed before attempting to reload the config.
|
|
245
|
+
- `acceptHMR`: By implementing this function, you can compare old and new functions and return `true` if a full reload is not needed.
|
|
246
|
+
- `onUpdate`: This function is always called after the new config is updated. If `acceptHMR` returns true, it will be skipped.
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
import { watchConfig } from "c12";
|
|
250
|
+
|
|
251
|
+
const config = watchConfig({
|
|
252
|
+
cwd: ".",
|
|
253
|
+
// chokidarOptions: {}, // Default is { ignoreInitial: true }
|
|
254
|
+
// debounce: 200 // Default is 100. You can set it to false to disable debounced watcher
|
|
255
|
+
onWatch: (event) => {
|
|
256
|
+
console.log("[watcher]", event.type, event.path);
|
|
257
|
+
},
|
|
258
|
+
acceptHMR({ oldConfig, newConfig, getDiff }) {
|
|
259
|
+
const diff = getDiff();
|
|
260
|
+
if (diff.length === 0) {
|
|
261
|
+
console.log("No config changed detected!");
|
|
262
|
+
return true; // No changes!
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
onUpdate({ oldConfig, newConfig, getDiff }) {
|
|
266
|
+
const diff = getDiff();
|
|
267
|
+
console.log("Config updated:\n" + diff.map((i) => i.toJSON()).join("\n"));
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
console.log("watching config files:", config.watchingFiles);
|
|
272
|
+
console.log("initial config", config.config);
|
|
273
|
+
|
|
274
|
+
// Stop watcher when not needed anymore
|
|
275
|
+
// await config.unwatch();
|
|
276
|
+
```
|
|
277
|
+
|
|
237
278
|
## 💻 Development
|
|
238
279
|
|
|
239
280
|
- Clone this repository
|
package/dist/index.cjs
CHANGED
|
@@ -9,6 +9,9 @@ const createJiti = require('jiti');
|
|
|
9
9
|
const rc9 = require('rc9');
|
|
10
10
|
const defu = require('defu');
|
|
11
11
|
const pkgTypes = require('pkg-types');
|
|
12
|
+
const chokidar = require('chokidar');
|
|
13
|
+
const perfectDebounce = require('perfect-debounce');
|
|
14
|
+
const ohash = require('ohash');
|
|
12
15
|
|
|
13
16
|
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
|
|
14
17
|
|
|
@@ -318,7 +321,86 @@ function createDefineConfig() {
|
|
|
318
321
|
return (input) => input;
|
|
319
322
|
}
|
|
320
323
|
|
|
324
|
+
const eventMap = {
|
|
325
|
+
add: "created",
|
|
326
|
+
change: "updated",
|
|
327
|
+
unlink: "removed"
|
|
328
|
+
};
|
|
329
|
+
async function watchConfig(options) {
|
|
330
|
+
let config = await loadConfig(options);
|
|
331
|
+
const configName = options.name || "config";
|
|
332
|
+
const watchingFiles = [
|
|
333
|
+
...new Set(
|
|
334
|
+
(config.layers || []).filter((l) => l.cwd).flatMap((l) => [
|
|
335
|
+
...["ts", "js", "mjs", "cjs", "cts", "mts", "json"].map(
|
|
336
|
+
(ext) => pathe.resolve(l.cwd, (options.name || "config") + "." + ext)
|
|
337
|
+
),
|
|
338
|
+
l.source && pathe.resolve(l.cwd, l.source),
|
|
339
|
+
// TODO: Support watching rc from home and workspace
|
|
340
|
+
options.rcFile && pathe.resolve(
|
|
341
|
+
l.cwd,
|
|
342
|
+
typeof options.rcFile === "string" ? options.rcFile : `.${configName}rc`
|
|
343
|
+
),
|
|
344
|
+
options.packageJson && pathe.resolve(l.cwd, "package.json")
|
|
345
|
+
]).filter(Boolean)
|
|
346
|
+
)
|
|
347
|
+
];
|
|
348
|
+
const _fswatcher = chokidar.watch(watchingFiles, {
|
|
349
|
+
ignoreInitial: true,
|
|
350
|
+
...options.chokidarOptions
|
|
351
|
+
});
|
|
352
|
+
const onChange = async (event, path) => {
|
|
353
|
+
const type = eventMap[event];
|
|
354
|
+
if (!type) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (options.onWatch) {
|
|
358
|
+
await options.onWatch({
|
|
359
|
+
type,
|
|
360
|
+
path
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
const oldConfig = config;
|
|
364
|
+
const newConfig = await loadConfig(options);
|
|
365
|
+
config = newConfig;
|
|
366
|
+
const changeCtx = {
|
|
367
|
+
newConfig,
|
|
368
|
+
oldConfig,
|
|
369
|
+
getDiff: () => ohash.diff(oldConfig.config, config.config)
|
|
370
|
+
};
|
|
371
|
+
if (options.acceptHMR) {
|
|
372
|
+
const changeHandled = await options.acceptHMR(changeCtx);
|
|
373
|
+
if (changeHandled) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (options.onUpdate) {
|
|
378
|
+
await options.onUpdate(changeCtx);
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
if (options.debounce !== false) {
|
|
382
|
+
_fswatcher.on("all", perfectDebounce.debounce(onChange, options.debounce));
|
|
383
|
+
} else {
|
|
384
|
+
_fswatcher.on("all", onChange);
|
|
385
|
+
}
|
|
386
|
+
const utils = {
|
|
387
|
+
watchingFiles,
|
|
388
|
+
unwatch: async () => {
|
|
389
|
+
await _fswatcher.close();
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
return new Proxy(utils, {
|
|
393
|
+
get(_, prop) {
|
|
394
|
+
if (prop in utils) {
|
|
395
|
+
return utils[prop];
|
|
396
|
+
}
|
|
397
|
+
return config[prop];
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
321
402
|
exports.createDefineConfig = createDefineConfig;
|
|
322
403
|
exports.loadConfig = loadConfig;
|
|
323
404
|
exports.loadDotenv = loadDotenv;
|
|
324
405
|
exports.setupDotenv = setupDotenv;
|
|
406
|
+
exports.watchConfig = watchConfig;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { JITI } from 'jiti';
|
|
2
2
|
import { JITIOptions } from 'jiti/dist/types';
|
|
3
|
+
import { WatchOptions } from 'chokidar';
|
|
4
|
+
import { diff } from 'ohash';
|
|
3
5
|
|
|
4
6
|
interface DotenvOptions {
|
|
5
7
|
/**
|
|
@@ -91,4 +93,28 @@ declare function createDefineConfig<T extends UserInputConfig = UserInputConfig,
|
|
|
91
93
|
|
|
92
94
|
declare function loadConfig<T extends UserInputConfig = UserInputConfig, MT extends ConfigLayerMeta = ConfigLayerMeta>(options: LoadConfigOptions<T, MT>): Promise<ResolvedConfig<T, MT>>;
|
|
93
95
|
|
|
94
|
-
|
|
96
|
+
type ConfigWatcher<T extends UserInputConfig = UserInputConfig, MT extends ConfigLayerMeta = ConfigLayerMeta> = ResolvedConfig<T, MT> & {
|
|
97
|
+
watchingFiles: string[];
|
|
98
|
+
unwatch: () => Promise<void>;
|
|
99
|
+
};
|
|
100
|
+
interface WatchConfigOptions<T extends UserInputConfig = UserInputConfig, MT extends ConfigLayerMeta = ConfigLayerMeta> extends LoadConfigOptions<T, MT> {
|
|
101
|
+
chokidarOptions?: WatchOptions;
|
|
102
|
+
debounce?: false | number;
|
|
103
|
+
onWatch?: (event: {
|
|
104
|
+
type: "created" | "updated" | "removed";
|
|
105
|
+
path: string;
|
|
106
|
+
}) => void | Promise<void>;
|
|
107
|
+
acceptHMR?: (context: {
|
|
108
|
+
getDiff: () => ReturnType<typeof diff>;
|
|
109
|
+
newConfig: ResolvedConfig<T, MT>;
|
|
110
|
+
oldConfig: ResolvedConfig<T, MT>;
|
|
111
|
+
}) => void | boolean | Promise<void | boolean>;
|
|
112
|
+
onUpdate?: (context: {
|
|
113
|
+
getDiff: () => ReturnType<typeof diff>;
|
|
114
|
+
newConfig: ResolvedConfig<T, MT>;
|
|
115
|
+
oldConfig: ResolvedConfig<T, MT>;
|
|
116
|
+
}) => void | Promise<void>;
|
|
117
|
+
}
|
|
118
|
+
declare function watchConfig<T extends UserInputConfig = UserInputConfig, MT extends ConfigLayerMeta = ConfigLayerMeta>(options: WatchConfigOptions<T, MT>): Promise<ConfigWatcher<T, MT>>;
|
|
119
|
+
|
|
120
|
+
export { C12InputConfig, ConfigLayer, ConfigLayerMeta, ConfigWatcher, DefineConfig, DotenvOptions, Env, InputConfig, LoadConfigOptions, ResolvedConfig, SourceOptions, UserInputConfig, WatchConfigOptions, createDefineConfig, loadConfig, loadDotenv, setupDotenv, watchConfig };
|
package/dist/index.mjs
CHANGED
|
@@ -7,6 +7,9 @@ import createJiti from 'jiti';
|
|
|
7
7
|
import * as rc9 from 'rc9';
|
|
8
8
|
import { defu } from 'defu';
|
|
9
9
|
import { findWorkspaceDir, readPackageJSON } from 'pkg-types';
|
|
10
|
+
import { watch } from 'chokidar';
|
|
11
|
+
import { debounce } from 'perfect-debounce';
|
|
12
|
+
import { diff } from 'ohash';
|
|
10
13
|
|
|
11
14
|
async function setupDotenv(options) {
|
|
12
15
|
const targetEnvironment = options.env ?? process.env;
|
|
@@ -298,4 +301,82 @@ function createDefineConfig() {
|
|
|
298
301
|
return (input) => input;
|
|
299
302
|
}
|
|
300
303
|
|
|
301
|
-
|
|
304
|
+
const eventMap = {
|
|
305
|
+
add: "created",
|
|
306
|
+
change: "updated",
|
|
307
|
+
unlink: "removed"
|
|
308
|
+
};
|
|
309
|
+
async function watchConfig(options) {
|
|
310
|
+
let config = await loadConfig(options);
|
|
311
|
+
const configName = options.name || "config";
|
|
312
|
+
const watchingFiles = [
|
|
313
|
+
...new Set(
|
|
314
|
+
(config.layers || []).filter((l) => l.cwd).flatMap((l) => [
|
|
315
|
+
...["ts", "js", "mjs", "cjs", "cts", "mts", "json"].map(
|
|
316
|
+
(ext) => resolve(l.cwd, (options.name || "config") + "." + ext)
|
|
317
|
+
),
|
|
318
|
+
l.source && resolve(l.cwd, l.source),
|
|
319
|
+
// TODO: Support watching rc from home and workspace
|
|
320
|
+
options.rcFile && resolve(
|
|
321
|
+
l.cwd,
|
|
322
|
+
typeof options.rcFile === "string" ? options.rcFile : `.${configName}rc`
|
|
323
|
+
),
|
|
324
|
+
options.packageJson && resolve(l.cwd, "package.json")
|
|
325
|
+
]).filter(Boolean)
|
|
326
|
+
)
|
|
327
|
+
];
|
|
328
|
+
const _fswatcher = watch(watchingFiles, {
|
|
329
|
+
ignoreInitial: true,
|
|
330
|
+
...options.chokidarOptions
|
|
331
|
+
});
|
|
332
|
+
const onChange = async (event, path) => {
|
|
333
|
+
const type = eventMap[event];
|
|
334
|
+
if (!type) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (options.onWatch) {
|
|
338
|
+
await options.onWatch({
|
|
339
|
+
type,
|
|
340
|
+
path
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
const oldConfig = config;
|
|
344
|
+
const newConfig = await loadConfig(options);
|
|
345
|
+
config = newConfig;
|
|
346
|
+
const changeCtx = {
|
|
347
|
+
newConfig,
|
|
348
|
+
oldConfig,
|
|
349
|
+
getDiff: () => diff(oldConfig.config, config.config)
|
|
350
|
+
};
|
|
351
|
+
if (options.acceptHMR) {
|
|
352
|
+
const changeHandled = await options.acceptHMR(changeCtx);
|
|
353
|
+
if (changeHandled) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (options.onUpdate) {
|
|
358
|
+
await options.onUpdate(changeCtx);
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
if (options.debounce !== false) {
|
|
362
|
+
_fswatcher.on("all", debounce(onChange, options.debounce));
|
|
363
|
+
} else {
|
|
364
|
+
_fswatcher.on("all", onChange);
|
|
365
|
+
}
|
|
366
|
+
const utils = {
|
|
367
|
+
watchingFiles,
|
|
368
|
+
unwatch: async () => {
|
|
369
|
+
await _fswatcher.close();
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
return new Proxy(utils, {
|
|
373
|
+
get(_, prop) {
|
|
374
|
+
if (prop in utils) {
|
|
375
|
+
return utils[prop];
|
|
376
|
+
}
|
|
377
|
+
return config[prop];
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export { createDefineConfig, loadConfig, loadDotenv, setupDotenv, watchConfig };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c12",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Smart Config Loader",
|
|
5
5
|
"repository": "unjs/c12",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,12 +30,15 @@
|
|
|
30
30
|
"test:types": "tsc --noEmit"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
+
"chokidar": "^3.5.3",
|
|
33
34
|
"defu": "^6.1.2",
|
|
34
35
|
"dotenv": "^16.0.3",
|
|
35
36
|
"giget": "^1.1.2",
|
|
36
37
|
"jiti": "^1.18.2",
|
|
37
38
|
"mlly": "^1.2.0",
|
|
39
|
+
"ohash": "^1.1.1",
|
|
38
40
|
"pathe": "^1.1.0",
|
|
41
|
+
"perfect-debounce": "^0.1.3",
|
|
39
42
|
"pkg-types": "^1.0.2",
|
|
40
43
|
"rc9": "^2.1.0"
|
|
41
44
|
},
|
|
@@ -50,5 +53,5 @@
|
|
|
50
53
|
"unbuild": "^1.2.1",
|
|
51
54
|
"vitest": "^0.30.1"
|
|
52
55
|
},
|
|
53
|
-
"packageManager": "pnpm@8.
|
|
56
|
+
"packageManager": "pnpm@8.3.0"
|
|
54
57
|
}
|