@theholocron/fs-utils 1.9.2 → 1.10.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 +28 -1
- package/dist/index.d.mts +34 -0
- package/dist/index.mjs +80 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,7 +11,11 @@ pnpm add @theholocron/fs-utils
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
13
|
```typescript
|
|
14
|
-
import { findUpward, hasGitEntry } from "@theholocron/fs-utils";
|
|
14
|
+
import { findRoot, findUpward, hasGitEntry } from "@theholocron/fs-utils";
|
|
15
|
+
|
|
16
|
+
// nearest ancestor holding a marker file or directory
|
|
17
|
+
const repoRoot = await findRoot(process.cwd(), ".git");
|
|
18
|
+
const monorepoRoot = await findRoot(process.cwd(), ["pnpm-workspace.yaml", "turbo.json"]);
|
|
15
19
|
|
|
16
20
|
// find the nearest ancestor holding a config file, stopping at the repo root
|
|
17
21
|
const configDir = await findUpward(
|
|
@@ -35,6 +39,29 @@ with no result, instead of continuing to the parent. Without `stop`,
|
|
|
35
39
|
walks all the way to the filesystem root. Always terminates: directory
|
|
36
40
|
nesting is finite.
|
|
37
41
|
|
|
42
|
+
### `findRoot(startDir, marker, options?)` / `findRootSync(...)`
|
|
43
|
+
|
|
44
|
+
The nearest ancestor of `startDir` — itself first — that contains `marker`: a
|
|
45
|
+
file or directory name, or a list of names where any one counts. Resolves to
|
|
46
|
+
`undefined` when nothing is found. `findRootSync` is the synchronous twin.
|
|
47
|
+
Each marker must be a bare name — an empty string, `.`, `..` or anything with a
|
|
48
|
+
path separator throws a `TypeError`, as does any filesystem error other than
|
|
49
|
+
"not there" (e.g. `EACCES`). A relative `startDir` is resolved against the
|
|
50
|
+
working directory up front, so results are always absolute.
|
|
51
|
+
|
|
52
|
+
- `options.fallback: "start"` — return the start directory instead of
|
|
53
|
+
`undefined` when nothing is found (a soft-skip for callers that treat the
|
|
54
|
+
start dir as the root anyway; always absolute).
|
|
55
|
+
- `options.realpath: true` — resolve symlinks of `startDir` before
|
|
56
|
+
walking, so a symlinked start walks the physical tree instead of the link's
|
|
57
|
+
own ancestors. A start path that doesn't exist
|
|
58
|
+
counts as not found.
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
// a git root that never throws and never returns undefined
|
|
62
|
+
const root = findRootSync(cwd, ".git", { fallback: "start", realpath: true });
|
|
63
|
+
```
|
|
64
|
+
|
|
38
65
|
### `hasGitEntry(dir)`
|
|
39
66
|
|
|
40
67
|
Returns `true` when `dir` has a `.git` entry — a directory in a normal
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,37 @@
|
|
|
1
|
+
//#region src/find-root.d.ts
|
|
2
|
+
export interface FindRootOptions {
|
|
3
|
+
/**
|
|
4
|
+
* `"start"` returns the start directory instead of `undefined` when no
|
|
5
|
+
* marker is found anywhere up the tree — a soft-skip for callers that
|
|
6
|
+
* treat the start dir as the root anyway. The returned path is always
|
|
7
|
+
* absolute (a relative `startDir` is resolved against the working
|
|
8
|
+
* directory), and symlink-resolved when `realpath` is on and the start
|
|
9
|
+
* exists.
|
|
10
|
+
*/
|
|
11
|
+
fallback?: "start";
|
|
12
|
+
/**
|
|
13
|
+
* Resolve symlinks of `startDir` before walking, so a symlinked start
|
|
14
|
+
* dir walks the physical tree instead of the link's own lexical
|
|
15
|
+
* ancestors. A start path that doesn't exist on disk counts as "not
|
|
16
|
+
* found". Any other failure resolving it (e.g. `EACCES`) is thrown.
|
|
17
|
+
*/
|
|
18
|
+
realpath?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The nearest ancestor of `startDir` (itself first) that contains `marker` —
|
|
22
|
+
* a file or directory name, or a list of names where any one counts. For
|
|
23
|
+
* example `".git"`, `"pnpm-workspace.yaml"`, or `["turbo.json", "nx.json"]`.
|
|
24
|
+
*
|
|
25
|
+
* Each marker must be a bare name (no separators, `.`, `..` or empty) or a
|
|
26
|
+
* `TypeError` is thrown; so is any filesystem error other than "not there".
|
|
27
|
+
* Resolves to `undefined` when nothing is found, unless `options.fallback` is
|
|
28
|
+
* `"start"`. See {@link FindRootOptions}; {@link findRootSync} is the
|
|
29
|
+
* synchronous twin. Built on {@link findUpward}.
|
|
30
|
+
*/
|
|
31
|
+
export declare function findRoot(startDir: string, marker: string | readonly string[], options?: FindRootOptions): Promise<string | undefined>;
|
|
32
|
+
/** Synchronous {@link findRoot}. */
|
|
33
|
+
export declare function findRootSync(startDir: string, marker: string | readonly string[], options?: FindRootOptions): string | undefined;
|
|
34
|
+
//#endregion
|
|
1
35
|
//#region src/find-upward.d.ts
|
|
2
36
|
/**
|
|
3
37
|
* Walks upward from `startDir` — `startDir` itself first, then its parent,
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { realpath, stat } from "node:fs/promises";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
4
|
//#region src/find-upward.ts
|
|
4
5
|
/**
|
|
5
6
|
* Walks upward from `startDir` — `startDir` itself first, then its parent,
|
|
@@ -42,4 +43,80 @@ async function hasGitEntry(dir) {
|
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
45
|
//#endregion
|
|
45
|
-
|
|
46
|
+
//#region src/find-root.ts
|
|
47
|
+
/** Only "nothing there" means the marker is absent; every other fs error is real. */
|
|
48
|
+
const isAbsent = (err) => {
|
|
49
|
+
const code = err?.code;
|
|
50
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Markers are bare names, joined onto each ancestor. Anything that could
|
|
54
|
+
* address a path outside that ancestor (`..`, a separator) or the ancestor
|
|
55
|
+
* itself (`""`, `.`) is rejected rather than probed.
|
|
56
|
+
*/
|
|
57
|
+
function asMarkers(marker) {
|
|
58
|
+
const list = typeof marker === "string" ? [marker] : marker;
|
|
59
|
+
if (list.length === 0) throw new TypeError("findRoot: at least one marker is required");
|
|
60
|
+
for (const m of list) if (m === "" || m === "." || m === ".." || /[\\/]/.test(m) || m.includes("\0")) throw new TypeError(`findRoot: marker must be a single file or directory name, got ${JSON.stringify(m)}`);
|
|
61
|
+
return list;
|
|
62
|
+
}
|
|
63
|
+
async function hasAny(dir, markers) {
|
|
64
|
+
for (const marker of markers) try {
|
|
65
|
+
await stat(join(dir, marker));
|
|
66
|
+
return true;
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (!isAbsent(err)) throw err;
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
function hasAnySync(dir, markers) {
|
|
73
|
+
for (const marker of markers) try {
|
|
74
|
+
statSync(join(dir, marker));
|
|
75
|
+
return true;
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (!isAbsent(err)) throw err;
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The nearest ancestor of `startDir` (itself first) that contains `marker` —
|
|
83
|
+
* a file or directory name, or a list of names where any one counts. For
|
|
84
|
+
* example `".git"`, `"pnpm-workspace.yaml"`, or `["turbo.json", "nx.json"]`.
|
|
85
|
+
*
|
|
86
|
+
* Each marker must be a bare name (no separators, `.`, `..` or empty) or a
|
|
87
|
+
* `TypeError` is thrown; so is any filesystem error other than "not there".
|
|
88
|
+
* Resolves to `undefined` when nothing is found, unless `options.fallback` is
|
|
89
|
+
* `"start"`. See {@link FindRootOptions}; {@link findRootSync} is the
|
|
90
|
+
* synchronous twin. Built on {@link findUpward}.
|
|
91
|
+
*/
|
|
92
|
+
async function findRoot(startDir, marker, options = {}) {
|
|
93
|
+
const markers = asMarkers(marker);
|
|
94
|
+
let start = resolve(startDir);
|
|
95
|
+
if (options.realpath) try {
|
|
96
|
+
start = await realpath(start);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
if (!isAbsent(err)) throw err;
|
|
99
|
+
return options.fallback === "start" ? start : void 0;
|
|
100
|
+
}
|
|
101
|
+
return await findUpward(start, async (dir) => await hasAny(dir, markers) ? dir : void 0) ?? (options.fallback === "start" ? start : void 0);
|
|
102
|
+
}
|
|
103
|
+
/** Synchronous {@link findRoot}. */
|
|
104
|
+
function findRootSync(startDir, marker, options = {}) {
|
|
105
|
+
const markers = asMarkers(marker);
|
|
106
|
+
let start = resolve(startDir);
|
|
107
|
+
if (options.realpath) try {
|
|
108
|
+
start = realpathSync(start);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
if (!isAbsent(err)) throw err;
|
|
111
|
+
return options.fallback === "start" ? start : void 0;
|
|
112
|
+
}
|
|
113
|
+
let dir = start;
|
|
114
|
+
for (;;) {
|
|
115
|
+
if (hasAnySync(dir, markers)) return dir;
|
|
116
|
+
const parent = dirname(dir);
|
|
117
|
+
if (parent === dir) return options.fallback === "start" ? start : void 0;
|
|
118
|
+
dir = parent;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
//#endregion
|
|
122
|
+
export { findRoot, findRootSync, findUpward, hasGitEntry };
|