@effected/walker 0.5.0 → 0.7.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 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 {@link descend}: a directory mid-walk was unreadable
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
- * Expand a compiled glob pattern against the filesystem, returning matching
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) => error.reason._tag === "NotFound" || onUnreadable === "skip" ? Effect.succeed([]) : Effect.fail(new DescendError({
106
- pattern: pattern.source,
107
- reason: "unreadableDirectory",
108
- path: frame.relative
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 {@link descend}. Everything `descend` documents about
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 {@link descend}.
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,16 +82,28 @@ 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 {@link descend}: a directory mid-walk was unreadable
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.
38
89
  *
39
90
  * @public
40
91
  */
41
- declare class DescendError extends DescendError_base {
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 @@ 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
- declare const descend: (pattern: GlobPattern, options: DescendOptions) => Effect.Effect<ReadonlyArray<string>, DescendError, FileSystem.FileSystem | Path.Path>;
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
  /**
@@ -122,7 +188,7 @@ declare const GlobExpansionError_base: Schema.Class<GlobExpansionError, Schema.T
122
188
  *
123
189
  * @public
124
190
  */
125
- declare class GlobExpansionError extends GlobExpansionError_base {
191
+ export declare class GlobExpansionError extends GlobExpansionError_base {
126
192
  /**
127
193
  * Which phase failed — `"compile"` when the pattern itself was rejected,
128
194
  * `"descend"` when the filesystem walk failed. A convenience over
@@ -136,7 +202,7 @@ 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 {@link descend}. Everything `descend` documents about
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:
@@ -171,7 +237,7 @@ declare class GlobExpansionError extends GlobExpansionError_base {
171
237
  *
172
238
  * @public
173
239
  */
174
- declare const compileAndExpand: (pattern: string, options: CompileAndExpandOptions) => Effect.Effect<ReadonlyArray<string>, GlobExpansionError, FileSystem.FileSystem | Path.Path>;
240
+ export declare const compileAndExpand: (pattern: string, options: CompileAndExpandOptions) => Effect.Effect<ReadonlyArray<string>, GlobExpansionError, FileSystem.FileSystem | Path.Path>;
175
241
  //#endregion
176
242
  //#region src/Walker.d.ts
177
243
  /**
@@ -229,7 +295,7 @@ interface AscendOptions {
229
295
  *
230
296
  * @public
231
297
  */
232
- declare class Walker {
298
+ export declare class Walker {
233
299
  private constructor();
234
300
  /**
235
301
  * Ascend from `start` toward the filesystem root, yielding each directory,
@@ -289,5 +355,5 @@ 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, type CompileAndExpandOptions, DescendError, type DescendOptions, GlobExpansionError, Walker, compileAndExpand, descend };
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.5.0",
3
+ "version": "0.7.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.4.0",
41
- "effect": "4.0.0-rc.109"
40
+ "@effected/glob": "^0.5.0",
41
+ "effect": "4.0.0-rc.112"
42
42
  },
43
43
  "engines": {
44
44
  "node": ">=24.11.0"
@@ -5,7 +5,7 @@
5
5
  "toolPackages": [
6
6
  {
7
7
  "packageName": "@microsoft/api-extractor",
8
- "packageVersion": "7.58.12"
8
+ "packageVersion": "7.59.0"
9
9
  }
10
10
  ]
11
11
  }