@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 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 resolves links separately.
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, (a, b) => path.join(a, b));
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, (a, b) => path.join(a, b));
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 { Effect, FileSystem, Option, Path } from "effect";
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 maxDepth = options?.maxDepth ?? 256;
65
- if (!Number.isInteger(maxDepth) || maxDepth < 1) return yield* Effect.die(/* @__PURE__ */ new Error(`Walker.ascend: maxDepth must be a positive integer, received ${maxDepth}`));
66
- if (options?.stopAt !== void 0 && !path.isAbsolute(options.stopAt)) return yield* Effect.die(/* @__PURE__ */ new Error(`Walker.ascend: stopAt must be an absolute path, received ${JSON.stringify(options.stopAt)} (ascending from ${JSON.stringify(start)})`));
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
- if (Option.isNone(ceiling)) return yield* Walker.ascend(start, options);
110
- return yield* ascendToPhysical(start, ceiling.value, options);
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.gen(function* () {
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 };