@theholocron/datapad 5.0.0-alpha.13 → 5.0.0-alpha.134
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 +39 -10
- package/dist/index.d.mts +57 -18
- package/dist/index.mjs +122 -93
- package/package.json +8 -5
package/README.md
CHANGED
|
@@ -43,15 +43,41 @@ export const defineConfig = createDefineConfig<TasksConfig>();
|
|
|
43
43
|
|
|
44
44
|
## API
|
|
45
45
|
|
|
46
|
-
### `loadConfigFile<T>({ cwd, name, extensions? })`
|
|
47
|
-
|
|
48
|
-
Loads the first `<name>.config.<ext>` found
|
|
49
|
-
**TS-first** — `.ts` → `.js` → `.mjs` → `.cjs` → `.json` — overridable
|
|
50
|
-
`extensions`. Returns `{ config, filepath } | null`. `null` means "no
|
|
51
|
-
file"; a file that exists but cannot be parsed /
|
|
52
|
-
export throws `ConfigFileError`.
|
|
53
|
-
|
|
54
|
-
|
|
46
|
+
### `loadConfigFile<T>({ cwd, name, extensions?, walkUp? })`
|
|
47
|
+
|
|
48
|
+
Loads the first `<name>.config.<ext>` found starting at `cwd`. Probe order
|
|
49
|
+
is **TS-first** — `.ts` → `.js` → `.mjs` → `.cjs` → `.json` — overridable
|
|
50
|
+
via `extensions`. Returns `{ config, filepath } | null`. `null` means "no
|
|
51
|
+
such file anywhere searched"; a file that exists but cannot be parsed /
|
|
52
|
+
loaded / has no default export throws `ConfigFileError`.
|
|
53
|
+
|
|
54
|
+
`walkUp: true` (off by default) adds the "nearest config" search
|
|
55
|
+
`cosmiconfig`/`postcss`/`stylelint`/ESLint's flat config all do: when `cwd`
|
|
56
|
+
has no match, check its parent, then its parent's parent, and so on —
|
|
57
|
+
stopping at the first directory containing a `.git` entry (that directory
|
|
58
|
+
is still searched before stopping) or the filesystem root. Never wanders
|
|
59
|
+
past a repo boundary into an unrelated enclosing directory. `loadLayered`
|
|
60
|
+
passes its own `walkUp` through to both the dedicated-file and
|
|
61
|
+
fallback-file lookups. Built on `@theholocron/fs-utils`'s generic
|
|
62
|
+
`findUpward`/`hasGitEntry` — not reimplemented here.
|
|
63
|
+
|
|
64
|
+
### `loadConfigFromContent<T>({ dir, content, name, extension })`
|
|
65
|
+
|
|
66
|
+
Loads config from content that didn't come from a file already on disk —
|
|
67
|
+
writes it to `<dir>/<name>.config.<extension>` first, then loads it through
|
|
68
|
+
the exact same internals `loadConfigFile` uses for a file it discovered
|
|
69
|
+
itself. `dir` matters: it's what upward `node_modules` resolution sees, so
|
|
70
|
+
content that does `import { defineConfig } from "@theholocron/cli"` only
|
|
71
|
+
resolves if `dir` sits under a tree where that package is a real
|
|
72
|
+
dependency. Callers own creating and cleaning up `dir`. Returns
|
|
73
|
+
`{ config, filepath }`; throws `ConfigFileError` the same way
|
|
74
|
+
`loadConfigFile` does on a parse/load failure.
|
|
75
|
+
|
|
76
|
+
Built for `@theholocron/sentinel`'s `validateConfig()` — it fetches a
|
|
77
|
+
repo's `holocron.config.*` from GitHub's API, not a local checkout, but
|
|
78
|
+
still needs the fetched content to execute as a real module.
|
|
79
|
+
|
|
80
|
+
### `loadLayered<T>({ cwd, name, fallback?, extensions?, walkUp? })`
|
|
55
81
|
|
|
56
82
|
`<name>.config.*` layered over the `[fallback.key]` of
|
|
57
83
|
`<fallback.file>.config.*`. Either, both, or neither may be present; the
|
|
@@ -63,7 +89,10 @@ paths actually merged, lowest priority first.
|
|
|
63
89
|
|
|
64
90
|
Deep-merge, `vite`-style: plain objects merge recursively, arrays
|
|
65
91
|
concatenate (`base` first), `undefined` in `override` is skipped, every
|
|
66
|
-
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.
|
|
67
96
|
|
|
68
97
|
### `createDefineConfig<T>()`
|
|
69
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.
|
|
@@ -31,22 +32,43 @@ declare class ConfigFileError extends Error {
|
|
|
31
32
|
//#endregion
|
|
32
33
|
//#region src/load.d.ts
|
|
33
34
|
/**
|
|
34
|
-
* Discover and load `<name>.config.*` files
|
|
35
|
-
*
|
|
35
|
+
* Discover and load `<name>.config.*` files, or load one from content that
|
|
36
|
+
* didn't come from a file at all ({@link loadConfigFromContent}).
|
|
37
|
+
* Holocron-agnostic — no schema, no validation, no defaults. Consumers
|
|
38
|
+
* layer those on top.
|
|
36
39
|
*
|
|
37
40
|
* Probe order is **TS-first**: `.ts` → `.js` → `.mjs` → `.cjs` → `.json`.
|
|
38
41
|
* TS is loaded through `tsx`'s `tsImport` (a runtime dependency) so a
|
|
39
42
|
* typed `defineConfig` file works with no build step.
|
|
43
|
+
*
|
|
44
|
+
* `walkUp` (off by default) adds the "nearest config" search
|
|
45
|
+
* cosmiconfig/postcss/stylelint/ESLint's flat config all do: when `cwd`
|
|
46
|
+
* itself has no match, check its parent, then its parent's parent, and so
|
|
47
|
+
* on — stopping at the first directory containing a `.git` entry (checked
|
|
48
|
+
* after that directory's own config probe, so the project root itself is
|
|
49
|
+
* still searched) or the filesystem root. Never wanders past a repo
|
|
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.
|
|
40
54
|
*/
|
|
41
55
|
/** Default extension probe order, highest priority first. */
|
|
42
56
|
declare const DEFAULT_EXTENSIONS: readonly ["ts", "js", "mjs", "cjs", "json"];
|
|
43
57
|
interface LoadConfigFileOptions {
|
|
44
|
-
/** Directory to
|
|
58
|
+
/** Directory to start looking in. */
|
|
45
59
|
cwd: string;
|
|
46
60
|
/** Base name — `"holocron"` resolves `holocron.config.{ts,js,mjs,cjs,json}`. */
|
|
47
61
|
name: string;
|
|
48
62
|
/** Override the extension probe order. */
|
|
49
63
|
extensions?: readonly string[];
|
|
64
|
+
/**
|
|
65
|
+
* Walk upward through ancestor directories when `cwd` itself has no
|
|
66
|
+
* match, stopping at the first directory containing a `.git` entry (that
|
|
67
|
+
* directory is still searched before stopping) or the filesystem root.
|
|
68
|
+
* Off by default — exact-`cwd`-only, unchanged behavior for any existing
|
|
69
|
+
* caller that doesn't pass this.
|
|
70
|
+
*/
|
|
71
|
+
walkUp?: boolean;
|
|
50
72
|
}
|
|
51
73
|
interface Loaded<T> {
|
|
52
74
|
config: T;
|
|
@@ -54,9 +76,11 @@ interface Loaded<T> {
|
|
|
54
76
|
filepath: string;
|
|
55
77
|
}
|
|
56
78
|
/**
|
|
57
|
-
* Load the first `<name>.config.<ext>`
|
|
58
|
-
* `
|
|
59
|
-
*
|
|
79
|
+
* Load the first `<name>.config.<ext>` found starting at `cwd` — just
|
|
80
|
+
* `cwd` itself by default, or walking up through ancestor directories when
|
|
81
|
+
* `walkUp` is set (see the module doc comment for the stop conditions).
|
|
82
|
+
* Returns `null` when none is present anywhere searched; throws
|
|
83
|
+
* {@link ConfigFileError} when a file exists but cannot be loaded.
|
|
60
84
|
*/
|
|
61
85
|
declare function loadConfigFile<T>(opts: LoadConfigFileOptions): Promise<Loaded<T> | null>;
|
|
62
86
|
interface LoadLayeredOptions extends LoadConfigFileOptions {
|
|
@@ -84,18 +108,33 @@ interface LayeredResult<T> {
|
|
|
84
108
|
* when neither source resolves.
|
|
85
109
|
*/
|
|
86
110
|
declare function loadLayered<T>(opts: LoadLayeredOptions): Promise<LayeredResult<T> | null>;
|
|
87
|
-
|
|
88
|
-
|
|
111
|
+
interface LoadConfigFromContentOptions {
|
|
112
|
+
/**
|
|
113
|
+
* Directory to write `<name>.config.<extension>` into before loading —
|
|
114
|
+
* this is what upward `node_modules` resolution sees, so a config that
|
|
115
|
+
* does `import { defineConfig } from "@theholocron/cli"` only resolves
|
|
116
|
+
* if `dir` sits under a tree where that package is a real dependency.
|
|
117
|
+
* Callers own creating and cleaning up `dir` — this function only
|
|
118
|
+
* writes the one file into it.
|
|
119
|
+
*/
|
|
120
|
+
dir: string;
|
|
121
|
+
/** Raw file content — not necessarily sourced from a local file at all (a network fetch, a git blob, …). */
|
|
122
|
+
content: string;
|
|
123
|
+
/** Base name — `"holocron"` writes `holocron.config.<extension>`. */
|
|
124
|
+
name: string;
|
|
125
|
+
/** Which of `DEFAULT_EXTENSIONS` `content` is — determines how it's interpreted (ts/js/mjs/cjs parsed as a module, json as `JSON.parse`). */
|
|
126
|
+
extension: string;
|
|
127
|
+
}
|
|
89
128
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
129
|
+
* Load config from content that didn't come from a file already on disk —
|
|
130
|
+
* writes it to `<dir>/<name>.config.<extension>` first, then loads it
|
|
131
|
+
* through the exact same path {@link loadConfigFile} uses for a file it
|
|
132
|
+
* discovered itself. Exists because sourcing config content from somewhere
|
|
133
|
+
* other than the local filesystem (a GitHub API fetch, for one — see
|
|
134
|
+
* `@theholocron/sentinel`) doesn't change what loading it correctly means:
|
|
135
|
+
* a `.ts` config still needs real module resolution, `defineConfig` import
|
|
136
|
+
* included, not a re-implemented parser.
|
|
98
137
|
*/
|
|
99
|
-
declare function
|
|
138
|
+
declare function loadConfigFromContent<T>(opts: LoadConfigFromContentOptions): Promise<Loaded<T>>;
|
|
100
139
|
//#endregion
|
|
101
|
-
export { ConfigFileError, DEFAULT_EXTENSIONS, type LayeredResult, type LoadConfigFileOptions, type LoadLayeredOptions, type Loaded, createDefineConfig, loadConfigFile, loadLayered, mergeConfig };
|
|
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
|
-
import { readFile, stat } from "node:fs/promises";
|
|
1
|
+
import { readFile, stat, writeFile } from "node:fs/promises";
|
|
2
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,43 +37,97 @@ 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
|
-
|
|
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.
|
|
49
|
+
*/
|
|
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.
|
|
47
80
|
*/
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
else if (isPlainObject(prev) && isPlainObject(value)) out[key] = mergeConfig(prev, value);
|
|
56
|
-
else out[key] = value;
|
|
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
|
|
67
111
|
/**
|
|
68
|
-
* Discover and load `<name>.config.*` files
|
|
69
|
-
*
|
|
112
|
+
* Discover and load `<name>.config.*` files, or load one from content that
|
|
113
|
+
* didn't come from a file at all ({@link loadConfigFromContent}).
|
|
114
|
+
* Holocron-agnostic — no schema, no validation, no defaults. Consumers
|
|
115
|
+
* layer those on top.
|
|
70
116
|
*
|
|
71
117
|
* Probe order is **TS-first**: `.ts` → `.js` → `.mjs` → `.cjs` → `.json`.
|
|
72
118
|
* TS is loaded through `tsx`'s `tsImport` (a runtime dependency) so a
|
|
73
119
|
* typed `defineConfig` file works with no build step.
|
|
120
|
+
*
|
|
121
|
+
* `walkUp` (off by default) adds the "nearest config" search
|
|
122
|
+
* cosmiconfig/postcss/stylelint/ESLint's flat config all do: when `cwd`
|
|
123
|
+
* itself has no match, check its parent, then its parent's parent, and so
|
|
124
|
+
* on — stopping at the first directory containing a `.git` entry (checked
|
|
125
|
+
* after that directory's own config probe, so the project root itself is
|
|
126
|
+
* still searched) or the filesystem root. Never wanders past a repo
|
|
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.
|
|
74
131
|
*/
|
|
75
132
|
/** Default extension probe order, highest priority first. */
|
|
76
133
|
const DEFAULT_EXTENSIONS = [
|
|
@@ -81,21 +138,34 @@ const DEFAULT_EXTENSIONS = [
|
|
|
81
138
|
"json"
|
|
82
139
|
];
|
|
83
140
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
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.
|
|
87
146
|
*/
|
|
88
|
-
async function
|
|
89
|
-
const extensions = opts.extensions ?? DEFAULT_EXTENSIONS;
|
|
147
|
+
async function probeExtensions(dir, name, extensions) {
|
|
90
148
|
for (const ext of extensions) {
|
|
91
|
-
const filepath = join(
|
|
149
|
+
const filepath = join(dir, `${name}.config.${ext}`);
|
|
92
150
|
if (!await isFile(filepath)) continue;
|
|
93
151
|
return {
|
|
94
152
|
config: await loadFile(filepath, ext),
|
|
95
153
|
filepath
|
|
96
154
|
};
|
|
97
155
|
}
|
|
98
|
-
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Load the first `<name>.config.<ext>` found starting at `cwd` — just
|
|
159
|
+
* `cwd` itself by default, or walking up through ancestor directories when
|
|
160
|
+
* `walkUp` is set (see the module doc comment for the stop conditions).
|
|
161
|
+
* Returns `null` when none is present anywhere searched; throws
|
|
162
|
+
* {@link ConfigFileError} when a file exists but cannot be loaded.
|
|
163
|
+
*/
|
|
164
|
+
async function loadConfigFile(opts) {
|
|
165
|
+
const extensions = opts.extensions ?? DEFAULT_EXTENSIONS;
|
|
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;
|
|
99
169
|
}
|
|
100
170
|
/**
|
|
101
171
|
* `<name>.config.*` layered over the `[fallback.key]` of
|
|
@@ -111,7 +181,8 @@ async function loadLayered(opts) {
|
|
|
111
181
|
const parent = await loadConfigFile({
|
|
112
182
|
cwd: opts.cwd,
|
|
113
183
|
name: opts.fallback.file,
|
|
114
|
-
extensions: opts.extensions
|
|
184
|
+
extensions: opts.extensions,
|
|
185
|
+
walkUp: opts.walkUp
|
|
115
186
|
});
|
|
116
187
|
if (parent && parent.config[opts.fallback.key] !== void 0) {
|
|
117
188
|
base = parent.config[opts.fallback.key];
|
|
@@ -119,7 +190,7 @@ async function loadLayered(opts) {
|
|
|
119
190
|
}
|
|
120
191
|
}
|
|
121
192
|
if (!dedicated && base === void 0) return null;
|
|
122
|
-
const config =
|
|
193
|
+
const config = deepMerge(base, dedicated?.config);
|
|
123
194
|
const sources = [baseFile, dedicated?.filepath ?? null].filter((s) => s !== null);
|
|
124
195
|
return {
|
|
125
196
|
config,
|
|
@@ -127,65 +198,23 @@ async function loadLayered(opts) {
|
|
|
127
198
|
sources
|
|
128
199
|
};
|
|
129
200
|
}
|
|
130
|
-
async function loadFile(filepath, ext) {
|
|
131
|
-
if (ext === "json") return loadJson(filepath);
|
|
132
|
-
return extractDefault(filepath, ext === "ts" ? await importTs(filepath) : await importModule(filepath));
|
|
133
|
-
}
|
|
134
|
-
async function loadJson(filepath) {
|
|
135
|
-
const text = await readFile(filepath, "utf8");
|
|
136
|
-
try {
|
|
137
|
-
return JSON.parse(text);
|
|
138
|
-
} catch (err) {
|
|
139
|
-
throw new ConfigFileError(`${filepath} is not valid JSON: ${message(err)}`, filepath);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
async function importModule(filepath) {
|
|
143
|
-
try {
|
|
144
|
-
return await import(pathToFileURL(filepath).href);
|
|
145
|
-
} catch (err) {
|
|
146
|
-
throw new ConfigFileError(`could not load ${filepath}: ${message(err)}`, filepath);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
let tsRegistered;
|
|
150
|
-
async function registerTsx() {
|
|
151
|
-
const { register } = await import("tsx/esm/api");
|
|
152
|
-
register();
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Load a `.ts` config. `tsx`'s ESM loader is registered once per process
|
|
156
|
-
* (lazily — only when a `.ts` config is actually loaded). `register()` is
|
|
157
|
-
* used over `tsImport()` because a single run may load several TS configs
|
|
158
|
-
* (`loadLayered` reads a dedicated file *and* a parent file) and
|
|
159
|
-
* `tsImport`'s one-off register/unregister cycle is not reentrant.
|
|
160
|
-
*/
|
|
161
|
-
async function importTs(filepath) {
|
|
162
|
-
tsRegistered ??= registerTsx();
|
|
163
|
-
await tsRegistered;
|
|
164
|
-
try {
|
|
165
|
-
return await import(pathToFileURL(filepath).href);
|
|
166
|
-
} catch (err) {
|
|
167
|
-
throw new ConfigFileError(`could not load ${filepath}: ${message(err)}`, filepath);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
201
|
/**
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
202
|
+
* Load config from content that didn't come from a file already on disk —
|
|
203
|
+
* writes it to `<dir>/<name>.config.<extension>` first, then loads it
|
|
204
|
+
* through the exact same path {@link loadConfigFile} uses for a file it
|
|
205
|
+
* discovered itself. Exists because sourcing config content from somewhere
|
|
206
|
+
* other than the local filesystem (a GitHub API fetch, for one — see
|
|
207
|
+
* `@theholocron/sentinel`) doesn't change what loading it correctly means:
|
|
208
|
+
* a `.ts` config still needs real module resolution, `defineConfig` import
|
|
209
|
+
* included, not a re-implemented parser.
|
|
175
210
|
*/
|
|
176
|
-
function
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
try {
|
|
184
|
-
return (await stat(path)).isFile();
|
|
185
|
-
} catch {
|
|
186
|
-
return false;
|
|
187
|
-
}
|
|
211
|
+
async function loadConfigFromContent(opts) {
|
|
212
|
+
const filepath = join(opts.dir, `${opts.name}.config.${opts.extension}`);
|
|
213
|
+
await writeFile(filepath, opts.content, "utf8");
|
|
214
|
+
return {
|
|
215
|
+
config: await loadFile(filepath, opts.extension),
|
|
216
|
+
filepath
|
|
217
|
+
};
|
|
188
218
|
}
|
|
189
|
-
const message = (err) => err instanceof Error ? err.message : String(err);
|
|
190
219
|
//#endregion
|
|
191
|
-
export { ConfigFileError, DEFAULT_EXTENSIONS, createDefineConfig, loadConfigFile, loadLayered, mergeConfig };
|
|
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.134",
|
|
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.1",
|
|
42
|
+
"@theholocron/tsconfig": "^8.7.1",
|
|
43
|
+
"@theholocron/tsdown-config": "^8.7.1",
|
|
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",
|