@effected/walker 0.21.0 → 0.23.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/index.d.ts CHANGED
@@ -20,10 +20,16 @@ import { GlobPattern, GlobPatternError, GlobPatternOptions } from "@effected/glo
20
20
  * reserved device name (`CON`, `PRN`, `AUX`, `NUL`, `COM1`–`COM9`,
21
21
  * `LPT1`–`LPT9`), in any case and with or without an extension
22
22
  * (`nul.txt`).
23
+ * - `outside` — `confineReal` only: the candidate's real path, links
24
+ * resolved, lies outside the base's real path.
25
+ * - `unresolvable` — `confineReal` only: resolving the candidate or the base
26
+ * failed for a reason other than absence (a permission error, a link loop,
27
+ * a file used as a directory, a dangling link), so containment cannot be
28
+ * verified. The failure rides in the error's `cause`.
23
29
  *
24
30
  * @public
25
31
  */
26
- export declare const PathEscapeReason: Schema.Literals<readonly ["nulByte", "absolute", "traversal", "empty", "reservedName"]>;
32
+ export declare const PathEscapeReason: Schema.Literals<readonly ["nulByte", "absolute", "traversal", "empty", "reservedName", "outside", "unresolvable"]>;
27
33
  /**
28
34
  * The decoded type of {@link (PathEscapeReason:variable)}.
29
35
  *
@@ -34,7 +40,9 @@ declare const PathEscapeError_base: Schema.Class<PathEscapeError, Schema.TaggedS
34
40
  /** The candidate path, exactly as the caller passed it. */
35
41
  readonly candidate: Schema.String;
36
42
  /** Why the candidate was refused. */
37
- readonly reason: Schema.Literals<readonly ["nulByte", "absolute", "traversal", "empty", "reservedName"]>;
43
+ readonly reason: Schema.Literals<readonly ["nulByte", "absolute", "traversal", "empty", "reservedName", "outside", "unresolvable"]>;
44
+ /** The resolution failure, present when `reason` is `"unresolvable"`. */
45
+ readonly cause: Schema.optionalKey<Schema.Defect>;
38
46
  }>, import("effect/Cause").YieldableError>;
39
47
  /**
40
48
  * An untrusted relative path would leave, or does not name anything under,
@@ -82,7 +90,7 @@ export declare class PathEscapeError extends PathEscapeError_base {
82
90
  * base yields the normalised relative path alone. The check is lexical, not
83
91
  * physical, and symlinks are out of scope: a symlink already inside the
84
92
  * base can still point out of it, so a caller extracting into a directory
85
- * it does not own resolves links separately.
93
+ * it does not own checks the result with `confineReal` as well.
86
94
  *
87
95
  * {@link confineRelative} is defined in terms of this function and adds
88
96
  * only the tracing span.
@@ -139,8 +147,49 @@ export declare const confineRelative: (base: string, candidate: string) => Effec
139
147
  * @public
140
148
  */
