c12 1.2.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 CHANGED
@@ -1,11 +1,11 @@
1
- # c12
1
+ # ⚙️ c12
2
2
 
3
3
  [![npm version][npm-version-src]][npm-version-href]
4
4
  [![npm downloads][npm-downloads-src]][npm-downloads-href]
5
- [![Github Actions][github-actions-src]][github-actions-href]
6
5
  [![Codecov][codecov-src]][codecov-href]
6
+ [![License][license-src]][license-href]
7
7
 
8
- > Smart Configuration Loader
8
+ Smart Configuration Loader.
9
9
 
10
10
  ## Features
11
11
 
@@ -13,8 +13,10 @@
13
13
  - RC config support with [unjs/rc9](https://github.com/unjs/rc9)
14
14
  - Multiple sources merged with [unjs/defu](https://github.com/unjs/defu)
15
15
  - `.env` support with [dotenv](https://www.npmjs.com/package/dotenv)
16
- - Support reading config from the nearest `package.json` file
17
- - Support extending nested configurations from multiple local or git sources
16
+ - Reads config from the nearest `package.json` file
17
+ - [Extends configurations](https://github.com/unjs/c12#extending-configuration) from multiple local or git sources
18
+ - Overwrite with [environment-specific configuration](#environment-specific-configuration)
19
+ - Config watcher with auto-reload and HMR support
18
20
 
19
21
  ## Usage
20
22
 
@@ -35,10 +37,10 @@ Import:
35
37
 
36
38
  ```js
37
39
  // ESM
38
- import { loadConfig } from "c12";
40
+ import { loadConfig, watchConfig } from "c12";
39
41
 
40
42
  // CommonJS
41
- const { loadConfig } = require("c12");
43
+ const { loadConfig, watchConfig } = require("c12");
42
44
  ```
43
45
 
44
46
  Load configuration:
@@ -58,7 +60,7 @@ c12 merged config sources with [unjs/defu](https://github.com/unjs/defu) by belo
58
60
  1. Config overrides passed by options
59
61
  2. Config file in CWD
60
62
  3. RC file in CWD
61
- 4. Global RC file in user's home directory
63
+ 4. Global RC file in the user's home directory
62
64
  5. Config from `package.json`
63
65
  6. Default config passed by options
64
66
  7. Extended config layers
@@ -233,6 +235,46 @@ c12 tries to match [`envName`](#envname) and override environment config if spec
233
235
  }
234
236
  ```
235
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
+
236
278
  ## 💻 Development
237
279
 
238
280
  - Clone this repository
@@ -246,11 +288,11 @@ Made with 💛 Published under [MIT License](./LICENSE).
246
288
 
247
289
  <!-- Badges -->
248
290
 
249
- [npm-version-src]: https://img.shields.io/npm/v/c12?style=flat-square
291
+ [npm-version-src]: https://img.shields.io/npm/v/c12?style=flat&colorA=18181B&colorB=F0DB4F
250
292
  [npm-version-href]: https://npmjs.com/package/c12
251
- [npm-downloads-src]: https://img.shields.io/npm/dm/c12?style=flat-square
293
+ [npm-downloads-src]: https://img.shields.io/npm/dm/c12?style=flat&colorA=18181B&colorB=F0DB4F
252
294
  [npm-downloads-href]: https://npmjs.com/package/c12
253
- [github-actions-src]: https://img.shields.io/github/actions/workflow/status/unjs/c12/ci.yml?branch=main&style=flat-square
254
- [github-actions-href]: https://github.com/unjs/c12/actions?query=workflow%3Aci
255
- [codecov-src]: https://img.shields.io/codecov/c/gh/unjs/c12/main?style=flat-square
295
+ [codecov-src]: https://img.shields.io/codecov/c/gh/unjs/c12/main?style=flat&colorA=18181B&colorB=F0DB4F
256
296
  [codecov-href]: https://codecov.io/gh/unjs/c12
297
+ [license-src]: https://img.shields.io/github/license/unjs/c12.svg?style=flat&colorA=18181B&colorB=F0DB4F
298
+ [license-href]: https://github.com/unjs/c12/blob/main/LICENSE
package/dist/index.cjs CHANGED
@@ -9,8 +9,14 @@ 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
- function _interopNamespaceDefault(e) {
16
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
17
+
18
+ function _interopNamespaceCompat(e) {
19
+ if (e && typeof e === 'object' && 'default' in e) return e;
14
20
  const n = Object.create(null);
15
21
  if (e) {
16
22
  for (const k in e) {
@@ -21,8 +27,9 @@ function _interopNamespaceDefault(e) {
21
27
  return n;
22
28
  }
23
29
 
24
- const dotenv__namespace = /*#__PURE__*/_interopNamespaceDefault(dotenv);
25
- const rc9__namespace = /*#__PURE__*/_interopNamespaceDefault(rc9);
30
+ const dotenv__namespace = /*#__PURE__*/_interopNamespaceCompat(dotenv);
31
+ const createJiti__default = /*#__PURE__*/_interopDefaultCompat(createJiti);
32
+ const rc9__namespace = /*#__PURE__*/_interopNamespaceCompat(rc9);
26
33
 
27
34
  async function setupDotenv(options) {
28
35
  const targetEnvironment = options.env ?? process.env;
@@ -46,7 +53,7 @@ async function loadDotenv(options) {
46
53
  const parsed = dotenv__namespace.parse(await node_fs.promises.readFile(dotenvFile, "utf8"));
47
54
  Object.assign(environment, parsed);
48
55
  }
49
- if (!options.env._applied) {
56
+ if (!options.env?._applied) {
50
57
  Object.assign(environment, options.env);
51
58
  environment._applied = true;
52
59
  }
@@ -67,15 +74,15 @@ function interpolate(target, source = {}, parse = (v) => v) {
67
74
  return parse(
68
75
  // eslint-disable-next-line unicorn/no-array-reduce
69
76
  matches.reduce((newValue, match) => {
70
- const parts = /(.?)\${?([\w:]+)?}?/g.exec(match);
77
+ const parts = /(.?)\${?([\w:]+)?}?/g.exec(match) || [];
71
78
  const prefix = parts[1];
72
79
  let value2, replacePart;
73
80
  if (prefix === "\\") {
74
- replacePart = parts[0];
81
+ replacePart = parts[0] || "";
75
82
  value2 = replacePart.replace("\\$", "$");
76
83
  } else {
77
84
  const key = parts[2];
78
- replacePart = parts[0].slice(prefix.length);
85
+ replacePart = (parts[0] || "").slice(prefix.length);
79
86
  if (parents.includes(key)) {
80
87
  console.warn(
81
88
  `Please avoid recursive environment variables ( loop: ${parents.join(
@@ -108,7 +115,7 @@ async function loadConfig(options) {
108
115
  ...options.extend
109
116
  };
110
117
  }
111
- options.jiti = options.jiti || createJiti(void 0, {
118
+ options.jiti = options.jiti || createJiti__default(void 0, {
112
119
  interopDefault: true,
113
120
  requireCache: false,
114
121
  esmResolve: true,
@@ -258,7 +265,7 @@ async function resolveConfig(source, options, sourceOptions = {}) {
258
265
  const name = gitRepo.replace(/[#/:@\\]/g, "_");
259
266
  const tmpDir = process.env.XDG_CACHE_HOME ? pathe.resolve(process.env.XDG_CACHE_HOME, "c12", name) : pathe.resolve(node_os.homedir(), ".cache/c12", name);
260
267
  if (node_fs.existsSync(tmpDir)) {
261
- await promises.rmdir(tmpDir, { recursive: true });
268
+ await promises.rm(tmpDir, { recursive: true });
262
269
  }
263
270
  const cloned = await downloadTemplate(source, { dir: tmpDir });
264
271
  source = cloned.dir;
@@ -274,7 +281,12 @@ async function resolveConfig(source, options, sourceOptions = {}) {
274
281
  if (isDir) {
275
282
  source = options.configFile;
276
283
  }
277
- const res = { config: void 0, cwd, source, sourceOptions };
284
+ const res = {
285
+ config: void 0,
286
+ cwd,
287
+ source,
288
+ sourceOptions
289
+ };
278
290
  try {
279
291
  res.configFile = options.jiti.resolve(pathe.resolve(cwd, source), {
280
292
  paths: [cwd]
@@ -305,6 +317,90 @@ async function resolveConfig(source, options, sourceOptions = {}) {
305
317
  return res;
306
318
  }
307
319
 
320
+ function createDefineConfig() {
321
+ return (input) => input;
322
+ }
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
+
402
+ exports.createDefineConfig = createDefineConfig;
308
403
  exports.loadConfig = loadConfig;
309
404
  exports.loadDotenv = loadDotenv;
310
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
  /**
@@ -37,41 +39,37 @@ declare function setupDotenv(options: DotenvOptions): Promise<Env>;
37
39
  /** Load environment variables into an object. */
38
40
  declare function loadDotenv(options: DotenvOptions): Promise<Env>;
39
41
 
40
- type UserInputConfig = Record<string, any>;
41
42
  interface ConfigLayerMeta {
42
43
  name?: string;
43
44
  [key: string]: any;
44
45
  }
45
- interface C12InputConfig {
46
- $test?: UserInputConfig;
47
- $development?: UserInputConfig;
48
- $production?: UserInputConfig;
49
- $env?: Record<string, UserInputConfig>;
50
- $meta?: ConfigLayerMeta;
51
- }
52
- interface InputConfig extends C12InputConfig, UserInputConfig {
46
+ type UserInputConfig = Record<string, any>;
47
+ interface C12InputConfig<T extends UserInputConfig = UserInputConfig, MT extends ConfigLayerMeta = ConfigLayerMeta> {
48
+ $test?: T;
49
+ $development?: T;
50
+ $production?: T;
51
+ $env?: Record<string, T>;
52
+ $meta?: MT;
53
53
  }
54
- interface SourceOptions {
55
- meta?: ConfigLayerMeta;
56
- overrides?: UserInputConfig;
54
+ type InputConfig<T extends UserInputConfig = UserInputConfig, MT extends ConfigLayerMeta = ConfigLayerMeta> = C12InputConfig<T, MT> & T;
55
+ interface SourceOptions<T extends UserInputConfig = UserInputConfig, MT extends ConfigLayerMeta = ConfigLayerMeta> {
56
+ meta?: MT;
57
+ overrides?: T;
57
58
  [key: string]: any;
58
59
  }
59
- interface ConfigLayer<T extends InputConfig = InputConfig> {
60
+ interface ConfigLayer<T extends UserInputConfig = UserInputConfig, MT extends ConfigLayerMeta = ConfigLayerMeta> {
60
61
  config: T | null;
61
62
  source?: string;
62
- sourceOptions?: SourceOptions;
63
- meta?: ConfigLayerMeta;
63
+ sourceOptions?: SourceOptions<T, MT>;
64
+ meta?: MT;
64
65
  cwd?: string;
65
66
  configFile?: string;
66
67
  }
67
- interface ResolvedConfig<T extends InputConfig = InputConfig> extends ConfigLayer<T> {
68
- layers?: ConfigLayer<T>[];
68
+ interface ResolvedConfig<T extends UserInputConfig = UserInputConfig, MT extends ConfigLayerMeta = ConfigLayerMeta> extends ConfigLayer<T, MT> {
69
+ layers?: ConfigLayer<T, MT>[];
69
70
  cwd?: string;
70
71
  }
71
- interface ResolveConfigOptions {
72
- cwd: string;
73
- }
74
- interface LoadConfigOptions<T extends InputConfig = InputConfig> {
72
+ interface LoadConfigOptions<T extends UserInputConfig = UserInputConfig, MT extends ConfigLayerMeta = ConfigLayerMeta> {
75
73
  name?: string;
76
74
  cwd?: string;
77
75
  configFile?: string;
@@ -83,13 +81,40 @@ interface LoadConfigOptions<T extends InputConfig = InputConfig> {
83
81
  defaults?: T;
84
82
  defaultConfig?: T;
85
83
  overrides?: T;
86
- resolve?: (id: string, options: LoadConfigOptions) => null | ResolvedConfig | Promise<ResolvedConfig | null>;
84
+ resolve?: (id: string, options: LoadConfigOptions<T, MT>) => null | undefined | ResolvedConfig<T, MT> | Promise<ResolvedConfig<T, MT> | undefined | null>;
87
85
  jiti?: JITI;
88
86
  jitiOptions?: JITIOptions;
89
87
  extend?: false | {
90
88
  extendKey?: string | string[];
91
89
  };
92
90
  }
93
- declare function loadConfig<T extends InputConfig = InputConfig>(options: LoadConfigOptions<T>): Promise<ResolvedConfig<T>>;
91
+ type DefineConfig<T extends UserInputConfig = UserInputConfig, MT extends ConfigLayerMeta = ConfigLayerMeta> = (input: InputConfig<T, MT>) => InputConfig<T, MT>;
92
+ declare function createDefineConfig<T extends UserInputConfig = UserInputConfig, MT extends ConfigLayerMeta = ConfigLayerMeta>(): DefineConfig<T, MT>;
93
+
94
+ declare function loadConfig<T extends UserInputConfig = UserInputConfig, MT extends ConfigLayerMeta = ConfigLayerMeta>(options: LoadConfigOptions<T, MT>): Promise<ResolvedConfig<T, MT>>;
95
+
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>>;
94
119
 
95
- export { C12InputConfig, ConfigLayer, ConfigLayerMeta, DotenvOptions, Env, InputConfig, LoadConfigOptions, ResolveConfigOptions, ResolvedConfig, SourceOptions, UserInputConfig, loadConfig, loadDotenv, setupDotenv };
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
@@ -1,12 +1,15 @@
1
1
  import { existsSync, promises } from 'node:fs';
2
2
  import { resolve, extname, dirname } from 'pathe';
3
3
  import * as dotenv from 'dotenv';
4
- import { rmdir } from 'node:fs/promises';
4
+ import { rm } from 'node:fs/promises';
5
5
  import { homedir } from 'node:os';
6
6
  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;
@@ -30,7 +33,7 @@ async function loadDotenv(options) {
30
33
  const parsed = dotenv.parse(await promises.readFile(dotenvFile, "utf8"));
31
34
  Object.assign(environment, parsed);
32
35
  }
33
- if (!options.env._applied) {
36
+ if (!options.env?._applied) {
34
37
  Object.assign(environment, options.env);
35
38
  environment._applied = true;
36
39
  }
@@ -51,15 +54,15 @@ function interpolate(target, source = {}, parse = (v) => v) {
51
54
  return parse(
52
55
  // eslint-disable-next-line unicorn/no-array-reduce
53
56
  matches.reduce((newValue, match) => {
54
- const parts = /(.?)\${?([\w:]+)?}?/g.exec(match);
57
+ const parts = /(.?)\${?([\w:]+)?}?/g.exec(match) || [];
55
58
  const prefix = parts[1];
56
59
  let value2, replacePart;
57
60
  if (prefix === "\\") {
58
- replacePart = parts[0];
61
+ replacePart = parts[0] || "";
59
62
  value2 = replacePart.replace("\\$", "$");
60
63
  } else {
61
64
  const key = parts[2];
62
- replacePart = parts[0].slice(prefix.length);
65
+ replacePart = (parts[0] || "").slice(prefix.length);
63
66
  if (parents.includes(key)) {
64
67
  console.warn(
65
68
  `Please avoid recursive environment variables ( loop: ${parents.join(
@@ -242,7 +245,7 @@ async function resolveConfig(source, options, sourceOptions = {}) {
242
245
  const name = gitRepo.replace(/[#/:@\\]/g, "_");
243
246
  const tmpDir = process.env.XDG_CACHE_HOME ? resolve(process.env.XDG_CACHE_HOME, "c12", name) : resolve(homedir(), ".cache/c12", name);
244
247
  if (existsSync(tmpDir)) {
245
- await rmdir(tmpDir, { recursive: true });
248
+ await rm(tmpDir, { recursive: true });
246
249
  }
247
250
  const cloned = await downloadTemplate(source, { dir: tmpDir });
248
251
  source = cloned.dir;
@@ -258,7 +261,12 @@ async function resolveConfig(source, options, sourceOptions = {}) {
258
261
  if (isDir) {
259
262
  source = options.configFile;
260
263
  }
261
- const res = { config: void 0, cwd, source, sourceOptions };
264
+ const res = {
265
+ config: void 0,
266
+ cwd,
267
+ source,
268
+ sourceOptions
269
+ };
262
270
  try {
263
271
  res.configFile = options.jiti.resolve(resolve(cwd, source), {
264
272
  paths: [cwd]
@@ -289,4 +297,86 @@ async function resolveConfig(source, options, sourceOptions = {}) {
289
297
  return res;
290
298
  }
291
299
 
292
- export { loadConfig, loadDotenv, setupDotenv };
300
+ function createDefineConfig() {
301
+ return (input) => input;
302
+ }
303
+
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.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Smart Config Loader",
5
5
  "repository": "unjs/c12",
6
6
  "license": "MIT",
@@ -26,27 +26,32 @@
26
26
  "lint:fix": "eslint --ext .ts,.js,.mjs,.cjs . --fix && prettier -w src test",
27
27
  "prepack": "unbuild",
28
28
  "release": "changelogen --release && npm publish && git push --follow-tags",
29
- "test": "vitest run --coverage"
29
+ "test": "vitest run --coverage && pnpm test:types",
30
+ "test:types": "tsc --noEmit"
30
31
  },
31
32
  "dependencies": {
33
+ "chokidar": "^3.5.3",
32
34
  "defu": "^6.1.2",
33
35
  "dotenv": "^16.0.3",
34
36
  "giget": "^1.1.2",
35
- "jiti": "^1.17.2",
37
+ "jiti": "^1.18.2",
36
38
  "mlly": "^1.2.0",
39
+ "ohash": "^1.1.1",
37
40
  "pathe": "^1.1.0",
41
+ "perfect-debounce": "^0.1.3",
38
42
  "pkg-types": "^1.0.2",
39
- "rc9": "^2.0.1"
43
+ "rc9": "^2.1.0"
40
44
  },
41
45
  "devDependencies": {
42
- "@vitest/coverage-c8": "^0.29.2",
43
- "changelogen": "^0.5.1",
44
- "eslint": "^8.36.0",
46
+ "@vitest/coverage-c8": "^0.30.1",
47
+ "changelogen": "^0.5.3",
48
+ "eslint": "^8.38.0",
45
49
  "eslint-config-unjs": "^0.1.0",
46
- "prettier": "^2.8.4",
47
- "typescript": "^4.9.5",
48
- "unbuild": "^1.1.2",
49
- "vitest": "^0.29.2"
50
+ "expect-type": "^0.15.0",
51
+ "prettier": "^2.8.7",
52
+ "typescript": "^5.0.4",
53
+ "unbuild": "^1.2.1",
54
+ "vitest": "^0.30.1"
50
55
  },
51
- "packageManager": "pnpm@7.29.1"
52
- }
56
+ "packageManager": "pnpm@8.3.0"
57
+ }