@effected/walker 0.2.2 → 0.3.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/Expand.js +96 -0
- package/README.md +1 -1
- package/Walker.js +12 -1
- package/index.d.ts +140 -3
- package/index.js +2 -1
- package/package.json +2 -2
package/Expand.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { DescendError, descend } from "./Descend.js";
|
|
2
|
+
import { Effect, Result, Schema } from "effect";
|
|
3
|
+
import { GlobPattern, GlobPatternError } from "@effected/glob";
|
|
4
|
+
|
|
5
|
+
//#region src/Expand.ts
|
|
6
|
+
/**
|
|
7
|
+
* Typed failure raised by {@link compileAndExpand}: the single error the
|
|
8
|
+
* compile+expand recipe fails with, so a caller catches one tag rather than
|
|
9
|
+
* folding two error channels by hand.
|
|
10
|
+
*
|
|
11
|
+
* @remarks
|
|
12
|
+
* One tag, two genuinely different causes — "your pattern is malformed" and
|
|
13
|
+
* "that directory is unreadable" are different problems with different fixes,
|
|
14
|
+
* so `cause` keeps the underlying typed error intact
|
|
15
|
+
* rather than flattening it into a string. Discriminate on `cause._tag`
|
|
16
|
+
* (`"GlobPatternError"` vs `"DescendError"`), or read
|
|
17
|
+
* {@link GlobExpansionError.stage} when only the phase matters; either way the
|
|
18
|
+
* original payload — a guard's `limit`/`actual`, a descent's `path` — is still
|
|
19
|
+
* there. `cause` is also the native `Error` cause, so error chaining and
|
|
20
|
+
* stack-printing work without extra wiring.
|
|
21
|
+
*
|
|
22
|
+
* @public
|
|
23
|
+
*/
|
|
24
|
+
var GlobExpansionError = class extends Schema.TaggedErrorClass()("GlobExpansionError", {
|
|
25
|
+
/** The glob pattern's source text, as handed to {@link compileAndExpand}. */
|
|
26
|
+
pattern: Schema.String,
|
|
27
|
+
/** The underlying typed failure, intact: a compile guard trip or a descent failure. */
|
|
28
|
+
cause: Schema.Union([GlobPatternError, DescendError])
|
|
29
|
+
}) {
|
|
30
|
+
/**
|
|
31
|
+
* Which phase failed — `"compile"` when the pattern itself was rejected,
|
|
32
|
+
* `"descend"` when the filesystem walk failed. A convenience over
|
|
33
|
+
* `cause._tag` for callers that only need the phase.
|
|
34
|
+
*/
|
|
35
|
+
get stage() {
|
|
36
|
+
return this.cause._tag === "GlobPatternError" ? "compile" : "descend";
|
|
37
|
+
}
|
|
38
|
+
get message() {
|
|
39
|
+
const shown = this.pattern.length > 64 ? `${this.pattern.slice(0, 64)}…` : this.pattern;
|
|
40
|
+
return `glob expansion of ${JSON.stringify(shown)} failed during ${this.stage}: ${this.cause.message}`;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Compile a glob pattern and expand it against the filesystem in one call:
|
|
45
|
+
* matching FILE paths relative to `options.cwd`, POSIX separators, sorted.
|
|
46
|
+
*
|
|
47
|
+
* @remarks
|
|
48
|
+
* The recipe form of {@link descend}. Everything `descend` documents about
|
|
49
|
+
* traversal holds unchanged — the literal fast-path, the negated-pattern walk
|
|
50
|
+
* from `cwd`, files-only matching, symlink and prune handling, `maxDepth`,
|
|
51
|
+
* `onUnreadable` — because this delegates to it. What this adds is the seam:
|
|
52
|
+
* the pattern arrives as a string, and both failure modes arrive as one
|
|
53
|
+
* {@link GlobExpansionError} with the underlying error preserved in `cause`.
|
|
54
|
+
*
|
|
55
|
+
* A missing base directory, a pattern that climbs above `cwd`, and a pattern
|
|
56
|
+
* that simply matches nothing are all an EMPTY result, not a failure — zero
|
|
57
|
+
* matches is a normal glob answer. Only a rejected pattern or a failed walk
|
|
58
|
+
* produces an error.
|
|
59
|
+
*
|
|
60
|
+
* `FileSystem` and `Path` stay in the `R` channel and are **deliberately not
|
|
61
|
+
* provided here**, even though hand-providing them is the friction this
|
|
62
|
+
* recipe otherwise removes. `FileSystem` cannot be provided — a library that
|
|
63
|
+
* picks its own filesystem cannot be tested against a fixture tree. Given
|
|
64
|
+
* that, providing `Path` internally would not save the caller a layer (they
|
|
65
|
+
* still supply `FileSystem`), and it would actively break win32: the walk
|
|
66
|
+
* would join paths POSIX-style against a caller's win32 filesystem. The
|
|
67
|
+
* consumer's platform layer stays the single place that choice is made.
|
|
68
|
+
* Provide both once at the application boundary, not per call site.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```ts
|
|
72
|
+
* import { compileAndExpand } from "@effected/walker"
|
|
73
|
+
* import { GlobPatternOptions } from "@effected/glob"
|
|
74
|
+
*
|
|
75
|
+
* const files = yield* compileAndExpand("packages/*/src/**/*.ts", {
|
|
76
|
+
* cwd: "/repo",
|
|
77
|
+
* glob: GlobPatternOptions.make({ dot: true })
|
|
78
|
+
* })
|
|
79
|
+
* ```
|
|
80
|
+
*
|
|
81
|
+
* @public
|
|
82
|
+
*/
|
|
83
|
+
const compileAndExpand = Effect.fn("Walker.compileAndExpand")(function* (pattern, options) {
|
|
84
|
+
const compiled = GlobPattern.compileResult(pattern, options.glob);
|
|
85
|
+
if (Result.isFailure(compiled)) return yield* new GlobExpansionError({
|
|
86
|
+
pattern,
|
|
87
|
+
cause: compiled.failure
|
|
88
|
+
});
|
|
89
|
+
return yield* descend(compiled.success, options).pipe(Effect.mapError((cause) => new GlobExpansionError({
|
|
90
|
+
pattern,
|
|
91
|
+
cause
|
|
92
|
+
})));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
//#endregion
|
|
96
|
+
export { GlobExpansionError, compileAndExpand };
|
package/README.md
CHANGED
|
@@ -82,7 +82,7 @@ The predicate can be expensive — reading and parsing a `package.json` to decid
|
|
|
82
82
|
|
|
83
83
|
## Features
|
|
84
84
|
|
|
85
|
-
- `Walker.ascend(start, options?)` — the directory chain from `start` toward the filesystem root, nearest first. `stopAt` halts the ascent inclusively, `maxDepth` (default 256) caps
|
|
85
|
+
- `Walker.ascend(start, options?)` — the directory chain from `start` toward the filesystem root, nearest first. `stopAt` halts the ascent inclusively — it must be absolute, and is matched in normalized form, so a trailing separator or a `.`/`..` segment still stops where it names; a relative ceiling is a defect rather than being resolved against the working directory, exactly as an invalid `maxDepth` is. `maxDepth` (default 256) caps the chain. Lexical, not physical: `Path.dirname` does not resolve symlinks, so ascending out of a symlinked directory follows the path you were given.
|
|
86
86
|
- `Walker.firstMatch(candidates, predicate)` — the first candidate the predicate accepts. Absorbs each predicate failure individually and short-circuits at the first match.
|
|
87
87
|
- `Walker.findUpward(dirs, candidatesFor)` — the first existing path, directory-major: every candidate in the nearest directory is tried before ascending.
|
|
88
88
|
- `Walker.findRoot(dirs, isRoot)` — the nearest directory a marker predicate accepts. `firstMatch` where the candidate expansion is the identity.
|
package/Walker.js
CHANGED
|
@@ -14,17 +14,28 @@ import { Effect, FileSystem, Option, Path } from "effect";
|
|
|
14
14
|
* Bounded twice over: `dirname` is a fixpoint at the root, and `maxDepth` guards
|
|
15
15
|
* a pathological `Path` implementation that never reaches one.
|
|
16
16
|
*
|
|
17
|
+
* `stopAt` is compared in normalized form — see {@link AscendOptions.stopAt}.
|
|
18
|
+
* Raw string equality made the ceiling fail OPEN: an unnormalized ceiling
|
|
19
|
+
* matched nothing and the ascent ran silently to the filesystem root, which is
|
|
20
|
+
* the unbounded walk the option exists to prevent, with no error to notice it
|
|
21
|
+
* by. A relative ceiling **dies** for the same reason — resolving one against
|
|
22
|
+
* `process.cwd()` would reintroduce the silent-wrong-walk failure through a
|
|
23
|
+
* different door. See {@link AscendOptions.stopAt} for why that is a defect
|
|
24
|
+
* rather than a typed error; the error channel stays `never`.
|
|
25
|
+
*
|
|
17
26
|
* @public
|
|
18
27
|
*/
|
|
19
28
|
const ascend = (start, options) => Effect.gen(function* () {
|
|
20
29
|
const path = yield* Path.Path;
|
|
21
30
|
const maxDepth = options?.maxDepth ?? 256;
|
|
22
31
|
if (!Number.isInteger(maxDepth) || maxDepth < 1) return yield* Effect.die(/* @__PURE__ */ new Error(`Walker.ascend: maxDepth must be a positive integer, received ${maxDepth}`));
|
|
32
|
+
if (options?.stopAt !== void 0 && !path.isAbsolute(options.stopAt)) return yield* Effect.die(/* @__PURE__ */ new Error(`Walker.ascend: stopAt must be an absolute path, received ${JSON.stringify(options.stopAt)} (ascending from ${JSON.stringify(start)})`));
|
|
33
|
+
const ceiling = options?.stopAt === void 0 ? void 0 : path.resolve(options.stopAt);
|
|
23
34
|
const dirs = [];
|
|
24
35
|
let current = start;
|
|
25
36
|
for (let depth = 0; depth < maxDepth; depth++) {
|
|
26
37
|
dirs.push(current);
|
|
27
|
-
if (
|
|
38
|
+
if (ceiling !== void 0 && (current === ceiling || path.resolve(current) === ceiling)) break;
|
|
28
39
|
const parent = path.dirname(current);
|
|
29
40
|
if (parent === current) break;
|
|
30
41
|
current = parent;
|
package/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GlobPattern } from "@effected/glob";
|
|
1
|
+
import { GlobPattern, GlobPatternError, GlobPatternOptions } from "@effected/glob";
|
|
2
2
|
import { Effect, FileSystem, Option, Path, Schema } from "effect";
|
|
3
3
|
//#region src/Descend.d.ts
|
|
4
4
|
/**
|
|
@@ -75,6 +75,104 @@ declare class DescendError extends DescendError_base {
|
|
|
75
75
|
*/
|
|
76
76
|
declare const descend: (pattern: GlobPattern, options: DescendOptions) => Effect.Effect<ReadonlyArray<string>, DescendError, FileSystem.FileSystem | Path.Path>;
|
|
77
77
|
//#endregion
|
|
78
|
+
//#region src/Expand.d.ts
|
|
79
|
+
/**
|
|
80
|
+
* Options for {@link compileAndExpand}: every {@link DescendOptions} field,
|
|
81
|
+
* plus the glob options the pattern compiles under.
|
|
82
|
+
*
|
|
83
|
+
* @public
|
|
84
|
+
*/
|
|
85
|
+
interface CompileAndExpandOptions extends DescendOptions {
|
|
86
|
+
/**
|
|
87
|
+
* The options the pattern compiles under — **required, deliberately**.
|
|
88
|
+
*
|
|
89
|
+
* @remarks
|
|
90
|
+
* Matching semantics (`dot` above all) are the thing two call sites most
|
|
91
|
+
* easily disagree about, and an optional field invites exactly that: one
|
|
92
|
+
* site passes `{ dot: true }`, another omits it, and the same package now
|
|
93
|
+
* has two glob dialects that nothing makes visible. Required means every
|
|
94
|
+
* call site states its dialect in its own source, so a divergence is a
|
|
95
|
+
* visible difference between two spellings rather than the absence of one.
|
|
96
|
+
* Pass `GlobPatternOptions.make({})` to mean "the defaults" — that is a
|
|
97
|
+
* deliberate choice being written down, not boilerplate.
|
|
98
|
+
*/
|
|
99
|
+
readonly glob: GlobPatternOptions;
|
|
100
|
+
}
|
|
101
|
+
declare const GlobExpansionError_base: Schema.Class<GlobExpansionError, Schema.TaggedStruct<"GlobExpansionError", {
|
|
102
|
+
/** The glob pattern's source text, as handed to {@link compileAndExpand}. */
|
|
103
|
+
readonly pattern: Schema.String;
|
|
104
|
+
/** The underlying typed failure, intact: a compile guard trip or a descent failure. */
|
|
105
|
+
readonly cause: Schema.Union<readonly [typeof GlobPatternError, typeof DescendError]>;
|
|
106
|
+
}>, import("effect/Cause").YieldableError>;
|
|
107
|
+
/**
|
|
108
|
+
* Typed failure raised by {@link compileAndExpand}: the single error the
|
|
109
|
+
* compile+expand recipe fails with, so a caller catches one tag rather than
|
|
110
|
+
* folding two error channels by hand.
|
|
111
|
+
*
|
|
112
|
+
* @remarks
|
|
113
|
+
* One tag, two genuinely different causes — "your pattern is malformed" and
|
|
114
|
+
* "that directory is unreadable" are different problems with different fixes,
|
|
115
|
+
* so `cause` keeps the underlying typed error intact
|
|
116
|
+
* rather than flattening it into a string. Discriminate on `cause._tag`
|
|
117
|
+
* (`"GlobPatternError"` vs `"DescendError"`), or read
|
|
118
|
+
* {@link GlobExpansionError.stage} when only the phase matters; either way the
|
|
119
|
+
* original payload — a guard's `limit`/`actual`, a descent's `path` — is still
|
|
120
|
+
* there. `cause` is also the native `Error` cause, so error chaining and
|
|
121
|
+
* stack-printing work without extra wiring.
|
|
122
|
+
*
|
|
123
|
+
* @public
|
|
124
|
+
*/
|
|
125
|
+
declare class GlobExpansionError extends GlobExpansionError_base {
|
|
126
|
+
/**
|
|
127
|
+
* Which phase failed — `"compile"` when the pattern itself was rejected,
|
|
128
|
+
* `"descend"` when the filesystem walk failed. A convenience over
|
|
129
|
+
* `cause._tag` for callers that only need the phase.
|
|
130
|
+
*/
|
|
131
|
+
get stage(): "compile" | "descend";
|
|
132
|
+
get message(): string;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Compile a glob pattern and expand it against the filesystem in one call:
|
|
136
|
+
* matching FILE paths relative to `options.cwd`, POSIX separators, sorted.
|
|
137
|
+
*
|
|
138
|
+
* @remarks
|
|
139
|
+
* The recipe form of {@link descend}. Everything `descend` documents about
|
|
140
|
+
* traversal holds unchanged — the literal fast-path, the negated-pattern walk
|
|
141
|
+
* from `cwd`, files-only matching, symlink and prune handling, `maxDepth`,
|
|
142
|
+
* `onUnreadable` — because this delegates to it. What this adds is the seam:
|
|
143
|
+
* the pattern arrives as a string, and both failure modes arrive as one
|
|
144
|
+
* {@link GlobExpansionError} with the underlying error preserved in `cause`.
|
|
145
|
+
*
|
|
146
|
+
* A missing base directory, a pattern that climbs above `cwd`, and a pattern
|
|
147
|
+
* that simply matches nothing are all an EMPTY result, not a failure — zero
|
|
148
|
+
* matches is a normal glob answer. Only a rejected pattern or a failed walk
|
|
149
|
+
* produces an error.
|
|
150
|
+
*
|
|
151
|
+
* `FileSystem` and `Path` stay in the `R` channel and are **deliberately not
|
|
152
|
+
* provided here**, even though hand-providing them is the friction this
|
|
153
|
+
* recipe otherwise removes. `FileSystem` cannot be provided — a library that
|
|
154
|
+
* picks its own filesystem cannot be tested against a fixture tree. Given
|
|
155
|
+
* that, providing `Path` internally would not save the caller a layer (they
|
|
156
|
+
* still supply `FileSystem`), and it would actively break win32: the walk
|
|
157
|
+
* would join paths POSIX-style against a caller's win32 filesystem. The
|
|
158
|
+
* consumer's platform layer stays the single place that choice is made.
|
|
159
|
+
* Provide both once at the application boundary, not per call site.
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```ts
|
|
163
|
+
* import { compileAndExpand } from "@effected/walker"
|
|
164
|
+
* import { GlobPatternOptions } from "@effected/glob"
|
|
165
|
+
*
|
|
166
|
+
* const files = yield* compileAndExpand("packages/*/src/**/*.ts", {
|
|
167
|
+
* cwd: "/repo",
|
|
168
|
+
* glob: GlobPatternOptions.make({ dot: true })
|
|
169
|
+
* })
|
|
170
|
+
* ```
|
|
171
|
+
*
|
|
172
|
+
* @public
|
|
173
|
+
*/
|
|
174
|
+
declare const compileAndExpand: (pattern: string, options: CompileAndExpandOptions) => Effect.Effect<ReadonlyArray<string>, GlobExpansionError, FileSystem.FileSystem | Path.Path>;
|
|
175
|
+
//#endregion
|
|
78
176
|
//#region src/Walker.d.ts
|
|
79
177
|
/**
|
|
80
178
|
* Options for {@link Walker.ascend}.
|
|
@@ -82,7 +180,46 @@ declare const descend: (pattern: GlobPattern, options: DescendOptions) => Effect
|
|
|
82
180
|
* @public
|
|
83
181
|
*/
|
|
84
182
|
interface AscendOptions {
|
|
85
|
-
/**
|
|
183
|
+
/**
|
|
184
|
+
* Stop after this directory, inclusive.
|
|
185
|
+
*
|
|
186
|
+
* **Must be absolute**, and is **normalized before comparison**. The ceiling
|
|
187
|
+
* is matched against each directory's `Path.resolve` form rather than by raw
|
|
188
|
+
* string equality, so a trailing separator, a `.` or `..` segment, or a
|
|
189
|
+
* duplicated separator all stop at the ancestor they name. Normalization is
|
|
190
|
+
* idempotent — an already-resolved absolute path is unchanged — so callers
|
|
191
|
+
* that resolve at the call site are unaffected.
|
|
192
|
+
*
|
|
193
|
+
* A **relative** ceiling is a **defect**, not a typed failure, and is never
|
|
194
|
+
* resolved against the process working directory. Two rules meet here:
|
|
195
|
+
*
|
|
196
|
+
* - Malformed *input* fails typed; bad *wiring* dies. A relative ceiling is
|
|
197
|
+
* a caller-supplied option that is statically wrong at the call site — the
|
|
198
|
+
* same category as a `NaN` `maxDepth` just below, not a recoverable
|
|
199
|
+
* environmental condition — so it dies exactly as that does.
|
|
200
|
+
* - **Do not "upgrade" this to a typed error.** A typed error is only loud
|
|
201
|
+
* if somebody handles it, and `@effected/config-file`'s resolver contract
|
|
202
|
+
* absorbs every failure into `Option.none()`. A typed ceiling rejection
|
|
203
|
+
* would be swallowed there and re-emerge as a clean-looking "no config
|
|
204
|
+
* found" — the silent-wrong-answer failure this whole guard exists to
|
|
205
|
+
* close, reappearing through a third door. `Effect.catch` does not catch
|
|
206
|
+
* defects, so dying is what survives that absorption.
|
|
207
|
+
*
|
|
208
|
+
* Resolving a relative ceiling would be just as bad: the same `stopAt` would
|
|
209
|
+
* name different directories in a lint-staged hook, a CLI run from a package
|
|
210
|
+
* directory, and a test runner, with no way for the caller to see which they
|
|
211
|
+
* got. Rejecting costs one `path.resolve` at the site that knows the answer.
|
|
212
|
+
* It is also why `ascend` reads `process.cwd()` nowhere.
|
|
213
|
+
*
|
|
214
|
+
* Only the CEILING is constrained — a relative `start` is fine and ascends
|
|
215
|
+
* to the relative root. Absoluteness is judged by the injected `Path`
|
|
216
|
+
* service, so the win32 layer accepts `C:\repo` and posix does not.
|
|
217
|
+
*
|
|
218
|
+
* Normalization governs the comparison only: the chain `ascend` returns is
|
|
219
|
+
* still the lexical one derived from `start`, unrewritten. A ceiling naming
|
|
220
|
+
* no ancestor of `start` never matches and the ascent runs to the
|
|
221
|
+
* filesystem root.
|
|
222
|
+
*/
|
|
86
223
|
readonly stopAt?: string;
|
|
87
224
|
/** Hard cap on chain length. Defaults to 256. */
|
|
88
225
|
readonly maxDepth?: number;
|
|
@@ -99,5 +236,5 @@ declare const Walker: {
|
|
|
99
236
|
readonly findRoot: <E, R>(dirs: ReadonlyArray<string>, isRoot: (dir: string) => Effect.Effect<boolean, E, R>) => Effect.Effect<Option.Option<string>, never, R>;
|
|
100
237
|
};
|
|
101
238
|
//#endregion
|
|
102
|
-
export { type AscendOptions, DescendError, type DescendOptions, Walker, descend };
|
|
239
|
+
export { type AscendOptions, type CompileAndExpandOptions, DescendError, type DescendOptions, GlobExpansionError, Walker, compileAndExpand, descend };
|
|
103
240
|
//# sourceMappingURL=index.d.ts.map
|
package/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DescendError, descend } from "./Descend.js";
|
|
2
|
+
import { GlobExpansionError, compileAndExpand } from "./Expand.js";
|
|
2
3
|
import { Walker } from "./Walker.js";
|
|
3
4
|
|
|
4
|
-
export { DescendError, Walker, descend };
|
|
5
|
+
export { DescendError, GlobExpansionError, Walker, compileAndExpand, descend };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/walker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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,7 +37,7 @@
|
|
|
37
37
|
"./package.json": "./package.json"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"@effected/glob": "0.
|
|
40
|
+
"@effected/glob": "0.2.0",
|
|
41
41
|
"effect": "4.0.0-beta.99"
|
|
42
42
|
},
|
|
43
43
|
"engines": {
|