141
149
  interface DescendOptions {
142
- /** Absolute directory the pattern is resolved against. Required — walker never reads `process.cwd()`. */
150
+ /**
151
+ * Absolute directory the pattern is resolved against. Required — walker
152
+ * never reads `process.cwd()`.
153
+ *
154
+ * @remarks
155
+ * **A missing `cwd` is an empty result, never a failure**: when `cwd` does
156
+ * not exist, is a file, or is a dangling link, every pattern — literal or
157
+ * magic — answers zero matches (`[]`, or `{ matches: [], unreadable: [] }`
158
+ * under `"record"`), in the `Effect` and the sync forms alike. Zero
159
+ * matches is a normal glob answer, so a caller that must tell "nothing
160
+ * matched" from "there was nothing to look in" checks `cwd` itself first.
161
+ * The same holds for a pattern's literal base below `cwd`. Only an
162
+ * unreadable directory the walk does reach fails, under `"fail"`.
163
+ *
164
+ * **A relative `cwd` — the empty string included — is a defect**: the
165
+ * `Effect` forms die and the sync forms throw. A relative directory would
166
+ * resolve against the process working directory, so the same call would
167
+ * walk different trees from a hook, a CLI and a test runner, and on win32
168
+ * an empty `cwd` joined with a pattern spelled `C:/x` is the drive path
169
+ * `C:\x`. Absoluteness is judged by the injected `Path` (or the sync port's
170
+ * `isAbsolute`), so win32 accepts `C:\repo` and `\\server\share`; a
171
+ * win32 `\repo` names the root of whichever drive the process is on, so
172
+ * it dies as a relative `cwd` does.
173
+ */
143
174
  readonly cwd: string;
175
+ /**
176
+ * Which entry kinds match. `"file"` (the default) answers files only, as
177
+ * `descend` always has; `"directory"` answers directories only; `"any"`
178
+ * answers both.
179
+ *
180
+ * @remarks
181
+ * The kind is the RESOLVED one — links followed, as for files — so a link
182
+ * to a directory matches as a directory, even where `followSymlinks: false`
183
+ * keeps the walk from entering it, and even when its target lies outside
184
+ * `cwd`. Check such an entry with `confineReal` before acting on it. A
185
+ * directory matches as an entry of its parent: `prune`, a boundary and an
186
+ * unentered link withhold what lies below a directory, never the
187
+ * directory itself, so a globstar pattern for `node_modules` finds every
188
+ * `node_modules` under the default `prune` without reading inside one. A directory is tested bare
189
+ * and with a trailing `/`, so the directory-only glob form `dist/`
190
+ * matches, answered as spelled. `cwd` itself is never a match.
191
+ */
192
+ readonly match?: "file" | "directory" | "any";
144
193
  /** Hard cap on directory depth below the pattern's literal prefix. Defaults to 256. */
145
194
  readonly maxDepth?: number;
146
195
  /** Directory names never descended into. Defaults to `["node_modules", ".git"]`; a custom list replaces the default. */
@@ -279,8 +328,8 @@ export declare class DescendError extends DescendError_base {
279
328
  get message(): string;
280
329
  }
281
330
  /**
282
- * One matched file with the stat the walk already read for it — `descendEntries`'
283
- * element. Costs no syscall beyond `descend`'s: the walk stats every entry to
331
+ * One matched entry — a file, or under `match` a directory — with the stat
332
+ * the walk already read for it: `descendEntries`' element. Costs no syscall beyond `descend`'s: the walk stats every entry to
284
333
  * classify it, and this keeps the answer instead of discarding it.
285
334
  *
286
335
  * The type parameter is the stat's type, so the sync walk reuses this shape
@@ -289,7 +338,7 @@ export declare class DescendError extends DescendError_base {
289
338
  * @public
290
339
  */
291
340
  interface DescendEntry<I = FileSystem.File.Info> {
292
- /** The file's path relative to `cwd`, POSIX separators. */
341
+ /** The entry's path relative to `cwd`, POSIX separators. */
293
342
  readonly path: string;
294
343
  /** `FileSystem.stat` of the file — links followed, so a symlinked file reports its target. */
295
344
  readonly info: I;
@@ -322,7 +371,8 @@ interface DescendEntriesResult<I = FileSystem.File.Info, C = PlatformError.Platf
322
371
  export declare function descend(pattern: GlobPattern, options: DescendRecordOptions): Effect.Effect<DescendResult, DescendError, FileSystem.FileSystem | Path.Path>;
323
372
  /**
324
373
  * Expand a compiled glob pattern against the filesystem, returning matching
325
- * FILE paths relative to `cwd` (POSIX separators), sorted by relative path.
374
+ * FILE paths relative to `cwd` (POSIX separators), sorted by relative path —
375
+ * or, under `match: "directory"` or `"any"`, directory paths too.
326
376
  *
327
377
  * @remarks
328
378
  * A path is answered in the spelling the pattern matched it under: `./src/*.ts`
@@ -334,8 +384,9 @@ export declare function descend(pattern: GlobPattern, options: DescendRecordOpti
334
384
  * path is zero matches, not an error. A magic pattern walks each of its
335
385
  * `GlobPattern.enumerationRoots`, reading each directory once however many
336
386
  * alternatives spell it; a NEGATED pattern can match paths outside those
337
- * roots, so it walks from `cwd` itself. A missing
338
- * base directory is likewise an empty result, because zero matches is a
387
+ * roots, so it walks from `cwd` itself. A missing `cwd` or
388
+ * base directory (absent, a file, a dangling link) is likewise an empty
389
+ * result — see `DescendOptions.cwd` — because zero matches is a
339
390
  * normal glob answer — as is any pattern that lexically climbs above `cwd`
340
391
  * via `..` segments (walked paths never contain `..`). "Never reads outside
341
392
  * its documented root" holds lexically always, and physically only while
@@ -345,7 +396,8 @@ export declare function descend(pattern: GlobPattern, options: DescendRecordOpti
345
396
  * (under the default `onUnreadable: "fail"`) or a walk past `maxDepth` fails,
346
397
  * typed as {@link DescendError}.
347
398
  *
348
- * Only files match. A symlink counts when it stat-resolves to a file
399
+ * Only files match by default (`match` widens that to directories). A symlink
400
+ * counts when it stat-resolves to a file
349
401
  * (`FileSystem.stat` follows links, as node's does); a symlinked directory is
350
402
  * never descended into by default (cycle safety — detected by an `lstat`
351
403
  * probe), unless `followSymlinks: true` asks for it, which follows links under
@@ -462,17 +514,30 @@ interface WalkerSyncFileSystem {
462
514
  readonly realPath: (path: string) => string;
463
515
  }
464
516
  /**
465
- * The synchronous path operation the sync walk needs — a structural subset of
466
- * `node:path`, so the module itself (or its `win32` / `posix` variant)
517
+ * The synchronous path operations the sync forms need — a structural subset
518
+ * of `node:path`, so the module itself (or its `win32` / `posix` variant)
467
519
  * satisfies it. It shapes only the absolute paths handed to the
468
520
  * {@link WalkerSyncFileSystem}; matching is POSIX-relative by the glob
469
521
  * contract.
470
522
  *
523
+ * @remarks
524
+ * The members mirror core's `Path` service, so a sync form judges
525
+ * absoluteness, ascends and compares ceilings exactly as its `Effect` twin
526
+ * does under the `Path` layer of the same platform.
527
+ *
471
528
  * @public
472
529
  */
473
530
  interface WalkerSyncPath {
531
+ /** The implementation's separator (`path.sep`). */
532
+ readonly sep: string;
474
533
  /** Join segments with the implementation's separator (like `path.join`). */
475
534
  readonly join: (...segments: ReadonlyArray<string>) => string;
535
+ /** The parent directory of a path (like `path.dirname`); a fixpoint at the root. */
536
+ readonly dirname: (path: string) => string;
537
+ /** Resolve segments to a normalized path (like `path.resolve`). */
538
+ readonly resolve: (...segments: ReadonlyArray<string>) => string;
539
+ /** Whether a path is absolute under this implementation (like `path.isAbsolute`). */
540
+ readonly isAbsolute: (path: string) => boolean;
476
541
  }
477
542
  /**
478
543
  * The platform half of every sync walk.
@@ -544,6 +609,170 @@ export declare function descendEntriesSync(pattern: GlobPattern, options: Descen
544
609
  * @public
545
610
  */
546
611
  export declare function descendEntriesSync(pattern: GlobPattern, options: DescendSyncOptions): Result.Result<ReadonlyArray<DescendEntry<WalkerSyncStats>>, DescendError>;
612
+ /**
613
+ * Confine a path to a base directory PHYSICALLY: resolve links in both, and
614
+ * succeed with the candidate's real path only when it lies strictly inside
615
+ * the base's real path.
616
+ *
617
+ * @remarks
618
+ * Reach for it where `confineRelative` stops: a link inside the base that
619
+ * points out of it (a symlinked `dist`, a planted `node_modules` entry), an
620
+ * absolute path an agent or a tool handed over, a `descend` match — a
621
+ * matched link is answered even when its target lies outside `cwd`.
622
+ *
623
+ * The candidate may be absolute (checked as it is) or relative (taken
624
+ * against `base`). It is resolved the way the operating system resolves it:
625
+ * segment by segment, so a `..` after a link climbs out of the link's
626
+ * TARGET — `base/link/../x` is outside when `link` points outside — rather
627
+ * than being collapsed lexically first, as `path.resolve` and node's JS
628
+ * `fs.realpath` would. A path, or the tail of one, that does not exist yet
629
+ * is resolved lexically from its real existing ancestor, so a write target
630
+ * can be checked before it is created — but only once absence is proved: a
631
+ * segment whose real path is not found is checked again without following
632
+ * links, because a dangling link is "not found" too, and a write through one
633
+ * creates its target wherever it points. The base is resolved the same way.
634
+ *
635
+ * Refused, typed as {@link PathEscapeError}: a NUL character (`nulByte`);
636
+ * the empty string or any path whose real location IS the base (`empty`,
637
+ * the same ruling `confineRelative` makes — a caller confining a path names
638
+ * an entry under the base); on win32 a drive-relative `C:x` or a driveless
639
+ * rooted `\x`, which names the current drive (`absolute`), or a segment
640
+ * win32 reads as something else, such as a device name or `.. `
641
+ * (`reservedName`); a real path outside the base's (`outside`); and a
642
+ * resolution that fails for a reason other than absence, such as a
643
+ * permission error, a link loop, or a dangling link — a link whose target
644
+ * does not exist, refused even when that target would lie inside the base
645
+ * (`unresolvable`, with the failure in `cause`) — containment that cannot be
646
+ * verified is refused, never assumed.
647
+ *
648
+ * A relative `base` — the empty string included — is a **defect**: it would
649
+ * be resolved against the process working directory, the silent wrong-root
650
+ * walk `ascend` refuses for a relative `stopAt`. Absoluteness is judged by
651
+ * the injected `Path`, so win32 accepts `C:\repo` and `\\server\share`;
652
+ * a win32 `\repo`, rooted on whichever drive the process is on, is a defect
653
+ * too.
654
+ *
655
+ * Use the returned path, not the candidate: it is the location that was
656
+ * checked. Comparison is exact on the real paths, so on a case-insensitive
657
+ * filesystem a base and a candidate spelled in different case may be refused
658
+ * — the check fails closed. As with any check-then-use on a live filesystem,
659
+ * a link swapped in between the check and the use is not covered.
660
+ *
661
+ * @example
662
+ * ```ts
663
+ * import { confineReal } from "@effected/walker";
664
+ * import { Effect } from "effect";
665
+ *
666
+ * const target = confineReal("/repo", "packages/app/dist").pipe(
667
+ * Effect.catchTag("PathEscapeError", (error) => Effect.die(error)),
668
+ * );
669
+ * // Requires `FileSystem` and `Path` from the platform layer.
670
+ * ```
671
+ *
672
+ * @param base - The absolute directory the candidate must stay under.
673
+ * @param candidate - The path to check: absolute, or relative to `base`.
674
+ * @returns An `Effect` succeeding with the candidate's real path, or failing
675
+ * with {@link PathEscapeError}.
676
+ *
677
+ * @public
678
+ */
679
+ export declare const confineReal: (base: string, candidate: string) => Effect.Effect<string, PathEscapeError, FileSystem.FileSystem | Path.Path>;
680
+ /**
681
+ * {@link confineReal}, synchronously, over a caller-supplied port.
682
+ *
683
+ * @remarks
684
+ * The same resolution from the same code, through the port's `realPath`; a
685
+ * `realPath` throwing `code: "ENOENT"` is absence only when the port's
686
+ * `lstat` throws `ENOENT` too (otherwise a dangling link is there); anything
687
+ * else is `unresolvable`, with the thrown value as the `cause`. A relative `base`
688
+ * throws. For a caller with no Effect runtime; inside Effect, use
689
+ * `confineReal`.
690
+ *
691
+ * @example
692
+ * ```ts
693
+ * import { confineRealSync } from "@effected/walker";
694
+ * import { nodeWalkerSyncOps } from "@effected/walker/node-sync";
695
+ * import { Result } from "effect";
696
+ *
697
+ * const checked = confineRealSync("/repo", "/repo/packages/app/dist", nodeWalkerSyncOps);
698
+ * if (Result.isFailure(checked)) console.error(checked.failure.message);
699
+ * ```
700
+ *
701
+ * @param base - The absolute directory the candidate must stay under.
702
+ * @param candidate - The path to check: absolute, or relative to `base`.
703
+ * @param ops - The platform: spread or pass `nodeWalkerSyncOps`.
704
+ * @returns A `Result` succeeding with the candidate's real path, or failing
705
+ * with {@link PathEscapeError}.
706
+ *
707
+ * @public
708
+ */
709
+ export declare const confineRealSync: (base: string, candidate: string, ops: WalkerSyncOps) => Result.Result<string, PathEscapeError>;
710
+ /**
711
+ * Options for {@link relativeWithin}.
712
+ *
713
+ * @public
714
+ */
715
+ interface RelativeWithinOptions {
716
+ /**
717
+ * What a path that does not exist yet means. `"ancestor"` (the default)
718
+ * resolves the missing tail from the deepest existing ancestor, so a
719
+ * destination can be placed before it is created; `"refuse"` answers `None`
720
+ * for any path that does not exist.
721
+ */
722
+ readonly missing?: "ancestor" | "refuse";
723
+ }
724
+ /**
725
+ * The posix, base-relative path of an absolute path that lies inside a base
726
+ * directory PHYSICALLY, or `None` when it does not.
727
+ *
728
+ * @remarks
729
+ * The question `confineReal` does not answer: where, under `base`, does this
730
+ * absolute path sit? It resolves links the way `confineReal` does, and a path
731
+ * or tail that does not exist yet is resolved lexically from its deepest
732
+ * existing ancestor, so a destination can be placed before it is created.
733
+ * The base itself answers `""`. `None` covers every refusal: a relative or
734
+ * NUL-bearing candidate, a real location outside the base, and a resolution
735
+ * that cannot be verified (a permission error, a loop, a dangling link) —
736
+ * containment that cannot be proved is never assumed. A relative `base` is a
737
+ * **defect**, as it is for `confineReal`.
738
+ *
739
+ * @example
740
+ * ```ts
741
+ * import { relativeWithin } from "@effected/walker";
742
+ * import { Effect } from "effect";
743
+ *
744
+ * const inside = relativeWithin("/repo", "/repo/docs/new.md");
745
+ * // Requires `FileSystem` and `Path`; succeeds with Option.some("docs/new.md").
746
+ * ```
747
+ *
748
+ * @param base - The absolute directory the path must stay inside.
749
+ * @param absolute - The absolute path to place under `base`.
750
+ * @param options - See {@link RelativeWithinOptions}.
751
+ * @returns An `Effect` succeeding with the base-relative posix path, or `None`.
752
+ *
753
+ * @public
754
+ */
755
+ export declare const relativeWithin: (base: string, absolute: string, options?: RelativeWithinOptions) => Effect.Effect<Option.Option<string>, never, FileSystem.FileSystem | Path.Path>;
756
+ /**
757
+ * {@link relativeWithin}, synchronously, over a caller-supplied port.
758
+ *
759
+ * @example
760
+ * ```ts
761
+ * import { relativeWithinSync } from "@effected/walker";
762
+ * import { nodeWalkerSyncOps } from "@effected/walker/node-sync";
763
+ *
764
+ * const inside = relativeWithinSync("/repo", "/repo/docs/new.md", nodeWalkerSyncOps);
765
+ * ```
766
+ *
767
+ * @param base - The absolute directory the path must stay inside.
768
+ * @param absolute - The absolute path to place under `base`.
769
+ * @param ops - The platform: spread or pass `nodeWalkerSyncOps`.
770
+ * @param options - See {@link RelativeWithinOptions}.
771
+ * @returns The base-relative posix path, or `None`.
772
+ *
773
+ * @public
774
+ */
775
+ export declare const relativeWithinSync: (base: string, absolute: string, ops: WalkerSyncOps, options?: RelativeWithinOptions) => Option.Option<string>;
547
776
  /**
548
777
  * Options for {@link compileAndExpand}: every {@link DescendOptions} field,
549
778
  * plus the glob options the pattern compiles under.
@@ -684,7 +913,8 @@ interface AscendOptions {
684
913
  *
685
914
  * Only the CEILING is constrained — a relative `start` is fine and ascends
686
915
  * to the relative root. Absoluteness is judged by the injected `Path`
687
- * service, so the win32 layer accepts `C:\repo` and posix does not.
916
+ * service, so the win32 layer accepts `C:\repo` and posix does not; a
917
+ * win32 `\repo`, rooted on whichever drive the process is on, dies too.
688
918
  *
689
919
  * Normalization governs the comparison only: the chain `ascend` returns is
690
920
  * still the lexical one derived from `start`, unrewritten. A ceiling naming
@@ -700,6 +930,37 @@ interface AscendOptions {
700
930
  /** Hard cap on chain length. Defaults to 256. */
701
931
  readonly maxDepth?: number;
702
932
  }
933
+ /**
934
+ * Options for {@link Walker.ascendSync}: every {@link AscendOptions} field,
935
+ * plus the path implementation the chain is computed with.
936
+ *
937
+ * @public
938
+ */
939
+ interface AscendSyncOptions extends AscendOptions {
940
+ /** Path arithmetic — `node:path` satisfies it (`nodeWalkerPath` from `@effected/walker/node-sync`). */
941
+ readonly path: WalkerSyncPath;
942
+ }
943
+ /**
944
+ * Options for the synchronous upward forms that touch the filesystem —
945
+ * {@link Walker.ascendWithinSync} and {@link Walker.ascendToMarkerSync}: the
946
+ * depth cap, plus the platform (spread `nodeWalkerSyncOps` from
947
+ * `@effected/walker/node-sync`).
948
+ *
949
+ * @public
950
+ */
951
+ interface AscendWithinSyncOptions extends Pick<AscendOptions, "maxDepth">, WalkerSyncOps {}
952
+ /**
953
+ * {@link Walker.findUpwardAll}'s answer: the nearest directory holding any
954
+ * candidate, and every candidate that exists there.
955
+ *
956
+ * @public
957
+ */
958
+ interface UpwardMatches {
959
+ /** The directory whose candidates produced the hits. */
960
+ readonly dir: string;
961
+ /** Every existing candidate of `dir`, in the order `candidatesFor` gave them. Never empty. */
962
+ readonly found: readonly [string, ...Array<string>];
963
+ }
703
964
  /**
704
965
  * Upward path traversal primitives: the directory chain from a start path to the
705
966
  * root, and first-match scans over it.
@@ -804,6 +1065,171 @@ export declare class Walker {
804
1065
  * `pnpm-workspace.yaml`), and its failures are absorbed per directory.
805
1066
  */
806
1067
  static findRoot<E, R>(dirs: ReadonlyArray<string>, isRoot: (dir: string) => Effect.Effect<boolean, E, R>): Effect.Effect<Option.Option<string>, never, R>;
1068
+ /**
1069
+ * {@link Walker.ascend} stopped at the nearest directory that holds any of
1070
+ * `markers`, inclusive: the chain from `start` up to and including the
1071
+ * first directory with a `.git` (or whatever the markers name) entry.
1072
+ *
1073
+ * @remarks
1074
+ * A marker is an entry NAME probed with `FileSystem.exists` in each
1075
+ * directory, nearest first, so any entry kind counts — a `.git` directory,
1076
+ * or the `.git` FILE of a worktree or submodule — and a link counts when it
1077
+ * resolves. The probes stop at the hit: nothing above the directory that
1078
+ * holds a marker is ever touched. With no marker anywhere the chain runs to
1079
+ * the filesystem root, exactly as `ascend(start)` gives it.
1080
+ *
1081
+ * A failed probe is absorbed as "no marker here", as
1082
+ * {@link Walker.firstMatch} absorbs one, so one unreadable directory never
1083
+ * hides a marker above it. An empty `markers` list, or an empty marker
1084
+ * name, is a **defect**: the first would silently ask for the unbounded
1085
+ * walk and the second would match every directory, and both are
1086
+ * statically wrong at the call site. An invalid `maxDepth` dies exactly as
1087
+ * `ascend`'s does.
1088
+ *
1089
+ * There is no `stopAt` here: a marker stop and a lexical ceiling answer the
1090
+ * same question two ways. To bound a marker walk by a ceiling as well,
1091
+ * take the shorter of the two chains.
1092
+ *
1093
+ * @example
1094
+ * ```ts
1095
+ * import { Walker } from "@effected/walker";
1096
+ * import { Effect } from "effect";
1097
+ *
1098
+ * const program = Effect.gen(function* () {
1099
+ * // With `.git` in /repo: ["/repo/packages/app", "/repo/packages", "/repo"]
1100
+ * return yield* Walker.ascendToMarker("/repo/packages/app", [".git"]);
1101
+ * });
1102
+ * // Requires `FileSystem` and `Path` from the platform layer.
1103
+ * ```
1104
+ */
1105
+ static ascendToMarker(start: string, markers: ReadonlyArray<string>, options?: Pick<AscendOptions, "maxDepth">): Effect.Effect<ReadonlyArray<string>, never, FileSystem.FileSystem | Path.Path>;
1106
+ /**
1107
+ * Every existing candidate of the NEAREST directory in `dirs` that has any,
1108
+ * or `Option.none()` — {@link Walker.findUpward} for a caller that must see
1109
+ * a second hit (an ambiguity) rather than have the first one chosen.
1110
+ *
1111
+ * @remarks
1112
+ * Directories are scanned in order, and every candidate of a directory is
1113
+ * probed before the scan ascends; the scan stops after the first directory
1114
+ * with a hit, so nothing above it is probed. `found` keeps the order
1115
+ * `candidatesFor` produced and is never empty. A failed probe is absorbed
1116
+ * as "not there", as in `findUpward`, so not-found and cannot-look are
1117
+ * indistinguishable here too.
1118
+ *
1119
+ * @example
1120
+ * ```ts
1121
+ * import { Walker } from "@effected/walker";
1122
+ * import { Effect, Option } from "effect";
1123
+ *
1124
+ * const program = Effect.gen(function* () {
1125
+ * const dirs = yield* Walker.ascend("/repo/packages/app");
1126
+ * const hits = yield* Walker.findUpwardAll(dirs, (dir) => [`${dir}/tool.config.ts`, `${dir}/tool.config.js`]);
1127
+ * // Two config files in one directory is the caller's ambiguity to report.
1128
+ * return Option.map(hits, ({ found }) => found.length > 1);
1129
+ * });
1130
+ * ```
1131
+ */
1132
+ static findUpwardAll(dirs: ReadonlyArray<string>, candidatesFor: (dir: string) => ReadonlyArray<string>): Effect.Effect<Option.Option<UpwardMatches>, never, FileSystem.FileSystem>;
1133
+ /**
1134
+ * {@link Walker.ascend}, synchronously, over a caller-supplied path
1135
+ * implementation.
1136
+ *
1137
+ * @remarks
1138
+ * The same chain from the same code: lexical, nearest first, `stopAt`
1139
+ * inclusive and compared in resolved form. A relative `stopAt` or an
1140
+ * invalid `maxDepth` is a defect, which here THROWS, since there is no
1141
+ * error channel to die through. For a caller with no Effect runtime (a CLI
1142
+ * helper that runs before one, a bundler-plugin hook); inside Effect, use
1143
+ * `ascend`.
1144
+ *
1145
+ * @example
1146
+ * ```ts
1147
+ * import { Walker } from "@effected/walker";
1148
+ * import { nodeWalkerPath } from "@effected/walker/node-sync";
1149
+ *
1150
+ * const dirs = Walker.ascendSync("/repo/packages/app", { stopAt: "/repo", path: nodeWalkerPath });
1151
+ * // => ["/repo/packages/app", "/repo/packages", "/repo"]
1152
+ * ```
1153
+ */
1154
+ static ascendSync(start: string, options: AscendSyncOptions): ReadonlyArray<string>;
1155
+ /**
1156
+ * {@link Walker.ascendWithin}, synchronously, over a caller-supplied port:
1157
+ * the lexical chain stopped at the nearest directory whose `realPath` is
1158
+ * the ceiling's.
1159
+ *
1160
+ * @remarks
1161
+ * The same walk from the same code as `ascendWithin`: `Option.none()` is
1162
+ * exactly `ascendSync(start)`, a ceiling the chain already spells costs no
1163
+ * `realPath`, a `realPath` that throws is absorbed as "not the ceiling",
1164
+ * and an unresolvable ceiling leaves the lexical chain standing. A
1165
+ * relative ceiling or an invalid `maxDepth` throws.
1166
+ *
1167
+ * @example
1168
+ * ```ts
1169
+ * import { Walker } from "@effected/walker";
1170
+ * import { nodeWalkerSyncOps } from "@effected/walker/node-sync";
1171
+ * import { Option } from "effect";
1172
+ *
1173
+ * const dirs = Walker.ascendWithinSync("/repo/packages/app", Option.some("/repo"), nodeWalkerSyncOps);
1174
+ * ```
1175
+ */
1176
+ static ascendWithinSync(start: string, ceiling: Option.Option<string>, options: AscendWithinSyncOptions): ReadonlyArray<string>;
1177
+ /**
1178
+ * {@link Walker.ascendToMarker}, synchronously, over a caller-supplied port.
1179
+ *
1180
+ * @remarks
1181
+ * The same walk from the same code. A marker exists when the port's `stat`
1182
+ * (links followed) answers for it; a `stat` that throws, for any reason,
1183
+ * is "no marker here", as the `Effect` form absorbs a failed probe. An
1184
+ * empty `markers` list, an empty marker name or an invalid `maxDepth`
1185
+ * throws.
1186
+ *
1187
+ * @example
1188
+ * ```ts
1189
+ * import { Walker } from "@effected/walker";
1190
+ * import { nodeWalkerSyncOps } from "@effected/walker/node-sync";
1191
+ *
1192
+ * const dirs = Walker.ascendToMarkerSync("/repo/packages/app", [".git"], nodeWalkerSyncOps);
1193
+ * ```
1194
+ */
1195
+ static ascendToMarkerSync(start: string, markers: ReadonlyArray<string>, options: AscendWithinSyncOptions): ReadonlyArray<string>;
1196
+ /**
1197
+ * {@link Walker.findUpward}, synchronously, over a caller-supplied port: the
1198
+ * first existing candidate, directory-major, or `Option.none()`.
1199
+ *
1200
+ * @remarks
1201
+ * The same scan from the same code. A candidate exists when the port's
1202
+ * `stat` (links followed) answers for it; a `stat` that throws is "not
1203
+ * there". A throwing `candidatesFor` throws before any probe.
1204
+ *
1205
+ * @example
1206
+ * ```ts
1207
+ * import { Walker } from "@effected/walker";
1208
+ * import { nodeWalkerSyncOps } from "@effected/walker/node-sync";
1209
+ *
1210
+ * const dirs = Walker.ascendSync("/repo/packages/app", { path: nodeWalkerSyncOps.path });
1211
+ * const workspace = Walker.findUpwardSync(dirs, (dir) => [`${dir}/pnpm-workspace.yaml`], nodeWalkerSyncOps);
1212
+ * ```
1213
+ */
1214
+ static findUpwardSync(dirs: ReadonlyArray<string>, candidatesFor: (dir: string) => ReadonlyArray<string>, ops: WalkerSyncOps): Option.Option<string>;
1215
+ /**
1216
+ * {@link Walker.findUpwardAll}, synchronously, over a caller-supplied port.
1217
+ *
1218
+ * @remarks
1219
+ * The same scan from the same code: every existing candidate of the
1220
+ * nearest directory with any, in candidate order, and nothing above it
1221
+ * probed. A candidate exists when the port's `stat` answers for it.
1222
+ *
1223
+ * @example
1224
+ * ```ts
1225
+ * import { Walker } from "@effected/walker";
1226
+ * import { nodeWalkerSyncOps } from "@effected/walker/node-sync";
1227
+ *
1228
+ * const dirs = Walker.ascendSync("/repo/packages/app", { path: nodeWalkerSyncOps.path });
1229
+ * const hits = Walker.findUpwardAllSync(dirs, (dir) => [`${dir}/a.config.ts`, `${dir}/a.config.js`], nodeWalkerSyncOps);
1230
+ * ```
1231
+ */
1232
+ static findUpwardAllSync(dirs: ReadonlyArray<string>, candidatesFor: (dir: string) => ReadonlyArray<string>, ops: WalkerSyncOps): Option.Option<UpwardMatches>;
807
1233
  }
