@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/Confine.js +16 -4
- package/ConfineReal.js +183 -0
- package/Descend.js +5 -1
- package/DescendSync.js +5 -1
- package/Walker.js +193 -38
- package/index.d.ts +441 -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 +252 -0
- package/node-sync.d.ts +15 -2
- package/node-sync.js +7 -1
- package/package.json +2 -2
- package/tsdoc-metadata.json +1 -1
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
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 };
|