@theholocron/datapad 5.0.0-alpha.113 → 5.0.0-alpha.114

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
@@ -43,13 +43,22 @@ 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 in `cwd`. Probe order is
49
- **TS-first** — `.ts` → `.js` → `.mjs` → `.cjs` → `.json` — overridable via
50
- `extensions`. Returns `{ config, filepath } | null`. `null` means "no such
51
- file"; a file that exists but cannot be parsed / loaded / has no default
52
- export throws `ConfigFileError`.
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.
53
62
 
54
63
  ### `loadConfigFromContent<T>({ dir, content, name, extension })`
55
64
 
@@ -67,7 +76,7 @@ Built for `@theholocron/sentinel`'s `validateConfig()` — it fetches a
67
76
  repo's `holocron.config.*` from GitHub's API, not a local checkout, but
68
77
  still needs the fetched content to execute as a real module.
69
78
 
70
- ### `loadLayered<T>({ cwd, name, fallback?, extensions? })`
79
+ ### `loadLayered<T>({ cwd, name, fallback?, extensions?, walkUp? })`
71
80
 
72
81
  `<name>.config.*` layered over the `[fallback.key]` of
73
82
  `<fallback.file>.config.*`. Either, both, or neither may be present; the
package/dist/index.d.mts CHANGED
@@ -39,16 +39,32 @@ declare class ConfigFileError extends Error {
39
39
  * Probe order is **TS-first**: `.ts` → `.js` → `.mjs` → `.cjs` → `.json`.
40
40
  * TS is loaded through `tsx`'s `tsImport` (a runtime dependency) so a
41
41
  * typed `defineConfig` file works with no build step.
42
+ *
43
+ * `walkUp` (off by default) adds the "nearest config" search
44
+ * cosmiconfig/postcss/stylelint/ESLint's flat config all do: when `cwd`
45
+ * itself has no match, check its parent, then its parent's parent, and so
46
+ * on — stopping at the first directory containing a `.git` entry (checked
47
+ * after that directory's own config probe, so the project root itself is
48
+ * still searched) or the filesystem root. Never wanders past a repo
49
+ * boundary into an unrelated enclosing directory.
42
50
  */
43
51
  /** Default extension probe order, highest priority first. */
44
52
  declare const DEFAULT_EXTENSIONS: readonly ["ts", "js", "mjs", "cjs", "json"];
45
53
  interface LoadConfigFileOptions {
46
- /** Directory to look in. */
54
+ /** Directory to start looking in. */
47
55
  cwd: string;
48
56
  /** Base name — `"holocron"` resolves `holocron.config.{ts,js,mjs,cjs,json}`. */
49
57
  name: string;
50
58
  /** Override the extension probe order. */
51
59
  extensions?: readonly string[];
60
+ /**
61
+ * Walk upward through ancestor directories when `cwd` itself has no
62
+ * match, stopping at the first directory containing a `.git` entry (that
63
+ * directory is still searched before stopping) or the filesystem root.
64
+ * Off by default — exact-`cwd`-only, unchanged behavior for any existing
65
+ * caller that doesn't pass this.
66
+ */
67
+ walkUp?: boolean;
52
68
  }
53
69
  interface Loaded<T> {
54
70
  config: T;
@@ -56,9 +72,11 @@ interface Loaded<T> {
56
72
  filepath: string;
57
73
  }
58
74
  /**
59
- * Load the first `<name>.config.<ext>` that exists in `cwd`. Returns
60
- * `null` when none is present; throws {@link ConfigFileError} when a file
61
- * exists but cannot be loaded.
75
+ * Load the first `<name>.config.<ext>` found starting at `cwd` — just
76
+ * `cwd` itself by default, or walking up through ancestor directories when
77
+ * `walkUp` is set (see the module doc comment for the stop conditions).
78
+ * Returns `null` when none is present anywhere searched; throws
79
+ * {@link ConfigFileError} when a file exists but cannot be loaded.
62
80
  */
63
81
  declare function loadConfigFile<T>(opts: LoadConfigFileOptions): Promise<Loaded<T> | null>;
64
82
  interface LoadLayeredOptions extends LoadConfigFileOptions {
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { readFile, stat, writeFile } from "node:fs/promises";
2
- import { join } from "node:path";
2
+ import { dirname, join } from "node:path";
3
3
  import { pathToFileURL } from "node:url";
4
4
  //#region src/define.ts
5
5
  /**
@@ -73,6 +73,14 @@ function isPlainObject(value) {
73
73
  * Probe order is **TS-first**: `.ts` → `.js` → `.mjs` → `.cjs` → `.json`.
74
74
  * TS is loaded through `tsx`'s `tsImport` (a runtime dependency) so a
75
75
  * typed `defineConfig` file works with no build step.
76
+ *
77
+ * `walkUp` (off by default) adds the "nearest config" search
78
+ * cosmiconfig/postcss/stylelint/ESLint's flat config all do: when `cwd`
79
+ * itself has no match, check its parent, then its parent's parent, and so
80
+ * on — stopping at the first directory containing a `.git` entry (checked
81
+ * after that directory's own config probe, so the project root itself is
82
+ * still searched) or the filesystem root. Never wanders past a repo
83
+ * boundary into an unrelated enclosing directory.
76
84
  */
77
85
  /** Default extension probe order, highest priority first. */
78
86
  const DEFAULT_EXTENSIONS = [
@@ -83,21 +91,30 @@ const DEFAULT_EXTENSIONS = [
83
91
  "json"
84
92
  ];
85
93
  /**
86
- * Load the first `<name>.config.<ext>` that exists in `cwd`. Returns
87
- * `null` when none is present; throws {@link ConfigFileError} when a file
88
- * exists but cannot be loaded.
94
+ * Load the first `<name>.config.<ext>` found starting at `cwd` — just
95
+ * `cwd` itself by default, or walking up through ancestor directories when
96
+ * `walkUp` is set (see the module doc comment for the stop conditions).
97
+ * Returns `null` when none is present anywhere searched; throws
98
+ * {@link ConfigFileError} when a file exists but cannot be loaded.
89
99
  */
90
100
  async function loadConfigFile(opts) {
91
101
  const extensions = opts.extensions ?? DEFAULT_EXTENSIONS;
92
- for (const ext of extensions) {
93
- const filepath = join(opts.cwd, `${opts.name}.config.${ext}`);
94
- if (!await isFile(filepath)) continue;
95
- return {
96
- config: await loadFile(filepath, ext),
97
- filepath
98
- };
102
+ let dir = opts.cwd;
103
+ for (;;) {
104
+ for (const ext of extensions) {
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;
99
117
  }
100
- return null;
101
118
  }
102
119
  /**
103
120
  * `<name>.config.*` layered over the `[fallback.key]` of
@@ -113,7 +130,8 @@ async function loadLayered(opts) {
113
130
  const parent = await loadConfigFile({
114
131
  cwd: opts.cwd,
115
132
  name: opts.fallback.file,
116
- extensions: opts.extensions
133
+ extensions: opts.extensions,
134
+ walkUp: opts.walkUp
117
135
  });
118
136
  if (parent && parent.config[opts.fallback.key] !== void 0) {
119
137
  base = parent.config[opts.fallback.key];
@@ -206,6 +224,15 @@ async function isFile(path) {
206
224
  return false;
207
225
  }
208
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
+ }
209
236
  const message = (err) => err instanceof Error ? err.message : String(err);
210
237
  //#endregion
211
238
  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.113",
3
+ "version": "5.0.0-alpha.114",
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",