808
- export type { AscendOptions, CompileAndExpandOptions, DescendEntriesResult, DescendEntry, DescendOptions, DescendRecordOptions, DescendResult, DescendSyncOptions, DescendSyncRecordOptions, UnreadableDirectory, WalkerSyncFileSystem, WalkerSyncOps, WalkerSyncPath, WalkerSyncStats };
1234
+ export type { AscendOptions, AscendSyncOptions, AscendWithinSyncOptions, CompileAndExpandOptions, DescendEntriesResult, DescendEntry, DescendOptions, DescendRecordOptions, DescendResult, DescendSyncOptions, DescendSyncRecordOptions, RelativeWithinOptions, UnreadableDirectory, UpwardMatches, WalkerSyncFileSystem, WalkerSyncOps, WalkerSyncPath, WalkerSyncStats };
809
1235
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { PathEscapeError, PathEscapeReason, confineRelative, confineRelativeResult } from "./Confine.js";
2
+ import { confineReal, confineRealSync, relativeWithin, relativeWithinSync } from "./ConfineReal.js";
2
3
  import { DescendError, descend, descendEntries } from "./Descend.js";
3
4
  import { descendEntriesSync, descendSync } from "./DescendSync.js";
4
5
  import { GlobExpansionError, compileAndExpand } from "./Expand.js";
