@effected/walker 0.21.0 → 0.22.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/Confine.js +16 -4
- package/ConfineReal.js +123 -0
- package/Descend.js +5 -1
- package/DescendSync.js +5 -1
- package/Walker.js +193 -38
- package/index.d.ts +375 -15
- package/index.js +2 -1
- package/internal/absolute.js +19 -0
- package/internal/descendMachine.js +50 -8
- package/internal/upwardDrivers.js +71 -0
- package/internal/upwardMachine.js +239 -0
- package/node-sync.d.ts +15 -2
- package/node-sync.js +7 -1
- package/package.json +2 -2
package/Confine.js
CHANGED
|
@@ -23,6 +23,12 @@ import { Effect, Result, Schema } from "effect";
|
|
|
23
23
|
* reserved device name (`CON`, `PRN`, `AUX`, `NUL`, `COM1`–`COM9`,
|
|
24
24
|
* `LPT1`–`LPT9`), in any case and with or without an extension
|
|
25
25
|
* (`nul.txt`).
|
|
26
|
+
* - `outside` — `confineReal` only: the candidate's real path, links
|
|
27
|
+
* resolved, lies outside the base's real path.
|
|
28
|
+
* - `unresolvable` — `confineReal` only: resolving the candidate or the base
|
|
29
|
+
* failed for a reason other than absence (a permission error, a link loop,
|
|
30
|
+
* a file used as a directory, a dangling link), so containment cannot be
|
|
31
|
+
* verified. The failure rides in the error's `cause`.
|
|
26
32
|
*
|
|
27
33
|
* @public
|
|
28
34
|
*/
|
|
@@ -31,7 +37,9 @@ const PathEscapeReason = Schema.Literals([
|
|
|
31
37
|
"absolute",
|
|
32
38
|
"traversal",
|
|
33
39
|
"empty",
|
|
34
|
-
"reservedName"
|
|
40
|
+
"reservedName",
|
|
41
|
+
"outside",
|
|
42
|
+
"unresolvable"
|
|
35
43
|
]);
|
|
36
44
|
/**
|
|
37
45
|
* An untrusted relative path would leave, or does not name anything under,
|
|
@@ -50,7 +58,9 @@ var PathEscapeError = class extends Schema.TaggedError()("PathEscapeError", {
|
|
|
50
58
|
/** The candidate path, exactly as the caller passed it. */
|
|
51
59
|
candidate: Schema.String,
|
|
52
60
|
/** Why the candidate was refused. */
|
|
53
|
-
reason: PathEscapeReason
|
|
61
|
+
reason: PathEscapeReason,
|
|
62
|
+
/** The resolution failure, present when `reason` is `"unresolvable"`. */
|
|
63
|
+
cause: Schema.optionalKey(Schema.Defect())
|
|
54
64
|
}) {
|
|
55
65
|
get message() {
|
|
56
66
|
return `the path ${JSON.stringify(this.candidate)} ${REASON_TEXT[this.reason]}`;
|
|
@@ -62,7 +72,9 @@ const REASON_TEXT = {
|
|
|
62
72
|
absolute: "is absolute, so it is not confined to the base directory",
|
|
63
73
|
traversal: "climbs above the base directory",
|
|
64
74
|
empty: "names the base directory itself, not an entry under it",
|
|
65
|
-
reservedName: "has a segment win32 reserves or reads as something else"
|
|
75
|
+
reservedName: "has a segment win32 reserves or reads as something else",
|
|
76
|
+
outside: "resolves, links followed, to a path outside the base directory",
|
|
77
|
+
unresolvable: "could not be resolved, so its containment cannot be verified"
|
|
66
78
|
};
|
|
67
79
|
/** A win32 drive prefix: `C:\x`, `C:/x`, and the drive-relative `C:x`. */
|
|
68
80
|
const DRIVE_PREFIX = /^[A-Za-z]:/;
|
|
@@ -104,7 +116,7 @@ const stripTrailingSeparators = (path) => trimTrailing(path, (character) => char
|
|
|
104
116
|
* base yields the normalised relative path alone. The check is lexical, not
|
|
105
117
|
* physical, and symlinks are out of scope: a symlink already inside the
|
|
106
118
|
* base can still point out of it, so a caller extracting into a directory
|
|
107
|
-
* it does not own
|
|
119
|
+
* it does not own checks the result with `confineReal` as well.
|
|
108
120
|
*
|
|
109
121
|
* {@link confineRelative} is defined in terms of this function and adds
|
|
110
122
|
* only the tracing span.
|
package/ConfineReal.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { PathEscapeError } from "./Confine.js";
|
|
2
|
+
import { runUpward, runUpwardSync } from "./internal/upwardDrivers.js";
|
|
3
|
+
import { confineRealMachine } from "./internal/upwardMachine.js";
|
|
4
|
+
import { Effect, Path, Result } from "effect";
|
|
5
|
+
|
|
6
|
+
// src/ConfineReal.ts
|
|
7
|
+
/** The typed error a refusal describes — the one conversion both forms share. */
|
|
8
|
+
const toResult = (candidate, outcome) => {
|
|
9
|
+
if (outcome._tag === "Ok") return Result.succeed(outcome.value);
|
|
10
|
+
if (outcome._tag === "Unresolvable") return Result.fail(new PathEscapeError({
|
|
11
|
+
candidate,
|
|
12
|
+
reason: "unresolvable",
|
|
13
|
+
cause: outcome.cause
|
|
14
|
+
}));
|
|
15
|
+
return Result.fail(new PathEscapeError({
|
|
16
|
+
candidate,
|
|
17
|
+
reason: outcome.reason
|
|
18
|
+
}));
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Confine a path to a base directory PHYSICALLY: resolve links in both, and
|
|
22
|
+
* succeed with the candidate's real path only when it lies strictly inside
|
|
23
|
+
* the base's real path.
|
|
24
|
+
*
|
|
25
|
+
* @remarks
|
|
26
|
+
* Reach for it where `confineRelative` stops: a link inside the base that
|
|
27
|
+
* points out of it (a symlinked `dist`, a planted `node_modules` entry), an
|
|
28
|
+
* absolute path an agent or a tool handed over, a `descend` match — a
|
|
29
|
+
* matched link is answered even when its target lies outside `cwd`.
|
|
30
|
+
*
|
|
31
|
+
* The candidate may be absolute (checked as it is) or relative (taken
|
|
32
|
+
* against `base`). It is resolved the way the operating system resolves it:
|
|
33
|
+
* segment by segment, so a `..` after a link climbs out of the link's
|
|
34
|
+
* TARGET — `base/link/../x` is outside when `link` points outside — rather
|
|
35
|
+
* than being collapsed lexically first, as `path.resolve` and node's JS
|
|
36
|
+
* `fs.realpath` would. A path, or the tail of one, that does not exist yet
|
|
37
|
+
* is resolved lexically from its real existing ancestor, so a write target
|
|
38
|
+
* can be checked before it is created — but only once absence is proved: a
|
|
39
|
+
* segment whose real path is not found is checked again without following
|
|
40
|
+
* links, because a dangling link is "not found" too, and a write through one
|
|
41
|
+
* creates its target wherever it points. The base is resolved the same way.
|
|
42
|
+
*
|
|
43
|
+
* Refused, typed as {@link PathEscapeError}: a NUL character (`nulByte`);
|
|
44
|
+
* the empty string or any path whose real location IS the base (`empty`,
|
|
45
|
+
* the same ruling `confineRelative` makes — a caller confining a path names
|
|
46
|
+
* an entry under the base); on win32 a drive-relative `C:x` or a driveless
|
|
47
|
+
* rooted `\x`, which names the current drive (`absolute`), or a segment
|
|
48
|
+
* win32 reads as something else, such as a device name or `.. `
|
|
49
|
+
* (`reservedName`); a real path outside the base's (`outside`); and a
|
|
50
|
+
* resolution that fails for a reason other than absence, such as a
|
|
51
|
+
* permission error, a link loop, or a dangling link — a link whose target
|
|
52
|
+
* does not exist, refused even when that target would lie inside the base
|
|
53
|
+
* (`unresolvable`, with the failure in `cause`) — containment that cannot be
|
|
54
|
+
* verified is refused, never assumed.
|
|
55
|
+
*
|
|
56
|
+
* A relative `base` — the empty string included — is a **defect**: it would
|
|
57
|
+
* be resolved against the process working directory, the silent wrong-root
|
|
58
|
+
* walk `ascend` refuses for a relative `stopAt`. Absoluteness is judged by
|
|
59
|
+
* the injected `Path`, so win32 accepts `C:\repo` and `\\server\share`;
|
|
60
|
+
* a win32 `\repo`, rooted on whichever drive the process is on, is a defect
|
|
61
|
+
* too.
|
|
62
|
+
*
|
|
63
|
+
* Use the returned path, not the candidate: it is the location that was
|
|
64
|
+
* checked. Comparison is exact on the real paths, so on a case-insensitive
|
|
65
|
+
* filesystem a base and a candidate spelled in different case may be refused
|
|
66
|
+
* — the check fails closed. As with any check-then-use on a live filesystem,
|
|
67
|
+
* a link swapped in between the check and the use is not covered.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* import { confineReal } from "@effected/walker";
|
|
72
|
+
* import { Effect } from "effect";
|
|
73
|
+
*
|
|
74
|
+
* const target = confineReal("/repo", "packages/app/dist").pipe(
|
|
75
|
+
* Effect.catchTag("PathEscapeError", (error) => Effect.die(error)),
|
|
76
|
+
* );
|
|
77
|
+
* // Requires `FileSystem` and `Path` from the platform layer.
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* @param base - The absolute directory the candidate must stay under.
|
|
81
|
+
* @param candidate - The path to check: absolute, or relative to `base`.
|
|
82
|
+
* @returns An `Effect` succeeding with the candidate's real path, or failing
|
|
83
|
+
* with {@link PathEscapeError}.
|
|
84
|
+
*
|
|
85
|
+
* @public
|
|
86
|
+
*/
|
|
87
|
+
const confineReal = Effect.fn("Walker.confineReal")(function* (base, candidate) {
|
|
88
|
+
const path = yield* Path.Path;
|
|
89
|
+
const outcome = yield* runUpward(confineRealMachine("Walker.confineReal", base, candidate, path));
|
|
90
|
+
return yield* Effect.fromResult(toResult(candidate, outcome));
|
|
91
|
+
});
|
|
92
|
+
/**
|
|
93
|
+
* {@link confineReal}, synchronously, over a caller-supplied port.
|
|
94
|
+
*
|
|
95
|
+
* @remarks
|
|
96
|
+
* The same resolution from the same code, through the port's `realPath`; a
|
|
97
|
+
* `realPath` throwing `code: "ENOENT"` is absence only when the port's
|
|
98
|
+
* `lstat` throws `ENOENT` too (otherwise a dangling link is there); anything
|
|
99
|
+
* else is `unresolvable`, with the thrown value as the `cause`. A relative `base`
|
|
100
|
+
* throws. For a caller with no Effect runtime; inside Effect, use
|
|
101
|
+
* `confineReal`.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* import { confineRealSync } from "@effected/walker";
|
|
106
|
+
* import { nodeWalkerSyncOps } from "@effected/walker/node-sync";
|
|
107
|
+
* import { Result } from "effect";
|
|
108
|
+
*
|
|
109
|
+
* const checked = confineRealSync("/repo", "/repo/packages/app/dist", nodeWalkerSyncOps);
|
|
110
|
+
* if (Result.isFailure(checked)) console.error(checked.failure.message);
|
|
111
|
+
* ```
|
|
112
|
+
*
|
|
113
|
+
* @param base - The absolute directory the candidate must stay under.
|
|
114
|
+
* @param candidate - The path to check: absolute, or relative to `base`.
|
|
115
|
+
* @param ops - The platform: spread or pass `nodeWalkerSyncOps`.
|
|
116
|
+
* @returns A `Result` succeeding with the candidate's real path, or failing
|
|
117
|
+
* with {@link PathEscapeError}.
|
|
118
|
+
*
|
|
119
|
+
* @public
|
|
120
|
+
*/
|
|
121
|
+
const confineRealSync = (base, candidate, ops) => toResult(candidate, runUpwardSync(confineRealMachine("Walker.confineRealSync", base, candidate, ops.path), ops.fileSystem));
|
|
122
|
+
|
|
123
|
+
export { confineReal, confineRealSync };
|
package/Descend.js
CHANGED
|
@@ -56,7 +56,11 @@ const walk = (pattern, options) => Effect.gen(function* () {
|
|
|
56
56
|
case "ReadDirectory": return read(fs.readDirectory(request.path));
|
|
57
57
|
}
|
|
58
58
|
};
|
|
59
|
-
const machine = descendMachine(pattern, options,
|
|
59
|
+
const machine = descendMachine(pattern, options, {
|
|
60
|
+
sep: path.sep,
|
|
61
|
+
join: (a, b) => path.join(a, b),
|
|
62
|
+
isAbsolute: (p) => path.isAbsolute(p)
|
|
63
|
+
});
|
|
60
64
|
let step = machine.next();
|
|
61
65
|
while (step.done !== true) step = machine.next(yield* answer(step.value));
|
|
62
66
|
const result = step.value;
|
package/DescendSync.js
CHANGED
|
@@ -45,7 +45,11 @@ const walkSync = (pattern, options) => {
|
|
|
45
45
|
case "ReadDirectory": return read(() => fileSystem.readDirectory(request.path));
|
|
46
46
|
}
|
|
47
47
|
};
|
|
48
|
-
const machine = descendMachine(pattern, options,
|
|
48
|
+
const machine = descendMachine(pattern, options, {
|
|
49
|
+
sep: path.sep,
|
|
50
|
+
join: (a, b) => path.join(a, b),
|
|
51
|
+
isAbsolute: (p) => path.isAbsolute(p)
|
|
52
|
+
});
|
|
49
53
|
let step = machine.next();
|
|
50
54
|
while (step.done !== true) step = machine.next(answer(step.value));
|
|
51
55
|
const result = step.value;
|
package/Walker.js
CHANGED
|
@@ -1,21 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { runUpward, runUpwardSync } from "./internal/upwardDrivers.js";
|
|
2
|
+
import { ascendChain, ascendToMarkerMachine, ascendWithinMachine, findUpwardAllMachine, findUpwardMachine } from "./internal/upwardMachine.js";
|
|
3
|
+
import { Effect, Option, Path } from "effect";
|
|
2
4
|
|
|
3
5
|
// src/Walker.ts
|
|
4
|
-
const ascendToPhysical = (start, ceiling, options) => Effect.gen(function* () {
|
|
5
|
-
const fs = yield* FileSystem.FileSystem;
|
|
6
|
-
const path = yield* Path.Path;
|
|
7
|
-
if (!path.isAbsolute(ceiling)) return yield* Effect.die(/* @__PURE__ */ new Error(`Walker.ascendWithin: ceiling must be an absolute path, received ${JSON.stringify(ceiling)} (ascending from ${JSON.stringify(start)})`));
|
|
8
|
-
const dirs = yield* Walker.ascend(start, {
|
|
9
|
-
...options,
|
|
10
|
-
stopAt: ceiling
|
|
11
|
-
});
|
|
12
|
-
const last = dirs.at(-1);
|
|
13
|
-
if (last !== void 0 && path.resolve(last) === path.resolve(ceiling)) return dirs;
|
|
14
|
-
const physical = yield* Effect.option(fs.realPath(ceiling));
|
|
15
|
-
if (Option.isNone(physical)) return dirs;
|
|
16
|
-
const hit = yield* Walker.findRoot(dirs, (dir) => Effect.map(fs.realPath(dir), (real) => real === physical.value));
|
|
17
|
-
return Option.isSome(hit) ? dirs.slice(0, dirs.indexOf(hit.value) + 1) : dirs;
|
|
18
|
-
});
|
|
19
6
|
/**
|
|
20
7
|
* Upward path traversal primitives: the directory chain from a start path to the
|
|
21
8
|
* root, and first-match scans over it.
|
|
@@ -61,20 +48,9 @@ var Walker = class Walker {
|
|
|
61
48
|
static ascend(start, options) {
|
|
62
49
|
return Effect.gen(function* () {
|
|
63
50
|
const path = yield* Path.Path;
|
|
64
|
-
const
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
const ceiling = options?.stopAt === void 0 ? void 0 : path.resolve(options.stopAt);
|
|
68
|
-
const dirs = [];
|
|
69
|
-
let current = start;
|
|
70
|
-
for (let depth = 0; depth < maxDepth; depth++) {
|
|
71
|
-
dirs.push(current);
|
|
72
|
-
if (ceiling !== void 0 && (current === ceiling || path.resolve(current) === ceiling)) break;
|
|
73
|
-
const parent = path.dirname(current);
|
|
74
|
-
if (parent === current) break;
|
|
75
|
-
current = parent;
|
|
76
|
-
}
|
|
77
|
-
return dirs;
|
|
51
|
+
const chain = ascendChain("Walker.ascend", start, options, path);
|
|
52
|
+
if (chain._tag === "Defect") return yield* Effect.die(new Error(chain.message));
|
|
53
|
+
return chain.value;
|
|
78
54
|
});
|
|
79
55
|
}
|
|
80
56
|
/**
|
|
@@ -106,8 +82,8 @@ var Walker = class Walker {
|
|
|
106
82
|
*/
|
|
107
83
|
static ascendWithin(start, ceiling, options) {
|
|
108
84
|
return Effect.gen(function* () {
|
|
109
|
-
|
|
110
|
-
return yield*
|
|
85
|
+
const path = yield* Path.Path;
|
|
86
|
+
return yield* runUpward(ascendWithinMachine("Walker.ascendWithin", start, Option.getOrUndefined(ceiling), options, path));
|
|
111
87
|
});
|
|
112
88
|
}
|
|
113
89
|
/**
|
|
@@ -139,12 +115,7 @@ var Walker = class Walker {
|
|
|
139
115
|
* output — a few hundred strings under the default `maxDepth`.
|
|
140
116
|
*/
|
|
141
117
|
static findUpward(dirs, candidatesFor) {
|
|
142
|
-
return Effect.
|
|
143
|
-
const fs = yield* FileSystem.FileSystem;
|
|
144
|
-
const candidates = [];
|
|
145
|
-
for (const dir of dirs) candidates.push(...candidatesFor(dir));
|
|
146
|
-
return yield* Walker.firstMatch(candidates, (candidate) => fs.exists(candidate));
|
|
147
|
-
});
|
|
118
|
+
return Effect.map(runUpward(findUpwardMachine(dirs, candidatesFor)), Option.fromUndefinedOr);
|
|
148
119
|
}
|
|
149
120
|
/**
|
|
150
121
|
* The first directory in `dirs` that `isRoot` accepts.
|
|
@@ -157,6 +128,190 @@ var Walker = class Walker {
|
|
|
157
128
|
static findRoot(dirs, isRoot) {
|
|
158
129
|
return Walker.firstMatch(dirs, isRoot);
|
|
159
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* {@link Walker.ascend} stopped at the nearest directory that holds any of
|
|
133
|
+
* `markers`, inclusive: the chain from `start` up to and including the
|
|
134
|
+
* first directory with a `.git` (or whatever the markers name) entry.
|
|
135
|
+
*
|
|
136
|
+
* @remarks
|
|
137
|
+
* A marker is an entry NAME probed with `FileSystem.exists` in each
|
|
138
|
+
* directory, nearest first, so any entry kind counts — a `.git` directory,
|
|
139
|
+
* or the `.git` FILE of a worktree or submodule — and a link counts when it
|
|
140
|
+
* resolves. The probes stop at the hit: nothing above the directory that
|
|
141
|
+
* holds a marker is ever touched. With no marker anywhere the chain runs to
|
|
142
|
+
* the filesystem root, exactly as `ascend(start)` gives it.
|
|
143
|
+
*
|
|
144
|
+
* A failed probe is absorbed as "no marker here", as
|
|
145
|
+
* {@link Walker.firstMatch} absorbs one, so one unreadable directory never
|
|
146
|
+
* hides a marker above it. An empty `markers` list, or an empty marker
|
|
147
|
+
* name, is a **defect**: the first would silently ask for the unbounded
|
|
148
|
+
* walk and the second would match every directory, and both are
|
|
149
|
+
* statically wrong at the call site. An invalid `maxDepth` dies exactly as
|
|
150
|
+
* `ascend`'s does.
|
|
151
|
+
*
|
|
152
|
+
* There is no `stopAt` here: a marker stop and a lexical ceiling answer the
|
|
153
|
+
* same question two ways. To bound a marker walk by a ceiling as well,
|
|
154
|
+
* take the shorter of the two chains.
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```ts
|
|
158
|
+
* import { Walker } from "@effected/walker";
|
|
159
|
+
* import { Effect } from "effect";
|
|
160
|
+
*
|
|
161
|
+
* const program = Effect.gen(function* () {
|
|
162
|
+
* // With `.git` in /repo: ["/repo/packages/app", "/repo/packages", "/repo"]
|
|
163
|
+
* return yield* Walker.ascendToMarker("/repo/packages/app", [".git"]);
|
|
164
|
+
* });
|
|
165
|
+
* // Requires `FileSystem` and `Path` from the platform layer.
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
168
|
+
static ascendToMarker(start, markers, options) {
|
|
169
|
+
return Effect.gen(function* () {
|
|
170
|
+
const path = yield* Path.Path;
|
|
171
|
+
return yield* runUpward(ascendToMarkerMachine("Walker.ascendToMarker", start, markers, options, path));
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Every existing candidate of the NEAREST directory in `dirs` that has any,
|
|
176
|
+
* or `Option.none()` — {@link Walker.findUpward} for a caller that must see
|
|
177
|
+
* a second hit (an ambiguity) rather than have the first one chosen.
|
|
178
|
+
*
|
|
179
|
+
* @remarks
|
|
180
|
+
* Directories are scanned in order, and every candidate of a directory is
|
|
181
|
+
* probed before the scan ascends; the scan stops after the first directory
|
|
182
|
+
* with a hit, so nothing above it is probed. `found` keeps the order
|
|
183
|
+
* `candidatesFor` produced and is never empty. A failed probe is absorbed
|
|
184
|
+
* as "not there", as in `findUpward`, so not-found and cannot-look are
|
|
185
|
+
* indistinguishable here too.
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* ```ts
|
|
189
|
+
* import { Walker } from "@effected/walker";
|
|
190
|
+
* import { Effect, Option } from "effect";
|
|
191
|
+
*
|
|
192
|
+
* const program = Effect.gen(function* () {
|
|
193
|
+
* const dirs = yield* Walker.ascend("/repo/packages/app");
|
|
194
|
+
* const hits = yield* Walker.findUpwardAll(dirs, (dir) => [`${dir}/tool.config.ts`, `${dir}/tool.config.js`]);
|
|
195
|
+
* // Two config files in one directory is the caller's ambiguity to report.
|
|
196
|
+
* return Option.map(hits, ({ found }) => found.length > 1);
|
|
197
|
+
* });
|
|
198
|
+
* ```
|
|
199
|
+
*/
|
|
200
|
+
static findUpwardAll(dirs, candidatesFor) {
|
|
201
|
+
return Effect.map(runUpward(findUpwardAllMachine(dirs, candidatesFor)), Option.fromUndefinedOr);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* {@link Walker.ascend}, synchronously, over a caller-supplied path
|
|
205
|
+
* implementation.
|
|
206
|
+
*
|
|
207
|
+
* @remarks
|
|
208
|
+
* The same chain from the same code: lexical, nearest first, `stopAt`
|
|
209
|
+
* inclusive and compared in resolved form. A relative `stopAt` or an
|
|
210
|
+
* invalid `maxDepth` is a defect, which here THROWS, since there is no
|
|
211
|
+
* error channel to die through. For a caller with no Effect runtime (a CLI
|
|
212
|
+
* helper that runs before one, a bundler-plugin hook); inside Effect, use
|
|
213
|
+
* `ascend`.
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* ```ts
|
|
217
|
+
* import { Walker } from "@effected/walker";
|
|
218
|
+
* import { nodeWalkerPath } from "@effected/walker/node-sync";
|
|
219
|
+
*
|
|
220
|
+
* const dirs = Walker.ascendSync("/repo/packages/app", { stopAt: "/repo", path: nodeWalkerPath });
|
|
221
|
+
* // => ["/repo/packages/app", "/repo/packages", "/repo"]
|
|
222
|
+
* ```
|
|
223
|
+
*/
|
|
224
|
+
static ascendSync(start, options) {
|
|
225
|
+
const chain = ascendChain("Walker.ascendSync", start, options, options.path);
|
|
226
|
+
if (chain._tag === "Defect") throw new Error(chain.message);
|
|
227
|
+
return chain.value;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* {@link Walker.ascendWithin}, synchronously, over a caller-supplied port:
|
|
231
|
+
* the lexical chain stopped at the nearest directory whose `realPath` is
|
|
232
|
+
* the ceiling's.
|
|
233
|
+
*
|
|
234
|
+
* @remarks
|
|
235
|
+
* The same walk from the same code as `ascendWithin`: `Option.none()` is
|
|
236
|
+
* exactly `ascendSync(start)`, a ceiling the chain already spells costs no
|
|
237
|
+
* `realPath`, a `realPath` that throws is absorbed as "not the ceiling",
|
|
238
|
+
* and an unresolvable ceiling leaves the lexical chain standing. A
|
|
239
|
+
* relative ceiling or an invalid `maxDepth` throws.
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* ```ts
|
|
243
|
+
* import { Walker } from "@effected/walker";
|
|
244
|
+
* import { nodeWalkerSyncOps } from "@effected/walker/node-sync";
|
|
245
|
+
* import { Option } from "effect";
|
|
246
|
+
*
|
|
247
|
+
* const dirs = Walker.ascendWithinSync("/repo/packages/app", Option.some("/repo"), nodeWalkerSyncOps);
|
|
248
|
+
* ```
|
|
249
|
+
*/
|
|
250
|
+
static ascendWithinSync(start, ceiling, options) {
|
|
251
|
+
return runUpwardSync(ascendWithinMachine("Walker.ascendWithinSync", start, Option.getOrUndefined(ceiling), options, options.path), options.fileSystem);
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* {@link Walker.ascendToMarker}, synchronously, over a caller-supplied port.
|
|
255
|
+
*
|
|
256
|
+
* @remarks
|
|
257
|
+
* The same walk from the same code. A marker exists when the port's `stat`
|
|
258
|
+
* (links followed) answers for it; a `stat` that throws, for any reason,
|
|
259
|
+
* is "no marker here", as the `Effect` form absorbs a failed probe. An
|
|
260
|
+
* empty `markers` list, an empty marker name or an invalid `maxDepth`
|
|
261
|
+
* throws.
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* ```ts
|
|
265
|
+
* import { Walker } from "@effected/walker";
|
|
266
|
+
* import { nodeWalkerSyncOps } from "@effected/walker/node-sync";
|
|
267
|
+
*
|
|
268
|
+
* const dirs = Walker.ascendToMarkerSync("/repo/packages/app", [".git"], nodeWalkerSyncOps);
|
|
269
|
+
* ```
|
|
270
|
+
*/
|
|
271
|
+
static ascendToMarkerSync(start, markers, options) {
|
|
272
|
+
return runUpwardSync(ascendToMarkerMachine("Walker.ascendToMarkerSync", start, markers, options, options.path), options.fileSystem);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* {@link Walker.findUpward}, synchronously, over a caller-supplied port: the
|
|
276
|
+
* first existing candidate, directory-major, or `Option.none()`.
|
|
277
|
+
*
|
|
278
|
+
* @remarks
|
|
279
|
+
* The same scan from the same code. A candidate exists when the port's
|
|
280
|
+
* `stat` (links followed) answers for it; a `stat` that throws is "not
|
|
281
|
+
* there". A throwing `candidatesFor` throws before any probe.
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* ```ts
|
|
285
|
+
* import { Walker } from "@effected/walker";
|
|
286
|
+
* import { nodeWalkerSyncOps } from "@effected/walker/node-sync";
|
|
287
|
+
*
|
|
288
|
+
* const dirs = Walker.ascendSync("/repo/packages/app", { path: nodeWalkerSyncOps.path });
|
|
289
|
+
* const workspace = Walker.findUpwardSync(dirs, (dir) => [`${dir}/pnpm-workspace.yaml`], nodeWalkerSyncOps);
|
|
290
|
+
* ```
|
|
291
|
+
*/
|
|
292
|
+
static findUpwardSync(dirs, candidatesFor, ops) {
|
|
293
|
+
return Option.fromUndefinedOr(runUpwardSync(findUpwardMachine(dirs, candidatesFor), ops.fileSystem));
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* {@link Walker.findUpwardAll}, synchronously, over a caller-supplied port.
|
|
297
|
+
*
|
|
298
|
+
* @remarks
|
|
299
|
+
* The same scan from the same code: every existing candidate of the
|
|
300
|
+
* nearest directory with any, in candidate order, and nothing above it
|
|
301
|
+
* probed. A candidate exists when the port's `stat` answers for it.
|
|
302
|
+
*
|
|
303
|
+
* @example
|
|
304
|
+
* ```ts
|
|
305
|
+
* import { Walker } from "@effected/walker";
|
|
306
|
+
* import { nodeWalkerSyncOps } from "@effected/walker/node-sync";
|
|
307
|
+
*
|
|
308
|
+
* const dirs = Walker.ascendSync("/repo/packages/app", { path: nodeWalkerSyncOps.path });
|
|
309
|
+
* const hits = Walker.findUpwardAllSync(dirs, (dir) => [`${dir}/a.config.ts`, `${dir}/a.config.js`], nodeWalkerSyncOps);
|
|
310
|
+
* ```
|
|
311
|
+
*/
|
|
312
|
+
static findUpwardAllSync(dirs, candidatesFor, ops) {
|
|
313
|
+
return Option.fromUndefinedOr(runUpwardSync(findUpwardAllMachine(dirs, candidatesFor), ops.fileSystem));
|
|
314
|
+
}
|
|
160
315
|
};
|
|
161
316
|
|
|
162
317
|
export { Walker };
|