c12 0.1.0 → 0.1.1

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
@@ -5,7 +5,7 @@
5
5
  [![Github Actions][github-actions-src]][github-actions-href]
6
6
  [![Codecov][codecov-src]][codecov-href]
7
7
 
8
- > Smart Config Loader
8
+ > Smart Configuration Loader
9
9
 
10
10
  ## Features
11
11
 
@@ -13,6 +13,7 @@
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 extending nested configurations from multiple local or git sourecs
16
17
 
17
18
  ## Usage
18
19
 
@@ -42,7 +43,11 @@ const { loadConfig } = require('c12')
42
43
  Load configuration:
43
44
 
44
45
  ```js
46
+ // Get loaded config
45
47
  const { config } = await loadConfig({})
48
+
49
+ // Get resolved config and extended layers
50
+ const { config, configFile, layers } = await loadConfig({})
46
51
  ```
47
52
 
48
53
  ## Loading priority
@@ -54,6 +59,7 @@ c12 merged config sources with [unjs/defu](https://github.com/unjs/defu) by belo
54
59
  3. RC file in CWD
55
60
  4. global RC file in user's home directory
56
61
  5. default config passed by options
62
+ 6. Extended config layers
57
63
 
58
64
  ## Options
59
65
 
@@ -93,6 +99,85 @@ Specify default configuration. It has the **lowest** priority.
93
99
 
94
100
  Specify override configuration. It has the **highest** priority.
95
101
 
102
+ ## Extending configuration
103
+
104
+ If resolved config contains a `extends` key, it will be used to extend configuration.
105
+
106
+ Extending can be nested and each layer can extend from one base or more.
107
+
108
+ Final config is merged result of extended options and user options with [unjs/defu](https://github.com/unjs/defu).
109
+
110
+ Each item in extends, is a string that can be either an absolute or relative path to current config file pointing to a config file for extending or directory containing config file.
111
+ If it starts with either of `github:`, `gitlab:`, `bitbucket:` or `https:`, c12 autmatically clones it.
112
+
113
+ For custom merging strategies, you can directly access each layer with `layers` property.
114
+
115
+ **Example:**
116
+
117
+ ```js
118
+ // config.ts
119
+ export default {
120
+ colors: {
121
+ primary: 'user_primary'
122
+ },
123
+ extends: [
124
+ './theme',
125
+ './config.dev.ts'
126
+ ]
127
+ }
128
+ ```
129
+
130
+ ```js
131
+ // config.dev.ts
132
+ export default {
133
+ dev: true
134
+ }
135
+ ```
136
+
137
+ ```js
138
+ // theme/config.ts
139
+ export default {
140
+ extends: '../base',
141
+ colors: {
142
+ primary: 'theme_primary',
143
+ secondary: 'theme_secondary'
144
+ }
145
+ }
146
+ ```
147
+
148
+ ```js
149
+ // base/config.ts
150
+ export default {
151
+ colors: {
152
+ primary: 'base_primary'
153
+ text: 'base_text'
154
+ }
155
+ }
156
+ ```
157
+
158
+ Loaded configuration would look like this:
159
+
160
+ ```js
161
+ {
162
+ dev: true,
163
+ colors: {
164
+ primary: 'user_primary',
165
+ secondary: 'theme_secondary',
166
+ text: 'base_text'
167
+ }
168
+ }
169
+ ```
170
+
171
+ Layers:
172
+
173
+ ```js
174
+ [
175
+ { config: /* theme config */, configFile: /* path/to/theme/config.ts */, cwd: /* path/to/theme */ },
176
+ { config: /* base config */, configFile: /* path/to/base/config.ts */, cwd: /* path/to/base */ },
177
+ { config: /* dev config */, configFile: /* path/to/config.dev.ts */, cwd: /* path/ */ },
178
+ ]
179
+ ```
180
+
96
181
  ## 💻 Development
97
182
 
98
183
  - Clone this repository
package/dist/index.cjs CHANGED
@@ -5,6 +5,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
5
5
  const fs = require('fs');
6
6
  const pathe = require('pathe');
7
7
  const dotenv = require('dotenv');
8
+ const os = require('os');
8
9
  const createJiti = require('jiti');
9
10
  const rc9 = require('rc9');
10
11
  const defu = require('defu');
@@ -24,6 +25,7 @@ function _interopNamespace(e) {
24
25
  }
25
26
 
26
27
  const dotenv__namespace = /*#__PURE__*/_interopNamespace(dotenv);
28
+ const os__default = /*#__PURE__*/_interopDefaultLegacy(os);
27
29
  const createJiti__default = /*#__PURE__*/_interopDefaultLegacy(createJiti);
28
30
  const rc9__namespace = /*#__PURE__*/_interopNamespace(rc9);
29
31
  const defu__default = /*#__PURE__*/_interopDefaultLegacy(defu);
@@ -98,17 +100,22 @@ async function loadConfig(opts) {
98
100
  opts.name = opts.name || "config";
99
101
  opts.configFile = opts.configFile ?? (opts.name !== "config" ? `${opts.name}.config` : "config");
100
102
  opts.rcFile = opts.rcFile ?? `.${opts.name}rc`;
101
- const ctx = {
102
- config: {}
103
+ const r = {
104
+ config: {},
105
+ cwd: opts.cwd,
106
+ configFile: pathe.resolve(opts.cwd, opts.configFile),
107
+ layers: []
103
108
  };
104
109
  if (opts.dotenv) {
105
- ctx.env = await setupDotenv({
110
+ await setupDotenv({
106
111
  cwd: opts.cwd,
107
112
  ...opts.dotenv === true ? {} : opts.dotenv
108
113
  });
109
114
  }
110
- const { config, configPath } = await loadConfigFile(opts);
111
- ctx.configPath = configPath;
115
+ const { config, configFile } = await loadConfigFile(opts.cwd, opts.configFile);
116
+ if (configFile) {
117
+ r.configFile = configFile;
118
+ }
112
119
  const configRC = {};
113
120
  if (opts.rcFile) {
114
121
  if (opts.globalRc) {
@@ -116,21 +123,61 @@ async function loadConfig(opts) {
116
123
  }
117
124
  Object.assign(configRC, rc9__namespace.read({ name: opts.rcFile, dir: opts.cwd }));
118
125
  }
119
- ctx.config = defu__default(opts.overrides, config, configRC, opts.defaults);
120
- return ctx;
126
+ r.config = defu__default(opts.overrides, config, configRC, opts.defaults);
127
+ await extendConfig(r.config, opts.configFile, opts.cwd);
128
+ r.layers = r.config._layers;
129
+ delete r.config._layers;
130
+ r.config = defu__default(r.config, ...r.layers.map((e) => e.config));
131
+ return r;
132
+ }
133
+ const GIT_PREFIXES = ["github:", "gitlab:", "bitbucket:", "https://"];
134
+ async function extendConfig(config, configFile, cwd) {
135
+ config._layers = config._layers || [];
136
+ const extendSources = (Array.isArray(config.extends) ? config.extends : [config.extends]).filter(Boolean);
137
+ delete config.extends;
138
+ for (let extendSource of extendSources) {
139
+ if (GIT_PREFIXES.some((prefix) => extendSource.startsWith(prefix))) {
140
+ const url = new URL(extendSource);
141
+ const subPath = url.pathname.split("/").slice(2).join("/");
142
+ const gitRepo = url.protocol + url.pathname.split("/").slice(0, 2).join("/");
143
+ const tmpdir = pathe.resolve(os__default.tmpdir(), "c12/", gitRepo.replace(/[#:@/\\]/g, "_"));
144
+ await fs.promises.rm(tmpdir, { recursive: true }).catch(() => {
145
+ });
146
+ const gittar = await import('gittar').then((r) => r.default || r);
147
+ const tarFile = await gittar.fetch(gitRepo);
148
+ await gittar.extract(tarFile, tmpdir);
149
+ extendSource = pathe.resolve(tmpdir, subPath);
150
+ }
151
+ const isDir = !pathe.extname(extendSource);
152
+ const _cwd = pathe.resolve(cwd, isDir ? extendSource : pathe.dirname(extendSource));
153
+ const _config = await loadConfigFile(_cwd, isDir ? configFile : extendSource);
154
+ if (!_config.config) {
155
+ continue;
156
+ }
157
+ await extendConfig(_config.config, configFile, _cwd);
158
+ config._layers.push({
159
+ config: _config.config,
160
+ cwd: _cwd,
161
+ configFile: _config.configFile
162
+ });
163
+ if (_config.config._layers) {
164
+ config._layers.push(..._config.config._layers);
165
+ delete _config.config._layers;
166
+ }
167
+ }
121
168
  }
122
169
  const jiti = createJiti__default(null, { cache: false, interopDefault: true });
123
- async function loadConfigFile(opts) {
170
+ async function loadConfigFile(cwd, configFile) {
124
171
  const res = {
125
- configPath: null,
172
+ configFile: null,
126
173
  config: null
127
174
  };
128
- if (!opts.configFile) {
175
+ if (!configFile) {
129
176
  return res;
130
177
  }
131
178
  try {
132
- res.configPath = jiti.resolve(pathe.resolve(opts.cwd, opts.configFile), { paths: [opts.cwd] });
133
- res.config = jiti(res.configPath);
179
+ res.configFile = jiti.resolve(pathe.resolve(cwd, configFile), { paths: [cwd] });
180
+ res.config = jiti(res.configFile);
134
181
  if (typeof res.config === "function") {
135
182
  res.config = await res.config();
136
183
  }
package/dist/index.d.ts CHANGED
@@ -34,22 +34,24 @@ declare function setupDotenv(options: DotenvOptions): Promise<Env>;
34
34
  /** Load environment variables into an object. */
35
35
  declare function loadDotenv(opts: DotenvOptions): Promise<Env>;
36
36
 
37
- declare type ConfigT = Record<string, any>;
38
- interface LoadConfigOptions<T extends ConfigT = ConfigT> {
37
+ interface InputConfig extends Record<string, any> {
38
+ }
39
+ interface ResolvedConfig<T extends InputConfig = InputConfig> {
40
+ config: T;
41
+ cwd: string;
42
+ configFile: string;
43
+ layers: ResolvedConfig<T>[];
44
+ }
45
+ interface LoadConfigOptions<T extends InputConfig = InputConfig> {
39
46
  name?: string;
40
47
  cwd?: string;
41
- configFile?: false | string;
48
+ configFile?: string;
42
49
  rcFile?: false | string;
43
50
  globalRc?: boolean;
44
51
  dotenv?: boolean | DotenvOptions;
45
52
  defaults?: T;
46
53
  overrides?: T;
47
54
  }
48
- interface ResolvedConfig<T extends ConfigT = ConfigT> {
49
- config: T;
50
- configPath?: string;
51
- env?: Record<string, any>;
52
- }
53
- declare function loadConfig<T extends ConfigT = ConfigT>(opts: LoadConfigOptions<T>): Promise<ResolvedConfig<T>>;
55
+ declare function loadConfig<T extends InputConfig = InputConfig>(opts: LoadConfigOptions<T>): Promise<ResolvedConfig<T>>;
54
56
 
55
- export { ConfigT, DotenvOptions, Env, LoadConfigOptions, ResolvedConfig, loadConfig, loadDotenv, setupDotenv };
57
+ export { DotenvOptions, Env, InputConfig, LoadConfigOptions, ResolvedConfig, loadConfig, loadDotenv, setupDotenv };
package/dist/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { existsSync, promises } from 'fs';
2
- import { resolve } from 'pathe';
2
+ import { resolve, extname, dirname } from 'pathe';
3
3
  import * as dotenv from 'dotenv';
4
+ import os from 'os';
4
5
  import createJiti from 'jiti';
5
6
  import * as rc9 from 'rc9';
6
7
  import defu from 'defu';
@@ -75,17 +76,22 @@ async function loadConfig(opts) {
75
76
  opts.name = opts.name || "config";
76
77
  opts.configFile = opts.configFile ?? (opts.name !== "config" ? `${opts.name}.config` : "config");
77
78
  opts.rcFile = opts.rcFile ?? `.${opts.name}rc`;
78
- const ctx = {
79
- config: {}
79
+ const r = {
80
+ config: {},
81
+ cwd: opts.cwd,
82
+ configFile: resolve(opts.cwd, opts.configFile),
83
+ layers: []
80
84
  };
81
85
  if (opts.dotenv) {
82
- ctx.env = await setupDotenv({
86
+ await setupDotenv({
83
87
  cwd: opts.cwd,
84
88
  ...opts.dotenv === true ? {} : opts.dotenv
85
89
  });
86
90
  }
87
- const { config, configPath } = await loadConfigFile(opts);
88
- ctx.configPath = configPath;
91
+ const { config, configFile } = await loadConfigFile(opts.cwd, opts.configFile);
92
+ if (configFile) {
93
+ r.configFile = configFile;
94
+ }
89
95
  const configRC = {};
90
96
  if (opts.rcFile) {
91
97
  if (opts.globalRc) {
@@ -93,21 +99,61 @@ async function loadConfig(opts) {
93
99
  }
94
100
  Object.assign(configRC, rc9.read({ name: opts.rcFile, dir: opts.cwd }));
95
101
  }
96
- ctx.config = defu(opts.overrides, config, configRC, opts.defaults);
97
- return ctx;
102
+ r.config = defu(opts.overrides, config, configRC, opts.defaults);
103
+ await extendConfig(r.config, opts.configFile, opts.cwd);
104
+ r.layers = r.config._layers;
105
+ delete r.config._layers;
106
+ r.config = defu(r.config, ...r.layers.map((e) => e.config));
107
+ return r;
108
+ }
109
+ const GIT_PREFIXES = ["github:", "gitlab:", "bitbucket:", "https://"];
110
+ async function extendConfig(config, configFile, cwd) {
111
+ config._layers = config._layers || [];
112
+ const extendSources = (Array.isArray(config.extends) ? config.extends : [config.extends]).filter(Boolean);
113
+ delete config.extends;
114
+ for (let extendSource of extendSources) {
115
+ if (GIT_PREFIXES.some((prefix) => extendSource.startsWith(prefix))) {
116
+ const url = new URL(extendSource);
117
+ const subPath = url.pathname.split("/").slice(2).join("/");
118
+ const gitRepo = url.protocol + url.pathname.split("/").slice(0, 2).join("/");
119
+ const tmpdir = resolve(os.tmpdir(), "c12/", gitRepo.replace(/[#:@/\\]/g, "_"));
120
+ await promises.rm(tmpdir, { recursive: true }).catch(() => {
121
+ });
122
+ const gittar = await import('gittar').then((r) => r.default || r);
123
+ const tarFile = await gittar.fetch(gitRepo);
124
+ await gittar.extract(tarFile, tmpdir);
125
+ extendSource = resolve(tmpdir, subPath);
126
+ }
127
+ const isDir = !extname(extendSource);
128
+ const _cwd = resolve(cwd, isDir ? extendSource : dirname(extendSource));
129
+ const _config = await loadConfigFile(_cwd, isDir ? configFile : extendSource);
130
+ if (!_config.config) {
131
+ continue;
132
+ }
133
+ await extendConfig(_config.config, configFile, _cwd);
134
+ config._layers.push({
135
+ config: _config.config,
136
+ cwd: _cwd,
137
+ configFile: _config.configFile
138
+ });
139
+ if (_config.config._layers) {
140
+ config._layers.push(..._config.config._layers);
141
+ delete _config.config._layers;
142
+ }
143
+ }
98
144
  }
99
145
  const jiti = createJiti(null, { cache: false, interopDefault: true });
100
- async function loadConfigFile(opts) {
146
+ async function loadConfigFile(cwd, configFile) {
101
147
  const res = {
102
- configPath: null,
148
+ configFile: null,
103
149
  config: null
104
150
  };
105
- if (!opts.configFile) {
151
+ if (!configFile) {
106
152
  return res;
107
153
  }
108
154
  try {
109
- res.configPath = jiti.resolve(resolve(opts.cwd, opts.configFile), { paths: [opts.cwd] });
110
- res.config = jiti(res.configPath);
155
+ res.configFile = jiti.resolve(resolve(cwd, configFile), { paths: [cwd] });
156
+ res.config = jiti(res.configFile);
111
157
  if (typeof res.config === "function") {
112
158
  res.config = await res.config();
113
159
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c12",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Smart Config Loader",
5
5
  "repository": "unjs/c12",
6
6
  "license": "MIT",
@@ -29,6 +29,7 @@
29
29
  "dependencies": {
30
30
  "defu": "^5.0.1",
31
31
  "dotenv": "^14.3.2",
32
+ "gittar": "^0.1.1",
32
33
  "jiti": "^1.12.14",
33
34
  "mlly": "^0.4.1",
34
35
  "pathe": "^0.2.0",