5
6
  import { Walker } from "./Walker.js";
6
7
 
7
- export { DescendError, GlobExpansionError, PathEscapeError, PathEscapeReason, Walker, compileAndExpand, confineRelative, confineRelativeResult, descend, descendEntries, descendEntriesSync, descendSync };
8
+ export { DescendError, GlobExpansionError, PathEscapeError, PathEscapeReason, Walker, compileAndExpand, confineReal, confineRealSync, confineRelative, confineRelativeResult, descend, descendEntries, descendEntriesSync, descendSync, relativeWithin, relativeWithinSync };
@@ -0,0 +1,19 @@
1
+ // src/internal/absolute.ts
2
+ /**
3
+ * A win32 path rooted on the CURRENT drive: one leading separator, not two
4
+ * (`\\server\share` and `\\?\C:\x` are UNC and device paths, which name their
5
+ * root). `path.win32.isAbsolute` calls `\repo` absolute, yet which drive it
6
+ * names depends on the process working directory — exactly like the
7
+ * drive-relative `C:x`.
8
+ */
9
+ const CURRENT_DRIVE_ROOT = /^[\\/](?![\\/])/;
10
+ /** Whether `path` is rooted on the current drive under win32 path rules. */
11
+ const isCurrentDriveRooted = (path, sep) => sep === "\\" && CURRENT_DRIVE_ROOT.test(path);
12
+ /**
13
+ * Whether `path` names one location whatever the process working directory:
14
+ * absolute to the injected implementation, and on win32 not rooted on the
15
+ * current drive.
16
+ */
17
+ const isPinnedAbsolute = (path, ops) => ops.isAbsolute(path) && !isCurrentDriveRooted(path, ops.sep);
18
+
19
+ export { isCurrentDriveRooted, isPinnedAbsolute };