@theholocron/datapad 5.0.0-alpha.114 → 5.0.0-alpha.115
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 +6 -2
- package/dist/index.d.mts +5 -14
- package/dist/index.mjs +93 -111
- package/package.json +8 -5
package/README.md
CHANGED
|
@@ -58,7 +58,8 @@ stopping at the first directory containing a `.git` entry (that directory
|
|
|
58
58
|
is still searched before stopping) or the filesystem root. Never wanders
|
|
59
59
|
past a repo boundary into an unrelated enclosing directory. `loadLayered`
|
|
60
60
|
passes its own `walkUp` through to both the dedicated-file and
|
|
61
|
-
fallback-file lookups.
|
|
61
|
+
fallback-file lookups. Built on `@theholocron/fs-utils`'s generic
|
|
62
|
+
`findUpward`/`hasGitEntry` — not reimplemented here.
|
|
62
63
|
|
|
63
64
|
### `loadConfigFromContent<T>({ dir, content, name, extension })`
|
|
64
65
|
|
|
@@ -88,7 +89,10 @@ paths actually merged, lowest priority first.
|
|
|
88
89
|
|
|
89
90
|
Deep-merge, `vite`-style: plain objects merge recursively, arrays
|
|
90
91
|
concatenate (`base` first), `undefined` in `override` is skipped, every
|
|
91
|
-
other `override` value replaces `base`. Neither input is mutated.
|
|
92
|
+
other `override` value replaces `base`. Neither input is mutated. A
|
|
93
|
+
re-export of `@theholocron/object-utils`'s `deepMerge` under this
|
|
94
|
+
package's established name — the merge logic itself has no awareness of
|
|
95
|
+
configs at all, so it lives there instead of being duplicated here.
|
|
92
96
|
|
|
93
97
|
### `createDefineConfig<T>()`
|
|
94
98
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { deepMerge as mergeConfig } from "@theholocron/object-utils";
|
|
1
2
|
//#region src/define.d.ts
|
|
2
3
|
/**
|
|
3
4
|
* Build a typed identity `defineConfig` for a specific config shape.
|
|
@@ -46,7 +47,10 @@ declare class ConfigFileError extends Error {
|
|
|
46
47
|
* on — stopping at the first directory containing a `.git` entry (checked
|
|
47
48
|
* after that directory's own config probe, so the project root itself is
|
|
48
49
|
* still searched) or the filesystem root. Never wanders past a repo
|
|
49
|
-
* boundary into an unrelated enclosing directory.
|
|
50
|
+
* boundary into an unrelated enclosing directory. Built on
|
|
51
|
+
* `@theholocron/fs-utils`'s `findUpward`/`hasGitEntry` — the same
|
|
52
|
+
* upward-walk shape `@theholocron/sentinel`'s own `findPackageRoot` needs,
|
|
53
|
+
* only with a different predicate.
|
|
50
54
|
*/
|
|
51
55
|
/** Default extension probe order, highest priority first. */
|
|
52
56
|
declare const DEFAULT_EXTENSIONS: readonly ["ts", "js", "mjs", "cjs", "json"];
|
|
@@ -133,17 +137,4 @@ interface LoadConfigFromContentOptions {
|
|
|
133
137
|
*/
|
|
134
138
|
declare function loadConfigFromContent<T>(opts: LoadConfigFromContentOptions): Promise<Loaded<T>>;
|
|
135
139
|
//#endregion
|
|
136
|
-
//#region src/merge.d.ts
|
|
137
|
-
/**
|
|
138
|
-
* Deep-merge two config objects, `vite`-style:
|
|
139
|
-
*
|
|
140
|
-
* - plain objects merge recursively
|
|
141
|
-
* - arrays concatenate (`base` first, then `override`)
|
|
142
|
-
* - an `undefined` value in `override` is skipped (keeps `base`)
|
|
143
|
-
* - every other `override` value replaces `base`
|
|
144
|
-
*
|
|
145
|
-
* Neither input is mutated.
|
|
146
|
-
*/
|
|
147
|
-
declare function mergeConfig<T>(base: T, override: unknown): T;
|
|
148
|
-
//#endregion
|
|
149
140
|
export { ConfigFileError, DEFAULT_EXTENSIONS, type LayeredResult, type LoadConfigFileOptions, type LoadConfigFromContentOptions, type LoadLayeredOptions, type Loaded, createDefineConfig, loadConfigFile, loadConfigFromContent, loadLayered, mergeConfig };
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { readFile, stat, writeFile } from "node:fs/promises";
|
|
2
|
-
import {
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { findUpward, hasGitEntry } from "@theholocron/fs-utils";
|
|
4
|
+
import { deepMerge, deepMerge as mergeConfig } from "@theholocron/object-utils";
|
|
3
5
|
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { errorMessage } from "@theholocron/misc-utils";
|
|
4
7
|
//#region src/define.ts
|
|
5
8
|
/**
|
|
6
9
|
* Build a typed identity `defineConfig` for a specific config shape.
|
|
@@ -34,33 +37,74 @@ var ConfigFileError = class extends Error {
|
|
|
34
37
|
}
|
|
35
38
|
};
|
|
36
39
|
//#endregion
|
|
37
|
-
//#region src/
|
|
40
|
+
//#region src/utils.ts
|
|
38
41
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* -
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* Neither input is mutated.
|
|
42
|
+
* The actual file-loading mechanics `load.ts`'s exported functions share:
|
|
43
|
+
* JSON parsing, dynamic `import()` (TS via `tsx`'s persistent ESM loader,
|
|
44
|
+
* JS/MJS/CJS natively), and unwrapping the CJS/ESM default-export
|
|
45
|
+
* double-wrap `tsx`'s transform produces. Kept separate from `load.ts`'s
|
|
46
|
+
* own orchestration (which directory, which extension order, upward
|
|
47
|
+
* search) — this file only knows "given one exact filepath and
|
|
48
|
+
* extension, load it," always throwing {@link ConfigFileError} on failure.
|
|
47
49
|
*/
|
|
48
|
-
function
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
async function loadFile(filepath, ext) {
|
|
51
|
+
if (ext === "json") return loadJson(filepath);
|
|
52
|
+
return extractDefault(filepath, ext === "ts" ? await importTs(filepath) : await importModule(filepath));
|
|
53
|
+
}
|
|
54
|
+
async function loadJson(filepath) {
|
|
55
|
+
const text = await readFile(filepath, "utf8");
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(text);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
throw new ConfigFileError(`${filepath} is not valid JSON: ${errorMessage(err)}`, filepath);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function importModule(filepath) {
|
|
63
|
+
try {
|
|
64
|
+
return await import(pathToFileURL(filepath).href);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
throw new ConfigFileError(`could not load ${filepath}: ${errorMessage(err)}`, filepath);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
let tsRegistered;
|
|
70
|
+
async function registerTsx() {
|
|
71
|
+
const { register } = await import("tsx/esm/api");
|
|
72
|
+
register();
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Load a `.ts` file. `tsx`'s ESM loader is registered once per process
|
|
76
|
+
* (lazily — only when a `.ts` file is actually loaded). `register()` is
|
|
77
|
+
* used over `tsImport()` because a single run may load several TS files
|
|
78
|
+
* (`loadLayered` reads a dedicated file *and* a parent file) and
|
|
79
|
+
* `tsImport`'s one-off register/unregister cycle is not reentrant.
|
|
80
|
+
*/
|
|
81
|
+
async function importTs(filepath) {
|
|
82
|
+
tsRegistered ??= registerTsx();
|
|
83
|
+
await tsRegistered;
|
|
84
|
+
try {
|
|
85
|
+
return await import(pathToFileURL(filepath).href);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
throw new ConfigFileError(`could not load ${filepath}: ${errorMessage(err)}`, filepath);
|
|
57
88
|
}
|
|
58
|
-
return out;
|
|
59
89
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Unwrap `export default`. `tsx` CJS-transforms `export default x` into
|
|
92
|
+
* `exports.default = x`, which dynamic import wraps as
|
|
93
|
+
* `{ default: { __esModule: true, default: x } }` — strip the extra layer
|
|
94
|
+
* when present so ESM and CJS outputs both resolve.
|
|
95
|
+
*/
|
|
96
|
+
function extractDefault(filepath, mod) {
|
|
97
|
+
const outer = mod.default;
|
|
98
|
+
const raw = outer?.__esModule === true ? outer.default : outer;
|
|
99
|
+
if (raw === void 0 || raw === null) throw new ConfigFileError(`${filepath} must have a default export (use \`export default defineConfig({…})\`)`, filepath);
|
|
100
|
+
return raw;
|
|
101
|
+
}
|
|
102
|
+
async function isFile(path) {
|
|
103
|
+
try {
|
|
104
|
+
return (await stat(path)).isFile();
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
64
108
|
}
|
|
65
109
|
//#endregion
|
|
66
110
|
//#region src/load.ts
|
|
@@ -80,7 +124,10 @@ function isPlainObject(value) {
|
|
|
80
124
|
* on — stopping at the first directory containing a `.git` entry (checked
|
|
81
125
|
* after that directory's own config probe, so the project root itself is
|
|
82
126
|
* still searched) or the filesystem root. Never wanders past a repo
|
|
83
|
-
* boundary into an unrelated enclosing directory.
|
|
127
|
+
* boundary into an unrelated enclosing directory. Built on
|
|
128
|
+
* `@theholocron/fs-utils`'s `findUpward`/`hasGitEntry` — the same
|
|
129
|
+
* upward-walk shape `@theholocron/sentinel`'s own `findPackageRoot` needs,
|
|
130
|
+
* only with a different predicate.
|
|
84
131
|
*/
|
|
85
132
|
/** Default extension probe order, highest priority first. */
|
|
86
133
|
const DEFAULT_EXTENSIONS = [
|
|
@@ -91,6 +138,23 @@ const DEFAULT_EXTENSIONS = [
|
|
|
91
138
|
"json"
|
|
92
139
|
];
|
|
93
140
|
/**
|
|
141
|
+
* Probe `dir` for `<name>.config.<ext>` in extension-priority order,
|
|
142
|
+
* loading and returning the first match. The one piece of per-directory
|
|
143
|
+
* orchestration `loadConfigFile` needs on both its non-walking and
|
|
144
|
+
* walking paths — pulled out so it can be unit-tested directly instead of
|
|
145
|
+
* only through `loadConfigFile`'s full probe/walk behavior.
|
|
146
|
+
*/
|
|
147
|
+
async function probeExtensions(dir, name, extensions) {
|
|
148
|
+
for (const ext of extensions) {
|
|
149
|
+
const filepath = join(dir, `${name}.config.${ext}`);
|
|
150
|
+
if (!await isFile(filepath)) continue;
|
|
151
|
+
return {
|
|
152
|
+
config: await loadFile(filepath, ext),
|
|
153
|
+
filepath
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
94
158
|
* Load the first `<name>.config.<ext>` found starting at `cwd` — just
|
|
95
159
|
* `cwd` itself by default, or walking up through ancestor directories when
|
|
96
160
|
* `walkUp` is set (see the module doc comment for the stop conditions).
|
|
@@ -99,22 +163,9 @@ const DEFAULT_EXTENSIONS = [
|
|
|
99
163
|
*/
|
|
100
164
|
async function loadConfigFile(opts) {
|
|
101
165
|
const extensions = opts.extensions ?? DEFAULT_EXTENSIONS;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const filepath = join(dir, `${opts.name}.config.${ext}`);
|
|
106
|
-
if (!await isFile(filepath)) continue;
|
|
107
|
-
return {
|
|
108
|
-
config: await loadFile(filepath, ext),
|
|
109
|
-
filepath
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
if (!opts.walkUp) return null;
|
|
113
|
-
if (await hasGitEntry(dir)) return null;
|
|
114
|
-
const parent = dirname(dir);
|
|
115
|
-
if (parent === dir) return null;
|
|
116
|
-
dir = parent;
|
|
117
|
-
}
|
|
166
|
+
const checkDir = (dir) => probeExtensions(dir, opts.name, extensions);
|
|
167
|
+
if (!opts.walkUp) return await checkDir(opts.cwd) ?? null;
|
|
168
|
+
return await findUpward(opts.cwd, checkDir, hasGitEntry) ?? null;
|
|
118
169
|
}
|
|
119
170
|
/**
|
|
120
171
|
* `<name>.config.*` layered over the `[fallback.key]` of
|
|
@@ -139,7 +190,7 @@ async function loadLayered(opts) {
|
|
|
139
190
|
}
|
|
140
191
|
}
|
|
141
192
|
if (!dedicated && base === void 0) return null;
|
|
142
|
-
const config =
|
|
193
|
+
const config = deepMerge(base, dedicated?.config);
|
|
143
194
|
const sources = [baseFile, dedicated?.filepath ?? null].filter((s) => s !== null);
|
|
144
195
|
return {
|
|
145
196
|
config,
|
|
@@ -165,74 +216,5 @@ async function loadConfigFromContent(opts) {
|
|
|
165
216
|
filepath
|
|
166
217
|
};
|
|
167
218
|
}
|
|
168
|
-
async function loadFile(filepath, ext) {
|
|
169
|
-
if (ext === "json") return loadJson(filepath);
|
|
170
|
-
return extractDefault(filepath, ext === "ts" ? await importTs(filepath) : await importModule(filepath));
|
|
171
|
-
}
|
|
172
|
-
async function loadJson(filepath) {
|
|
173
|
-
const text = await readFile(filepath, "utf8");
|
|
174
|
-
try {
|
|
175
|
-
return JSON.parse(text);
|
|
176
|
-
} catch (err) {
|
|
177
|
-
throw new ConfigFileError(`${filepath} is not valid JSON: ${message(err)}`, filepath);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
async function importModule(filepath) {
|
|
181
|
-
try {
|
|
182
|
-
return await import(pathToFileURL(filepath).href);
|
|
183
|
-
} catch (err) {
|
|
184
|
-
throw new ConfigFileError(`could not load ${filepath}: ${message(err)}`, filepath);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
let tsRegistered;
|
|
188
|
-
async function registerTsx() {
|
|
189
|
-
const { register } = await import("tsx/esm/api");
|
|
190
|
-
register();
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* Load a `.ts` config. `tsx`'s ESM loader is registered once per process
|
|
194
|
-
* (lazily — only when a `.ts` config is actually loaded). `register()` is
|
|
195
|
-
* used over `tsImport()` because a single run may load several TS configs
|
|
196
|
-
* (`loadLayered` reads a dedicated file *and* a parent file) and
|
|
197
|
-
* `tsImport`'s one-off register/unregister cycle is not reentrant.
|
|
198
|
-
*/
|
|
199
|
-
async function importTs(filepath) {
|
|
200
|
-
tsRegistered ??= registerTsx();
|
|
201
|
-
await tsRegistered;
|
|
202
|
-
try {
|
|
203
|
-
return await import(pathToFileURL(filepath).href);
|
|
204
|
-
} catch (err) {
|
|
205
|
-
throw new ConfigFileError(`could not load ${filepath}: ${message(err)}`, filepath);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* Unwrap `export default`. `tsx` CJS-transforms `export default x` into
|
|
210
|
-
* `exports.default = x`, which dynamic import wraps as
|
|
211
|
-
* `{ default: { __esModule: true, default: x } }` — strip the extra layer
|
|
212
|
-
* when present so ESM and CJS outputs both resolve.
|
|
213
|
-
*/
|
|
214
|
-
function extractDefault(filepath, mod) {
|
|
215
|
-
const outer = mod.default;
|
|
216
|
-
const raw = outer?.__esModule === true ? outer.default : outer;
|
|
217
|
-
if (raw === void 0 || raw === null) throw new ConfigFileError(`${filepath} must have a default export (use \`export default defineConfig({…})\`)`, filepath);
|
|
218
|
-
return raw;
|
|
219
|
-
}
|
|
220
|
-
async function isFile(path) {
|
|
221
|
-
try {
|
|
222
|
-
return (await stat(path)).isFile();
|
|
223
|
-
} catch {
|
|
224
|
-
return false;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
/** `.git` is a directory in a normal checkout, a file in a worktree/submodule — existence is all that matters. */
|
|
228
|
-
async function hasGitEntry(dir) {
|
|
229
|
-
try {
|
|
230
|
-
await stat(join(dir, ".git"));
|
|
231
|
-
return true;
|
|
232
|
-
} catch {
|
|
233
|
-
return false;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
const message = (err) => err instanceof Error ? err.message : String(err);
|
|
237
219
|
//#endregion
|
|
238
220
|
export { ConfigFileError, DEFAULT_EXTENSIONS, createDefineConfig, loadConfigFile, loadConfigFromContent, loadLayered, mergeConfig };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theholocron/datapad",
|
|
3
|
-
"version": "5.0.0-alpha.
|
|
3
|
+
"version": "5.0.0-alpha.115",
|
|
4
4
|
"description": "Generic config-file loading for Holocron — discover, load (JSON/JS/TS/ESM/CJS), and merge <name>.config.* files with a typed defineConfig.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"config",
|
|
@@ -32,13 +32,16 @@
|
|
|
32
32
|
"dist"
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
|
+
"@theholocron/fs-utils": "^1.9.2",
|
|
36
|
+
"@theholocron/misc-utils": "^1.9.2",
|
|
37
|
+
"@theholocron/object-utils": "^1.9.2",
|
|
35
38
|
"tsx": "4.23.12"
|
|
36
39
|
},
|
|
37
40
|
"devDependencies": {
|
|
38
|
-
"@theholocron/eslint-config": "^8.
|
|
39
|
-
"@theholocron/tsconfig": "^8.
|
|
40
|
-
"@theholocron/tsdown-config": "^8.
|
|
41
|
-
"@theholocron/vitest-config": "^8.
|
|
41
|
+
"@theholocron/eslint-config": "^8.7.0",
|
|
42
|
+
"@theholocron/tsconfig": "^8.7.0",
|
|
43
|
+
"@theholocron/tsdown-config": "^8.7.0",
|
|
44
|
+
"@theholocron/vitest-config": "^8.7.0",
|
|
42
45
|
"@types/node": "^26",
|
|
43
46
|
"@vitest/coverage-v8": "^4.1.11",
|
|
44
47
|
"@vitest/eslint-plugin": "^1.6.27",
|