@effected/walker 0.6.0 → 0.8.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/Descend.js +27 -44
- package/Expand.js +1 -1
- package/index.d.ts +72 -6
- package/package.json +3 -3
- package/tsdoc-metadata.json +1 -1
package/Descend.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Effect, FileSystem, Path, Schema } from "effect";
|
|
|
2
2
|
|
|
3
3
|
//#region src/Descend.ts
|
|
4
4
|
/**
|
|
5
|
-
* Typed failure raised by
|
|
5
|
+
* Typed failure raised by `descend`: a directory mid-walk was unreadable
|
|
6
6
|
* (under `onUnreadable: "fail"`), or the walk descended past `maxDepth`. Depth
|
|
7
7
|
* exhaustion is a typed failure, never a truncation — silent truncation
|
|
8
8
|
* silently changes match semantics.
|
|
@@ -41,57 +41,32 @@ const escapesCwd = (relative) => {
|
|
|
41
41
|
}
|
|
42
42
|
return false;
|
|
43
43
|
};
|
|
44
|
-
/**
|
|
45
|
-
|
|
46
|
-
* FILE paths relative to `cwd` (POSIX separators), sorted by relative path.
|
|
47
|
-
*
|
|
48
|
-
* @remarks
|
|
49
|
-
* A literal pattern (no magic, not negated) fast-paths to a single stat: the
|
|
50
|
-
* result is `[source]` when it resolves to a file, `[]` otherwise — a missing
|
|
51
|
-
* path is zero matches, not an error. A magic pattern walks from its literal
|
|
52
|
-
* directory prefix (`GlobPattern.enumerationPrefix`); a NEGATED pattern can
|
|
53
|
-
* match paths outside that prefix, so it walks from `cwd` itself. A missing
|
|
54
|
-
* base directory is likewise an empty result, because zero matches is a
|
|
55
|
-
* normal glob answer — as is any pattern that lexically climbs above `cwd`
|
|
56
|
-
* via `..` segments (walked paths never contain `..`, and the walk never
|
|
57
|
-
* reads outside its documented root). Only an unreadable directory mid-walk
|
|
58
|
-
* (under the default `onUnreadable: "fail"`) or a walk past `maxDepth` fails,
|
|
59
|
-
* typed as {@link DescendError}.
|
|
60
|
-
*
|
|
61
|
-
* Only files match. A symlink counts when it stat-resolves to a file
|
|
62
|
-
* (`FileSystem.stat` follows links, as node's does); a symlinked directory is
|
|
63
|
-
* never descended into (cycle safety — detected by a `readLink` probe); a
|
|
64
|
-
* dangling symlink is not a match. A directory that vanishes between its
|
|
65
|
-
* parent's listing and its own read is a benign race and reads as empty. A
|
|
66
|
-
* pattern that cannot match below one level (no globstar, no mid-pattern
|
|
67
|
-
* magic segment) reads a single level and never descends.
|
|
68
|
-
*
|
|
69
|
-
* The descent is a worklist, not a recursion — it cannot overflow the stack —
|
|
70
|
-
* dequeued by head index, never `Array.shift()`. Like `ascend`, `maxDepth`
|
|
71
|
-
* must be a positive integer: anything else is a defect, never a
|
|
72
|
-
* silently-empty result.
|
|
73
|
-
*
|
|
74
|
-
* @public
|
|
75
|
-
*/
|
|
76
|
-
const descend = Effect.fn("Walker.descend")(function* (pattern, options) {
|
|
44
|
+
/** Not exported — `descend`'s overloads below carry the public documentation. */
|
|
45
|
+
const descendImpl = Effect.fn("Walker.descend")(function* (pattern, options) {
|
|
77
46
|
const fs = yield* FileSystem.FileSystem;
|
|
78
47
|
const path = yield* Path.Path;
|
|
79
48
|
const maxDepth = options.maxDepth ?? 256;
|
|
80
49
|
if (!Number.isInteger(maxDepth) || maxDepth < 1) return yield* Effect.die(/* @__PURE__ */ new Error(`Walker.descend: maxDepth must be a positive integer, received ${maxDepth}`));
|
|
81
50
|
const prune = new Set(options.prune ?? DEFAULT_PRUNE);
|
|
82
51
|
const onUnreadable = options.onUnreadable ?? "fail";
|
|
52
|
+
const unreadable = [];
|
|
83
53
|
/** The stat-resolved type of `absolute`, or `undefined` when it does not resolve (missing, dangling symlink, unstatable). */
|
|
84
54
|
const typeOf = (absolute) => fs.stat(absolute).pipe(Effect.map((info) => info.type), Effect.orElseSucceed(() => void 0));
|
|
85
55
|
/** Whether `absolute` is itself a symlink. `readLink` succeeds only on links; any failure means "not one". */
|
|
86
56
|
const isSymbolicLink = (absolute) => fs.readLink(absolute).pipe(Effect.map(() => true), Effect.orElseSucceed(() => false));
|
|
57
|
+
/** Wrap the walk's success value per `onUnreadable`: a plain array unless "record" asked for the pair. */
|
|
58
|
+
const finish = (matches) => onUnreadable === "record" ? {
|
|
59
|
+
matches,
|
|
60
|
+
unreadable
|
|
61
|
+
} : matches;
|
|
87
62
|
if (!pattern.hasMagic && !pattern.negated) {
|
|
88
|
-
if (escapesCwd(pattern.source)) return [];
|
|
89
|
-
return (yield* typeOf(path.join(options.cwd, pattern.source))) === "File" ? [pattern.source] : [];
|
|
63
|
+
if (escapesCwd(pattern.source)) return finish([]);
|
|
64
|
+
return finish((yield* typeOf(path.join(options.cwd, pattern.source))) === "File" ? [pattern.source] : []);
|
|
90
65
|
}
|
|
91
66
|
const base = pattern.negated ? "" : pattern.enumerationPrefix.replace(/\/+$/, "");
|
|
92
|
-
if (escapesCwd(base)) return [];
|
|
67
|
+
if (escapesCwd(base)) return finish([]);
|
|
93
68
|
const absoluteBase = base === "" ? options.cwd : path.join(options.cwd, base);
|
|
94
|
-
if ((yield* typeOf(absoluteBase)) !== "Directory") return [];
|
|
69
|
+
if ((yield* typeOf(absoluteBase)) !== "Directory") return finish([]);
|
|
95
70
|
const deep = pattern.crossesSegments || pattern.negated;
|
|
96
71
|
const results = [];
|
|
97
72
|
const frames = [{
|
|
@@ -102,11 +77,16 @@ const descend = Effect.fn("Walker.descend")(function* (pattern, options) {
|
|
|
102
77
|
for (let head = 0; head < frames.length; head += 1) {
|
|
103
78
|
const frame = frames[head];
|
|
104
79
|
if (frame === void 0) break;
|
|
105
|
-
const entries = yield* fs.readDirectory(frame.absolute).pipe(Effect.catch((error) =>
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
80
|
+
const entries = yield* fs.readDirectory(frame.absolute).pipe(Effect.catch((error) => {
|
|
81
|
+
if (error.reason._tag === "NotFound") return Effect.succeed([]);
|
|
82
|
+
if (onUnreadable === "fail") return Effect.fail(new DescendError({
|
|
83
|
+
pattern: pattern.source,
|
|
84
|
+
reason: "unreadableDirectory",
|
|
85
|
+
path: frame.relative
|
|
86
|
+
}));
|
|
87
|
+
if (onUnreadable === "record") unreadable.push(frame.relative);
|
|
88
|
+
return Effect.succeed([]);
|
|
89
|
+
}));
|
|
110
90
|
for (const entry of entries) {
|
|
111
91
|
const relative = frame.relative === "" ? entry : `${frame.relative}/${entry}`;
|
|
112
92
|
const absolute = path.join(frame.absolute, entry);
|
|
@@ -132,8 +112,11 @@ const descend = Effect.fn("Walker.descend")(function* (pattern, options) {
|
|
|
132
112
|
}
|
|
133
113
|
}
|
|
134
114
|
results.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
135
|
-
return results;
|
|
115
|
+
return finish(results);
|
|
136
116
|
});
|
|
117
|
+
function descend(pattern, options) {
|
|
118
|
+
return descendImpl(pattern, options);
|
|
119
|
+
}
|
|
137
120
|
|
|
138
121
|
//#endregion
|
|
139
122
|
export { DescendError, descend };
|
package/Expand.js
CHANGED
|
@@ -45,7 +45,7 @@ var GlobExpansionError = class extends Schema.TaggedError()("GlobExpansionError"
|
|
|
45
45
|
* matching FILE paths relative to `options.cwd`, POSIX separators, sorted.
|
|
46
46
|
*
|
|
47
47
|
* @remarks
|
|
48
|
-
* The recipe form of
|
|
48
|
+
* The recipe form of `descend`. Everything `descend` documents about
|
|
49
49
|
* traversal holds unchanged — the literal fast-path, the negated-pattern walk
|
|
50
50
|
* from `cwd`, files-only matching, symlink and prune handling, `maxDepth`,
|
|
51
51
|
* `onUnreadable` — because this delegates to it. What this adds is the seam:
|
package/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { GlobPattern, GlobPatternError, GlobPatternOptions } from "@effected/glo
|
|
|
2
2
|
import { Effect, FileSystem, Option, Path, Schema } from "effect";
|
|
3
3
|
//#region src/Descend.d.ts
|
|
4
4
|
/**
|
|
5
|
-
* Options for
|
|
5
|
+
* Options for `descend`.
|
|
6
6
|
*
|
|
7
7
|
* @public
|
|
8
8
|
*/
|
|
@@ -17,10 +17,61 @@ interface DescendOptions {
|
|
|
17
17
|
* What an unreadable directory mid-walk does. `"fail"` (the default) fails
|
|
18
18
|
* typed — downward enumeration must not silently swallow a subtree, or the
|
|
19
19
|
* answer is silently missing membership dressed as an empty one. `"skip"`
|
|
20
|
-
* absorbs the failure and continues.
|
|
20
|
+
* absorbs the failure and continues, discarding which directory it was.
|
|
21
|
+
* To collect the offending directories instead of discarding them, pass
|
|
22
|
+
* {@link DescendRecordOptions} — `"record"` is deliberately NOT a member
|
|
23
|
+
* here, because this type is the options contract of the overload that
|
|
24
|
+
* returns a bare match array.
|
|
21
25
|
*/
|
|
22
26
|
readonly onUnreadable?: "fail" | "skip";
|
|
23
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Options for `descend` under `onUnreadable: "record"`: every
|
|
30
|
+
* {@link DescendOptions} field, with `onUnreadable` fixed to `"record"`.
|
|
31
|
+
*
|
|
32
|
+
* @remarks
|
|
33
|
+
* A separate type rather than `DescendOptions & { onUnreadable: "record" }`,
|
|
34
|
+
* because the two options types are the discriminator between two different
|
|
35
|
+
* RETURN types. If `DescendOptions` itself admitted `"record"`, a value
|
|
36
|
+
* widened to `DescendOptions` — annotated as such, or passed through a
|
|
37
|
+
* function taking it — would select the array-returning overload at compile
|
|
38
|
+
* time while the implementation resolved a {@link DescendResult} at runtime,
|
|
39
|
+
* and every array method on that result would fail with no type error
|
|
40
|
+
* anywhere. Keeping `"record"` out of `DescendOptions` makes that
|
|
41
|
+
* unrepresentable.
|
|
42
|
+
*
|
|
43
|
+
* @public
|
|
44
|
+
*/
|
|
45
|
+
interface DescendRecordOptions extends Omit<DescendOptions, "onUnreadable"> {
|
|
46
|
+
/** Collect every unreadable directory rather than failing or discarding it. */
|
|
47
|
+
readonly onUnreadable: "record";
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* `descend`'s success value under `onUnreadable: "record"`: the matched
|
|
51
|
+
* FILE paths plus the `cwd`-relative path of every mid-walk directory whose
|
|
52
|
+
* `readDirectory` failed for a reason other than `NotFound` (a vanished
|
|
53
|
+
* directory stays a benign race in every mode and is never recorded).
|
|
54
|
+
*
|
|
55
|
+
* @public
|
|
56
|
+
*/
|
|
57
|
+
interface DescendResult {
|
|
58
|
+
/** Matching FILE paths relative to `cwd`, POSIX separators, sorted — identical in shape to the `"fail"`/`"skip"` success value. */
|
|
59
|
+
readonly matches: ReadonlyArray<string>;
|
|
60
|
+
/**
|
|
61
|
+
* `cwd`-relative paths of directories that could not be read, in walk order.
|
|
62
|
+
*
|
|
63
|
+
* @remarks
|
|
64
|
+
* **The walk base appears as the empty string `""`**, since its own
|
|
65
|
+
* `cwd`-relative path is empty — so an unreadable base yields
|
|
66
|
+
* `{ matches: [], unreadable: [""] }`. Code matching these entries as
|
|
67
|
+
* ordinary paths will not expect that; special-case it.
|
|
68
|
+
*
|
|
69
|
+
* No cause travels with an entry. A caller that must report WHY a directory
|
|
70
|
+
* was unreadable has to re-read it to obtain the `PlatformError`, which is
|
|
71
|
+
* a second syscall for information this walk had and discarded.
|
|
72
|
+
*/
|
|
73
|
+
readonly unreadable: ReadonlyArray<string>;
|
|
74
|
+
}
|
|
24
75
|
declare const DescendError_base: Schema.Class<DescendError, Schema.TaggedStruct<"DescendError", {
|
|
25
76
|
/** The glob pattern's source text. */
|
|
26
77
|
readonly pattern: Schema.String;
|
|
@@ -31,7 +82,7 @@ declare const DescendError_base: Schema.Class<DescendError, Schema.TaggedStruct<
|
|
|
31
82
|
readonly limit: Schema.optionalKey<Schema.Number>;
|
|
32
83
|
}>, import("effect/Cause").YieldableError>;
|
|
33
84
|
/**
|
|
34
|
-
* Typed failure raised by
|
|
85
|
+
* Typed failure raised by `descend`: a directory mid-walk was unreadable
|
|
35
86
|
* (under `onUnreadable: "fail"`), or the walk descended past `maxDepth`. Depth
|
|
36
87
|
* exhaustion is a typed failure, never a truncation — silent truncation
|
|
37
88
|
* silently changes match semantics.
|
|
@@ -41,6 +92,18 @@ declare const DescendError_base: Schema.Class<DescendError, Schema.TaggedStruct<
|
|
|
41
92
|
export declare class DescendError extends DescendError_base {
|
|
42
93
|
get message(): string;
|
|
43
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Expand a compiled glob pattern against the filesystem under
|
|
97
|
+
* `onUnreadable: "record"`, resolving to a {@link DescendResult} — the
|
|
98
|
+
* matched FILE paths relative to `cwd` (POSIX separators, sorted) plus the
|
|
99
|
+
* `cwd`-relative path of every mid-walk directory whose `readDirectory`
|
|
100
|
+
* failed for a reason other than `NotFound`. The walk continues past each
|
|
101
|
+
* such directory exactly as `"skip"` does; it never aborts and it never
|
|
102
|
+
* discards the offending path.
|
|
103
|
+
*
|
|
104
|
+
* @public
|
|
105
|
+
*/
|
|
106
|
+
export declare function descend(pattern: GlobPattern, options: DescendRecordOptions): Effect.Effect<DescendResult, DescendError, FileSystem.FileSystem | Path.Path>;
|
|
44
107
|
/**
|
|
45
108
|
* Expand a compiled glob pattern against the filesystem, returning matching
|
|
46
109
|
* FILE paths relative to `cwd` (POSIX separators), sorted by relative path.
|
|
@@ -71,9 +134,12 @@ export declare class DescendError extends DescendError_base {
|
|
|
71
134
|
* must be a positive integer: anything else is a defect, never a
|
|
72
135
|
* silently-empty result.
|
|
73
136
|
*
|
|
137
|
+
* Passing `onUnreadable: "record"` resolves to a {@link DescendResult}
|
|
138
|
+
* instead — see that overload.
|
|
139
|
+
*
|
|
74
140
|
* @public
|
|
75
141
|
*/
|
|
76
|
-
export declare
|
|
142
|
+
export declare function descend(pattern: GlobPattern, options: DescendOptions): Effect.Effect<ReadonlyArray<string>, DescendError, FileSystem.FileSystem | Path.Path>;
|
|
77
143
|
//#endregion
|
|
78
144
|
//#region src/Expand.d.ts
|
|
79
145
|
/**
|
|
@@ -136,7 +202,7 @@ export declare class GlobExpansionError extends GlobExpansionError_base {
|
|
|
136
202
|
* matching FILE paths relative to `options.cwd`, POSIX separators, sorted.
|
|
137
203
|
*
|
|
138
204
|
* @remarks
|
|
139
|
-
* The recipe form of
|
|
205
|
+
* The recipe form of `descend`. Everything `descend` documents about
|
|
140
206
|
* traversal holds unchanged — the literal fast-path, the negated-pattern walk
|
|
141
207
|
* from `cwd`, files-only matching, symlink and prune handling, `maxDepth`,
|
|
142
208
|
* `onUnreadable` — because this delegates to it. What this adds is the seam:
|
|
@@ -289,5 +355,5 @@ export declare class Walker {
|
|
|
289
355
|
static readonly findRoot: <E, R>(dirs: ReadonlyArray<string>, isRoot: (dir: string) => Effect.Effect<boolean, E, R>) => Effect.Effect<Option.Option<string>, never, R>;
|
|
290
356
|
}
|
|
291
357
|
//#endregion
|
|
292
|
-
export type { AscendOptions, CompileAndExpandOptions, DescendOptions };
|
|
358
|
+
export type { AscendOptions, CompileAndExpandOptions, DescendOptions, DescendRecordOptions, DescendResult };
|
|
293
359
|
//# sourceMappingURL=index.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/walker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Path traversal as Effect primitives: ascend a directory chain to the first matching candidate, or descend a tree expanding a glob against the filesystem.",
|
|
6
6
|
"keywords": [
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"./package.json": "./package.json"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"@effected/glob": "^0.
|
|
41
|
-
"effect": "4.0.0-rc.
|
|
40
|
+
"@effected/glob": "^0.6.0",
|
|
41
|
+
"effect": "4.0.0-rc.115"
|
|
42
42
|
},
|
|
43
43
|
"engines": {
|
|
44
44
|
"node": ">=24.11.0"
|