@effected/walker 0.20.0 → 0.21.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 +3 -6
- package/Descend.js +56 -126
- package/DescendSync.js +75 -0
- package/Walker.js +45 -40
- package/index.d.ts +228 -16
- package/index.js +3 -2
- package/internal/descendMachine.js +240 -0
- package/internal/trimTrailing.js +13 -0
- package/node-sync.d.ts +89 -0
- package/node-sync.js +55 -0
- package/package.json +7 -2
- package/tsdoc-metadata.json +1 -1
package/Confine.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolveSegments } from "./internal/segments.js";
|
|
2
|
+
import { trimTrailing } from "./internal/trimTrailing.js";
|
|
2
3
|
import { Effect, Result, Schema } from "effect";
|
|
3
4
|
|
|
4
5
|
// src/Confine.ts
|
|
@@ -71,12 +72,8 @@ const DOTS_AND_SPACES = /^[. ]+$/;
|
|
|
71
72
|
const DEVICE_NAME = /^(?:con|prn|aux|nul|com[0-9\u00b9\u00b2\u00b3]|lpt[0-9\u00b9\u00b2\u00b3]|conin\$|conout\$) *(?:\..*)?$/i;
|
|
72
73
|
/** Whether win32 reads a normalised segment as something other than a plain entry. */
|
|
73
74
|
const isReservedSegment = (segment) => segment.includes(":") || DOTS_AND_SPACES.test(segment) || DEVICE_NAME.test(segment);
|
|
74
|
-
/** `path` without trailing `/` or
|
|
75
|
-
const stripTrailingSeparators = (path) =>
|
|
76
|
-
let end = path.length;
|
|
77
|
-
while (end > 0 && (path[end - 1] === "/" || path[end - 1] === "\\")) end -= 1;
|
|
78
|
-
return path.slice(0, end);
|
|
79
|
-
};
|
|
75
|
+
/** `path` without trailing `/` or `\`, by a loop: `/[\\/]+$/` backtracks quadratically on a long separator run. */
|
|
76
|
+
const stripTrailingSeparators = (path) => trimTrailing(path, (character) => character === "/" || character === "\\");
|
|
80
77
|
/**
|
|
81
78
|
* Confine an untrusted relative path to a base directory, synchronously.
|
|
82
79
|
*
|
package/Descend.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { descendMachine, failureOf } from "./internal/descendMachine.js";
|
|
2
2
|
import { Effect, FileSystem, Path, Schema } from "effect";
|
|
3
3
|
|
|
4
4
|
// src/Descend.ts
|
|
@@ -25,140 +25,70 @@ var DescendError = class extends Schema.TaggedError()("DescendError", {
|
|
|
25
25
|
return this.reason === "depthExceeded" ? `glob descent for ${JSON.stringify(this.pattern)} descended past ${this.limit ?? "the depth cap"} levels below ${where}` : `glob descent for ${JSON.stringify(this.pattern)} could not read ${where}`;
|
|
26
26
|
}
|
|
27
27
|
};
|
|
28
|
-
|
|
29
|
-
const DEFAULT_PRUNE = ["node_modules", ".git"];
|
|
28
|
+
const kindOf = (info) => info.type === "File" ? "File" : info.type === "Directory" ? "Directory" : "Other";
|
|
30
29
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* root).
|
|
30
|
+
* The `Effect` driver of the shared walk (`internal/descendMachine.ts`): each
|
|
31
|
+
* request the walk yields is answered through core's `FileSystem`, joins
|
|
32
|
+
* through `Path`. Every rule lives in the machine; the sync driver in
|
|
33
|
+
* `DescendSync.ts` answers the same requests from a synchronous port.
|
|
36
34
|
*/
|
|
37
|
-
const
|
|
38
|
-
/** Shared empty ancestor chain: frames under `followSymlinks: false` never consult theirs. */
|
|
39
|
-
const NO_ANCESTORS = Object.freeze([]);
|
|
40
|
-
/** Not exported — `descend`'s overloads below carry the public documentation. */
|
|
41
|
-
const descendImpl = Effect.fn("Walker.descend")(function* (pattern, options) {
|
|
35
|
+
const walk = (pattern, options) => Effect.gen(function* () {
|
|
42
36
|
const fs = yield* FileSystem.FileSystem;
|
|
43
37
|
const path = yield* Path.Path;
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return Effect.succeed(void 0);
|
|
82
|
-
}));
|
|
83
|
-
/** Wrap the walk's success value per `onUnreadable`: a plain array unless "record" asked for the pair. */
|
|
84
|
-
const finish = (matches) => onUnreadable === "record" ? {
|
|
38
|
+
const read = (effect) => effect.pipe(Effect.map((value) => ({
|
|
39
|
+
_tag: "Ok",
|
|
40
|
+
value
|
|
41
|
+
})), Effect.catch((error) => Effect.succeed(error.reason._tag === "NotFound" ? { _tag: "NotFound" } : {
|
|
42
|
+
_tag: "Failed",
|
|
43
|
+
cause: error
|
|
44
|
+
})));
|
|
45
|
+
const answer = (request) => {
|
|
46
|
+
switch (request._tag) {
|
|
47
|
+
case "Stat": return fs.stat(request.path).pipe(Effect.map((info) => ({
|
|
48
|
+
kind: kindOf(info),
|
|
49
|
+
info
|
|
50
|
+
})), Effect.orElseSucceed(() => void 0));
|
|
51
|
+
case "Lstat": return fs.lstat(request.path).pipe(Effect.map((info) => info.type === "SymbolicLink" ? { kind: "SymbolicLink" } : {
|
|
52
|
+
kind: kindOf(info),
|
|
53
|
+
info
|
|
54
|
+
}), Effect.orElseSucceed(() => void 0));
|
|
55
|
+
case "RealPath": return read(fs.realPath(request.path));
|
|
56
|
+
case "ReadDirectory": return read(fs.readDirectory(request.path));
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const machine = descendMachine(pattern, options, (a, b) => path.join(a, b));
|
|
60
|
+
let step = machine.next();
|
|
61
|
+
while (step.done !== true) step = machine.next(yield* answer(step.value));
|
|
62
|
+
const result = step.value;
|
|
63
|
+
if (result._tag === "Defect") return yield* Effect.die(new Error(result.message));
|
|
64
|
+
if (result._tag === "Fail") return yield* new DescendError(failureOf(pattern, result));
|
|
65
|
+
return {
|
|
66
|
+
entries: result.entries,
|
|
67
|
+
unreadable: result.unreadable
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
/** Not exported — `descend`'s overloads below carry the public documentation. */
|
|
71
|
+
const descendImpl = Effect.fn("Walker.descend")(function* (pattern, options) {
|
|
72
|
+
const { entries, unreadable } = yield* walk(pattern, options);
|
|
73
|
+
const matches = entries.map((entry) => entry.path);
|
|
74
|
+
return options.onUnreadable === "record" ? {
|
|
85
75
|
matches,
|
|
86
76
|
unreadable
|
|
87
77
|
} : matches;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
let baseAncestors = NO_ANCESTORS;
|
|
97
|
-
if (followSymlinks) {
|
|
98
|
-
const real = yield* realPathOf(absoluteBase, base);
|
|
99
|
-
if (real === void 0) return finish([]);
|
|
100
|
-
baseAncestors = [real];
|
|
101
|
-
}
|
|
102
|
-
const deep = pattern.crossesSegments || pattern.negated;
|
|
103
|
-
const results = [];
|
|
104
|
-
const frames = [{
|
|
105
|
-
relative: base,
|
|
106
|
-
absolute: absoluteBase,
|
|
107
|
-
depth: 0,
|
|
108
|
-
ancestors: baseAncestors
|
|
109
|
-
}];
|
|
110
|
-
for (let head = 0; head < frames.length; head += 1) {
|
|
111
|
-
const frame = frames[head];
|
|
112
|
-
if (frame === void 0) break;
|
|
113
|
-
const entries = yield* fs.readDirectory(frame.absolute).pipe(Effect.catch((error) => {
|
|
114
|
-
if (error.reason._tag === "NotFound") return Effect.succeed([]);
|
|
115
|
-
if (onUnreadable === "fail") return Effect.fail(new DescendError({
|
|
116
|
-
pattern: pattern.source,
|
|
117
|
-
reason: "unreadableDirectory",
|
|
118
|
-
path: frame.relative
|
|
119
|
-
}));
|
|
120
|
-
if (onUnreadable === "record") unreadable.push({
|
|
121
|
-
path: frame.relative,
|
|
122
|
-
cause: error
|
|
123
|
-
});
|
|
124
|
-
return Effect.succeed([]);
|
|
125
|
-
}));
|
|
126
|
-
for (const entry of entries) {
|
|
127
|
-
const relative = frame.relative === "" ? entry : `${frame.relative}/${entry}`;
|
|
128
|
-
const absolute = path.join(frame.absolute, entry);
|
|
129
|
-
const kind = yield* typeOf(absolute);
|
|
130
|
-
if (kind === "File") {
|
|
131
|
-
if (pattern.matches(relative)) results.push(relative);
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
if (kind !== "Directory" || !deep) continue;
|
|
135
|
-
if (prune.has(entry)) continue;
|
|
136
|
-
let ancestors = frame.ancestors;
|
|
137
|
-
if (followSymlinks) {
|
|
138
|
-
const parentReal = frame.ancestors[frame.ancestors.length - 1] ?? frame.absolute;
|
|
139
|
-
const real = (yield* isSymbolicLink(absolute)) ? yield* realPathOf(absolute, relative) : path.join(parentReal, entry);
|
|
140
|
-
if (real === void 0 || frame.ancestors.includes(real)) continue;
|
|
141
|
-
ancestors = [...frame.ancestors, real];
|
|
142
|
-
} else if (yield* isSymbolicLink(absolute)) continue;
|
|
143
|
-
if (frame.depth + 1 > maxDepth) return yield* new DescendError({
|
|
144
|
-
pattern: pattern.source,
|
|
145
|
-
reason: "depthExceeded",
|
|
146
|
-
path: frame.relative,
|
|
147
|
-
limit: maxDepth
|
|
148
|
-
});
|
|
149
|
-
frames.push({
|
|
150
|
-
relative,
|
|
151
|
-
absolute,
|
|
152
|
-
depth: frame.depth + 1,
|
|
153
|
-
ancestors
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
results.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
158
|
-
return finish(results);
|
|
78
|
+
});
|
|
79
|
+
/** Not exported — `descendEntries`' overloads below carry the public documentation. */
|
|
80
|
+
const descendEntriesImpl = Effect.fn("Walker.descendEntries")(function* (pattern, options) {
|
|
81
|
+
const { entries, unreadable } = yield* walk(pattern, options);
|
|
82
|
+
return options.onUnreadable === "record" ? {
|
|
83
|
+
matches: entries,
|
|
84
|
+
unreadable
|
|
85
|
+
} : entries;
|
|
159
86
|
});
|
|
160
87
|
function descend(pattern, options) {
|
|
161
88
|
return descendImpl(pattern, options);
|
|
162
89
|
}
|
|
90
|
+
function descendEntries(pattern, options) {
|
|
91
|
+
return descendEntriesImpl(pattern, options);
|
|
92
|
+
}
|
|
163
93
|
|
|
164
|
-
export { DescendError, descend };
|
|
94
|
+
export { DescendError, descend, descendEntries };
|
package/DescendSync.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { descendMachine, failureOf } from "./internal/descendMachine.js";
|
|
2
|
+
import { DescendError } from "./Descend.js";
|
|
3
|
+
import { Result } from "effect";
|
|
4
|
+
|
|
5
|
+
// src/DescendSync.ts
|
|
6
|
+
const isNotFound = (error) => typeof error === "object" && error !== null && error.code === "ENOENT";
|
|
7
|
+
const kindOf = (stats) => stats.isFile() ? "File" : stats.isDirectory() ? "Directory" : "Other";
|
|
8
|
+
/** The sync driver of the shared walk. Throws only for a defect (an invalid `maxDepth`). */
|
|
9
|
+
const walkSync = (pattern, options) => {
|
|
10
|
+
const { fileSystem, path } = options;
|
|
11
|
+
const read = (f) => {
|
|
12
|
+
try {
|
|
13
|
+
return {
|
|
14
|
+
_tag: "Ok",
|
|
15
|
+
value: f()
|
|
16
|
+
};
|
|
17
|
+
} catch (cause) {
|
|
18
|
+
return isNotFound(cause) ? { _tag: "NotFound" } : {
|
|
19
|
+
_tag: "Failed",
|
|
20
|
+
cause
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const answer = (request) => {
|
|
25
|
+
switch (request._tag) {
|
|
26
|
+
case "Stat": try {
|
|
27
|
+
const info = fileSystem.stat(request.path);
|
|
28
|
+
return {
|
|
29
|
+
kind: kindOf(info),
|
|
30
|
+
info
|
|
31
|
+
};
|
|
32
|
+
} catch {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
case "Lstat": try {
|
|
36
|
+
const info = fileSystem.lstat(request.path);
|
|
37
|
+
return info.isSymbolicLink() ? { kind: "SymbolicLink" } : {
|
|
38
|
+
kind: kindOf(info),
|
|
39
|
+
info
|
|
40
|
+
};
|
|
41
|
+
} catch {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
case "RealPath": return read(() => fileSystem.realPath(request.path));
|
|
45
|
+
case "ReadDirectory": return read(() => fileSystem.readDirectory(request.path));
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const machine = descendMachine(pattern, options, (a, b) => path.join(a, b));
|
|
49
|
+
let step = machine.next();
|
|
50
|
+
while (step.done !== true) step = machine.next(answer(step.value));
|
|
51
|
+
const result = step.value;
|
|
52
|
+
if (result._tag === "Defect") throw new Error(result.message);
|
|
53
|
+
if (result._tag === "Fail") return Result.fail(new DescendError(failureOf(pattern, result)));
|
|
54
|
+
return Result.succeed({
|
|
55
|
+
entries: result.entries,
|
|
56
|
+
unreadable: result.unreadable
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
function descendSync(pattern, options) {
|
|
60
|
+
return Result.map(walkSync(pattern, options), ({ entries, unreadable }) => {
|
|
61
|
+
const matches = entries.map((entry) => entry.path);
|
|
62
|
+
return options.onUnreadable === "record" ? {
|
|
63
|
+
matches,
|
|
64
|
+
unreadable
|
|
65
|
+
} : matches;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function descendEntriesSync(pattern, options) {
|
|
69
|
+
return Result.map(walkSync(pattern, options), ({ entries, unreadable }) => options.onUnreadable === "record" ? {
|
|
70
|
+
matches: entries,
|
|
71
|
+
unreadable
|
|
72
|
+
} : entries);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export { descendEntriesSync, descendSync };
|
package/Walker.js
CHANGED
|
@@ -1,32 +1,11 @@
|
|
|
1
1
|
import { Effect, FileSystem, Option, Path } from "effect";
|
|
2
2
|
|
|
3
3
|
// src/Walker.ts
|
|
4
|
-
const ascend = (start, options) => Effect.gen(function* () {
|
|
5
|
-
const path = yield* Path.Path;
|
|
6
|
-
const maxDepth = options?.maxDepth ?? 256;
|
|
7
|
-
if (!Number.isInteger(maxDepth) || maxDepth < 1) return yield* Effect.die(/* @__PURE__ */ new Error(`Walker.ascend: maxDepth must be a positive integer, received ${maxDepth}`));
|
|
8
|
-
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)})`));
|
|
9
|
-
const ceiling = options?.stopAt === void 0 ? void 0 : path.resolve(options.stopAt);
|
|
10
|
-
const dirs = [];
|
|
11
|
-
let current = start;
|
|
12
|
-
for (let depth = 0; depth < maxDepth; depth++) {
|
|
13
|
-
dirs.push(current);
|
|
14
|
-
if (ceiling !== void 0 && (current === ceiling || path.resolve(current) === ceiling)) break;
|
|
15
|
-
const parent = path.dirname(current);
|
|
16
|
-
if (parent === current) break;
|
|
17
|
-
current = parent;
|
|
18
|
-
}
|
|
19
|
-
return dirs;
|
|
20
|
-
});
|
|
21
|
-
const ascendWithin = (start, ceiling, options) => Effect.gen(function* () {
|
|
22
|
-
if (Option.isNone(ceiling)) return yield* ascend(start, options);
|
|
23
|
-
return yield* ascendToPhysical(start, ceiling.value, options);
|
|
24
|
-
});
|
|
25
4
|
const ascendToPhysical = (start, ceiling, options) => Effect.gen(function* () {
|
|
26
5
|
const fs = yield* FileSystem.FileSystem;
|
|
27
6
|
const path = yield* Path.Path;
|
|
28
7
|
if (!path.isAbsolute(ceiling)) return yield* Effect.die(/* @__PURE__ */ new Error(`Walker.ascendWithin: ceiling must be an absolute path, received ${JSON.stringify(ceiling)} (ascending from ${JSON.stringify(start)})`));
|
|
29
|
-
const dirs = yield* ascend(start, {
|
|
8
|
+
const dirs = yield* Walker.ascend(start, {
|
|
30
9
|
...options,
|
|
31
10
|
stopAt: ceiling
|
|
32
11
|
});
|
|
@@ -34,27 +13,16 @@ const ascendToPhysical = (start, ceiling, options) => Effect.gen(function* () {
|
|
|
34
13
|
if (last !== void 0 && path.resolve(last) === path.resolve(ceiling)) return dirs;
|
|
35
14
|
const physical = yield* Effect.option(fs.realPath(ceiling));
|
|
36
15
|
if (Option.isNone(physical)) return dirs;
|
|
37
|
-
const hit = yield* findRoot(dirs, (dir) => Effect.map(fs.realPath(dir), (real) => real === physical.value));
|
|
16
|
+
const hit = yield* Walker.findRoot(dirs, (dir) => Effect.map(fs.realPath(dir), (real) => real === physical.value));
|
|
38
17
|
return Option.isSome(hit) ? dirs.slice(0, dirs.indexOf(hit.value) + 1) : dirs;
|
|
39
18
|
});
|
|
40
|
-
const firstMatch = (candidates, predicate) => Effect.gen(function* () {
|
|
41
|
-
for (const candidate of candidates) if (yield* Effect.catch(predicate(candidate), () => Effect.succeed(false))) return Option.some(candidate);
|
|
42
|
-
return Option.none();
|
|
43
|
-
});
|
|
44
|
-
const findUpward = (dirs, candidatesFor) => Effect.gen(function* () {
|
|
45
|
-
const fs = yield* FileSystem.FileSystem;
|
|
46
|
-
const candidates = [];
|
|
47
|
-
for (const dir of dirs) candidates.push(...candidatesFor(dir));
|
|
48
|
-
return yield* firstMatch(candidates, (candidate) => fs.exists(candidate));
|
|
49
|
-
});
|
|
50
|
-
const findRoot = (dirs, isRoot) => firstMatch(dirs, isRoot);
|
|
51
19
|
/**
|
|
52
20
|
* Upward path traversal primitives: the directory chain from a start path to the
|
|
53
21
|
* root, and first-match scans over it.
|
|
54
22
|
*
|
|
55
23
|
* @public
|
|
56
24
|
*/
|
|
57
|
-
var Walker = class {
|
|
25
|
+
var Walker = class Walker {
|
|
58
26
|
constructor() {}
|
|
59
27
|
/**
|
|
60
28
|
* Ascend from `start` toward the filesystem root, yielding each directory,
|
|
@@ -90,7 +58,25 @@ var Walker = class {
|
|
|
90
58
|
* // Requires `Path` from the platform layer.
|
|
91
59
|
* ```
|
|
92
60
|
*/
|
|
93
|
-
static ascend
|
|
61
|
+
static ascend(start, options) {
|
|
62
|
+
return Effect.gen(function* () {
|
|
63
|
+
const path = yield* Path.Path;
|
|
64
|
+
const maxDepth = options?.maxDepth ?? 256;
|
|
65
|
+
if (!Number.isInteger(maxDepth) || maxDepth < 1) return yield* Effect.die(/* @__PURE__ */ new Error(`Walker.ascend: maxDepth must be a positive integer, received ${maxDepth}`));
|
|
66
|
+
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)})`));
|
|
67
|
+
const ceiling = options?.stopAt === void 0 ? void 0 : path.resolve(options.stopAt);
|
|
68
|
+
const dirs = [];
|
|
69
|
+
let current = start;
|
|
70
|
+
for (let depth = 0; depth < maxDepth; depth++) {
|
|
71
|
+
dirs.push(current);
|
|
72
|
+
if (ceiling !== void 0 && (current === ceiling || path.resolve(current) === ceiling)) break;
|
|
73
|
+
const parent = path.dirname(current);
|
|
74
|
+
if (parent === current) break;
|
|
75
|
+
current = parent;
|
|
76
|
+
}
|
|
77
|
+
return dirs;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
94
80
|
/**
|
|
95
81
|
* {@link Walker.ascend} bounded by a PHYSICAL ceiling: stops at the nearest
|
|
96
82
|
* directory whose real path is the ceiling's real path, inclusive.
|
|
@@ -118,7 +104,12 @@ var Walker = class {
|
|
|
118
104
|
* A present ceiling must be absolute; a relative one **dies**, for the
|
|
119
105
|
* reasons {@link AscendOptions.stopAt} gives.
|
|
120
106
|
*/
|
|
121
|
-
static ascendWithin
|
|
107
|
+
static ascendWithin(start, ceiling, options) {
|
|
108
|
+
return Effect.gen(function* () {
|
|
109
|
+
if (Option.isNone(ceiling)) return yield* Walker.ascend(start, options);
|
|
110
|
+
return yield* ascendToPhysical(start, ceiling.value, options);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
122
113
|
/**
|
|
123
114
|
* The first candidate whose `predicate` reports true, or `Option.none()`.
|
|
124
115
|
*
|
|
@@ -133,7 +124,12 @@ var Walker = class {
|
|
|
133
124
|
* programmer error and surfaces as a defect. Do not change this to
|
|
134
125
|
* `Effect.catchCause` — the distinction is load-bearing.
|
|
135
126
|
*/
|
|
136
|
-
static firstMatch
|
|
127
|
+
static firstMatch(candidates, predicate) {
|
|
128
|
+
return Effect.gen(function* () {
|
|
129
|
+
for (const candidate of candidates) if (yield* Effect.catch(predicate(candidate), () => Effect.succeed(false))) return Option.some(candidate);
|
|
130
|
+
return Option.none();
|
|
131
|
+
});
|
|
132
|
+
}
|
|
137
133
|
/**
|
|
138
134
|
* The first existing path among the candidates `candidatesFor` produces for each
|
|
139
135
|
* directory in `dirs`, scanned in order. Nearer directories win.
|
|
@@ -142,7 +138,14 @@ var Walker = class {
|
|
|
142
138
|
* Candidates materialize up front, bounded by `dirs.length × candidatesFor`'s
|
|
143
139
|
* output — a few hundred strings under the default `maxDepth`.
|
|
144
140
|
*/
|
|
145
|
-
static findUpward
|
|
141
|
+
static findUpward(dirs, candidatesFor) {
|
|
142
|
+
return Effect.gen(function* () {
|
|
143
|
+
const fs = yield* FileSystem.FileSystem;
|
|
144
|
+
const candidates = [];
|
|
145
|
+
for (const dir of dirs) candidates.push(...candidatesFor(dir));
|
|
146
|
+
return yield* Walker.firstMatch(candidates, (candidate) => fs.exists(candidate));
|
|
147
|
+
});
|
|
148
|
+
}
|
|
146
149
|
/**
|
|
147
150
|
* The first directory in `dirs` that `isRoot` accepts.
|
|
148
151
|
*
|
|
@@ -151,7 +154,9 @@ var Walker = class {
|
|
|
151
154
|
* identity. `isRoot` is a caller-supplied marker test (a `.git` entry, a
|
|
152
155
|
* `pnpm-workspace.yaml`), and its failures are absorbed per directory.
|
|
153
156
|
*/
|
|
154
|
-
static findRoot
|
|
157
|
+
static findRoot(dirs, isRoot) {
|
|
158
|
+
return Walker.firstMatch(dirs, isRoot);
|
|
159
|
+
}
|
|
155
160
|
};
|
|
156
161
|
|
|
157
162
|
export { Walker };
|
package/index.d.ts
CHANGED
|
@@ -145,6 +145,23 @@ interface DescendOptions {
|
|
|
145
145
|
readonly maxDepth?: number;
|
|
146
146
|
/** Directory names never descended into. Defaults to `["node_modules", ".git"]`; a custom list replaces the default. */
|
|
147
147
|
readonly prune?: ReadonlyArray<string>;
|
|
148
|
+
/**
|
|
149
|
+
* Marker FILE names that make a directory a boundary — a nested unit (another
|
|
150
|
+
* package, say: `["package.json"]`) whose contents belong to a separate walk.
|
|
151
|
+
*
|
|
152
|
+
* @remarks
|
|
153
|
+
* A directory strictly below `cwd` holding a FILE with one of these names
|
|
154
|
+
* contributes nothing: none of its files match and its subtree is never
|
|
155
|
+
* descended. `cwd` itself is never a boundary, and a DIRECTORY named like a
|
|
156
|
+
* marker does not count. The rule is about where a file sits, not how the
|
|
157
|
+
* walk reached it, so it applies to a walk that starts below `cwd` (the
|
|
158
|
+
* pattern's literal prefix) and to a literal pattern alike: a boundary
|
|
159
|
+
* between `cwd` and the walk base, or between `cwd` and a literal file,
|
|
160
|
+
* also answers zero matches. Each directory pays one stat per marker it
|
|
161
|
+
* actually lists. Defaults to none. Not to be confused with
|
|
162
|
+
* `Walker.ascend`'s `stopAt`, an inclusive upward ceiling.
|
|
163
|
+
*/
|
|
164
|
+
readonly boundaries?: ReadonlyArray<string>;
|
|
148
165
|
/**
|
|
149
166
|
* What an unreadable directory mid-walk does. `"fail"` (the default) fails
|
|
150
167
|
* typed — downward enumeration must not silently swallow a subtree, or the
|
|
@@ -158,7 +175,9 @@ interface DescendOptions {
|
|
|
158
175
|
readonly onUnreadable?: "fail" | "skip";
|
|
159
176
|
/**
|
|
160
177
|
* Whether to descend into symlinked directories. Defaults to `false`: a
|
|
161
|
-
* symlinked directory is never entered (cycle safety)
|
|
178
|
+
* symlinked directory is never entered (cycle safety) — the walk base, and
|
|
179
|
+
* every directory on the way to it, included: a base reached through a link
|
|
180
|
+
* is an empty result. Under `true` links
|
|
162
181
|
* are followed with the cycle guard kept underneath, in `@actions/glob`'s
|
|
163
182
|
* `traversalChain` semantics: each descended directory records its real
|
|
164
183
|
* path on its own branch's ancestor chain (only the base and each link pay
|
|
@@ -208,7 +227,7 @@ interface DescendRecordOptions extends Omit<DescendOptions, "onUnreadable"> {
|
|
|
208
227
|
*
|
|
209
228
|
* @public
|
|
210
229
|
*/
|
|
211
|
-
interface UnreadableDirectory {
|
|
230
|
+
interface UnreadableDirectory<C = PlatformError.PlatformError> {
|
|
212
231
|
/**
|
|
213
232
|
* The directory's path relative to `cwd`, POSIX separators.
|
|
214
233
|
*
|
|
@@ -220,7 +239,7 @@ interface UnreadableDirectory {
|
|
|
220
239
|
*/
|
|
221
240
|
readonly path: string;
|
|
222
241
|
/** The `readDirectory` (or, for a link under `followSymlinks`, `realPath`) failure, never `NotFound` (a vanished directory is a benign race and is not recorded). */
|
|
223
|
-
readonly cause:
|
|
242
|
+
readonly cause: C;
|
|
224
243
|
}
|
|
225
244
|
/**
|
|
226
245
|
* `descend`'s success value under `onUnreadable: "record"`: the matched
|
|
@@ -232,11 +251,11 @@ interface UnreadableDirectory {
|
|
|
232
251
|
*
|
|
233
252
|
* @public
|
|
234
253
|
*/
|
|
235
|
-
interface DescendResult {
|
|
254
|
+
interface DescendResult<C = PlatformError.PlatformError> {
|
|
236
255
|
/** Matching FILE paths relative to `cwd`, POSIX separators, sorted — identical in shape to the `"fail"`/`"skip"` success value. */
|
|
237
256
|
readonly matches: ReadonlyArray<string>;
|
|
238
257
|
/** Directories that could not be read, each with its cause, in walk order. */
|
|
239
|
-
readonly unreadable: ReadonlyArray<UnreadableDirectory
|
|
258
|
+
readonly unreadable: ReadonlyArray<UnreadableDirectory<C>>;
|
|
240
259
|
}
|
|
241
260
|
declare const DescendError_base: Schema.Class<DescendError, Schema.TaggedStruct<"DescendError", {
|
|
242
261
|
/** The glob pattern's source text. */
|
|
@@ -259,6 +278,35 @@ declare const DescendError_base: Schema.Class<DescendError, Schema.TaggedStruct<
|
|
|
259
278
|
export declare class DescendError extends DescendError_base {
|
|
260
279
|
get message(): string;
|
|
261
280
|
}
|
|
281
|
+
/**
|
|
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
|
|
284
|
+
* classify it, and this keeps the answer instead of discarding it.
|
|
285
|
+
*
|
|
286
|
+
* The type parameter is the stat's type, so the sync walk reuses this shape
|
|
287
|
+
* with its port's stats.
|
|
288
|
+
*
|
|
289
|
+
* @public
|
|
290
|
+
*/
|
|
291
|
+
interface DescendEntry<I = FileSystem.File.Info> {
|
|
292
|
+
/** The file's path relative to `cwd`, POSIX separators. */
|
|
293
|
+
readonly path: string;
|
|
294
|
+
/** `FileSystem.stat` of the file — links followed, so a symlinked file reports its target. */
|
|
295
|
+
readonly info: I;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* `descendEntries`' success value under `onUnreadable: "record"`: the matched
|
|
299
|
+
* {@link DescendEntry} values plus every {@link UnreadableDirectory}, exactly
|
|
300
|
+
* as {@link DescendResult} carries them for `descend`.
|
|
301
|
+
*
|
|
302
|
+
* @public
|
|
303
|
+
*/
|
|
304
|
+
interface DescendEntriesResult<I = FileSystem.File.Info, C = PlatformError.PlatformError> {
|
|
305
|
+
/** Matching files with their stats, sorted by path. */
|
|
306
|
+
readonly matches: ReadonlyArray<DescendEntry<I>>;
|
|
307
|
+
/** Directories that could not be read, each with its cause, in walk order. */
|
|
308
|
+
readonly unreadable: ReadonlyArray<UnreadableDirectory<C>>;
|
|
309
|
+
}
|
|
262
310
|
/**
|
|
263
311
|
* Expand a compiled glob pattern against the filesystem under
|
|
264
312
|
* `onUnreadable: "record"`, resolving to a {@link DescendResult} — the
|
|
@@ -277,11 +325,16 @@ export declare function descend(pattern: GlobPattern, options: DescendRecordOpti
|
|
|
277
325
|
* FILE paths relative to `cwd` (POSIX separators), sorted by relative path.
|
|
278
326
|
*
|
|
279
327
|
* @remarks
|
|
280
|
-
* A
|
|
328
|
+
* A path is answered in the spelling the pattern matched it under: `./src/*.ts`
|
|
329
|
+
* answers `./src/a.ts`, and `src/*.ts` answers `src/a.ts`. A file several
|
|
330
|
+
* brace alternatives match under different spellings (`{src,./src}/*.ts`) is
|
|
331
|
+
* answered once, in the spelling of the earliest of them in pattern order. A
|
|
332
|
+
* literal pattern (no magic, not negated) fast-paths to a single stat: the
|
|
281
333
|
* result is `[source]` when it resolves to a file, `[]` otherwise — a missing
|
|
282
|
-
* path is zero matches, not an error. A magic pattern walks
|
|
283
|
-
*
|
|
284
|
-
*
|
|
334
|
+
* path is zero matches, not an error. A magic pattern walks each of its
|
|
335
|
+
* `GlobPattern.enumerationRoots`, reading each directory once however many
|
|
336
|
+
* alternatives spell it; a NEGATED pattern can match paths outside those
|
|
337
|
+
* roots, so it walks from `cwd` itself. A missing
|
|
285
338
|
* base directory is likewise an empty result, because zero matches is a
|
|
286
339
|
* normal glob answer — as is any pattern that lexically climbs above `cwd`
|
|
287
340
|
* via `..` segments (walked paths never contain `..`). "Never reads outside
|
|
@@ -294,7 +347,7 @@ export declare function descend(pattern: GlobPattern, options: DescendRecordOpti
|
|
|
294
347
|
*
|
|
295
348
|
* Only files match. A symlink counts when it stat-resolves to a file
|
|
296
349
|
* (`FileSystem.stat` follows links, as node's does); a symlinked directory is
|
|
297
|
-
* never descended into by default (cycle safety — detected by
|
|
350
|
+
* never descended into by default (cycle safety — detected by an `lstat`
|
|
298
351
|
* probe), unless `followSymlinks: true` asks for it, which follows links under
|
|
299
352
|
* `@actions/glob`'s `traversalChain` cycle guard: a directory is a cycle only
|
|
300
353
|
* when its real path is already an ancestor of the current branch, so sibling
|
|
@@ -332,6 +385,165 @@ export declare function descend(pattern: GlobPattern, options: DescendRecordOpti
|
|
|
332
385
|
* @public
|
|
333
386
|
*/
|
|
334
387
|
export declare function descend(pattern: GlobPattern, options: DescendOptions): Effect.Effect<ReadonlyArray<string>, DescendError, FileSystem.FileSystem | Path.Path>;
|
|
388
|
+
/**
|
|
389
|
+
* `descend` under `onUnreadable: "record"`, keeping each match's stat:
|
|
390
|
+
* resolves to a {@link DescendEntriesResult}.
|
|
391
|
+
*
|
|
392
|
+
* @public
|
|
393
|
+
*/
|
|
394
|
+
export declare function descendEntries(pattern: GlobPattern, options: DescendRecordOptions): Effect.Effect<DescendEntriesResult, DescendError, FileSystem.FileSystem | Path.Path>;
|
|
395
|
+
/**
|
|
396
|
+
* `descend`, keeping the stat the walk already read for each matched
|
|
397
|
+
* file: resolves to {@link DescendEntry} values sorted by path.
|
|
398
|
+
*
|
|
399
|
+
* @remarks
|
|
400
|
+
* The same walk, the same matches in the same order, the same errors — only
|
|
401
|
+
* the element type differs, and at no extra syscall (the walk stats every
|
|
402
|
+
* entry to classify it). For a cache signature over a tree, map each entry's
|
|
403
|
+
* `path` and `info.mtime` (or `info.size`) into whatever key the cache needs;
|
|
404
|
+
* the kit does not choose the fingerprint.
|
|
405
|
+
*
|
|
406
|
+
* @example
|
|
407
|
+
* ```ts
|
|
408
|
+
* import { GlobPattern, GlobPatternOptions } from "@effected/glob";
|
|
409
|
+
* import { descendEntries } from "@effected/walker";
|
|
410
|
+
* import { Effect, Option } from "effect";
|
|
411
|
+
*
|
|
412
|
+
* const signature = Effect.gen(function* () {
|
|
413
|
+
* const pattern = yield* GlobPattern.compile("**", GlobPatternOptions.make({ dot: true }));
|
|
414
|
+
* const entries = yield* descendEntries(pattern, { cwd: "/repo/src", boundaries: ["package.json"] });
|
|
415
|
+
* return entries.map((e) => `${e.path}:${Option.match(e.info.mtime, { onNone: () => "", onSome: (d) => d.getTime() })}`).join("|");
|
|
416
|
+
* });
|
|
417
|
+
* ```
|
|
418
|
+
*
|
|
419
|
+
* @public
|
|
420
|
+
*/
|
|
421
|
+
export declare function descendEntries(pattern: GlobPattern, options: DescendOptions): Effect.Effect<ReadonlyArray<DescendEntry>, DescendError, FileSystem.FileSystem | Path.Path>;
|
|
422
|
+
/**
|
|
423
|
+
* The stats {@link WalkerSyncFileSystem} answers — a structural subset of
|
|
424
|
+
* `node:fs` `Stats`, so `statSync` / `lstatSync` satisfy it verbatim.
|
|
425
|
+
*
|
|
426
|
+
* @public
|
|
427
|
+
*/
|
|
428
|
+
interface WalkerSyncStats {
|
|
429
|
+
/** Whether the entry is a regular file. */
|
|
430
|
+
isFile(): boolean;
|
|
431
|
+
/** Whether the entry is a directory. */
|
|
432
|
+
isDirectory(): boolean;
|
|
433
|
+
/** Whether the entry is a symbolic link (only ever true for `lstat`). */
|
|
434
|
+
isSymbolicLink(): boolean;
|
|
435
|
+
/** Modification time, epoch milliseconds. */
|
|
436
|
+
readonly mtimeMs: number;
|
|
437
|
+
/** Size in bytes. */
|
|
438
|
+
readonly size: number;
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* The synchronous file operations the sync walk needs, supplied by the
|
|
442
|
+
* consumer — `node:fs` satisfies it (`nodeWalkerFileSystem` from
|
|
443
|
+
* `@effected/walker/node-sync`).
|
|
444
|
+
*
|
|
445
|
+
* @remarks
|
|
446
|
+
* Every member reports failure by throwing a node-shaped error. A `code` of
|
|
447
|
+
* `"ENOENT"` is the benign vanished-path race (empty, never recorded); any
|
|
448
|
+
* other throw from `readDirectory` or `realPath` is an unreadable directory,
|
|
449
|
+
* handled per `onUnreadable`. A throwing `stat` or `lstat` reads as "no
|
|
450
|
+
* entry", exactly as the `Effect` walk reads its failed `stat` and `lstat`.
|
|
451
|
+
*
|
|
452
|
+
* @public
|
|
453
|
+
*/
|
|
454
|
+
interface WalkerSyncFileSystem {
|
|
455
|
+
/** The entry names inside the directory at `path` (`readdirSync`). */
|
|
456
|
+
readonly readDirectory: (path: string) => ReadonlyArray<string>;
|
|
457
|
+
/** Stats of `path`, following links (`statSync`). */
|
|
458
|
+
readonly stat: (path: string) => WalkerSyncStats;
|
|
459
|
+
/** Stats of `path` itself, not following a final link (`lstatSync`). */
|
|
460
|
+
readonly lstat: (path: string) => WalkerSyncStats;
|
|
461
|
+
/** The real path of `path`, links resolved (`realpathSync`). */
|
|
462
|
+
readonly realPath: (path: string) => string;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
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)
|
|
467
|
+
* satisfies it. It shapes only the absolute paths handed to the
|
|
468
|
+
* {@link WalkerSyncFileSystem}; matching is POSIX-relative by the glob
|
|
469
|
+
* contract.
|
|
470
|
+
*
|
|
471
|
+
* @public
|
|
472
|
+
*/
|
|
473
|
+
interface WalkerSyncPath {
|
|
474
|
+
/** Join segments with the implementation's separator (like `path.join`). */
|
|
475
|
+
readonly join: (...segments: ReadonlyArray<string>) => string;
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* The platform half of every sync walk.
|
|
479
|
+
*
|
|
480
|
+
* @public
|
|
481
|
+
*/
|
|
482
|
+
interface WalkerSyncOps {
|
|
483
|
+
/** File operations. */
|
|
484
|
+
readonly fileSystem: WalkerSyncFileSystem;
|
|
485
|
+
/** Path joining. */
|
|
486
|
+
readonly path: WalkerSyncPath;
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Options for `descendSync` / `descendEntriesSync`: every `DescendOptions`
|
|
490
|
+
* field, plus the platform.
|
|
491
|
+
*
|
|
492
|
+
* @public
|
|
493
|
+
*/
|
|
494
|
+
interface DescendSyncOptions extends DescendOptions, WalkerSyncOps {}
|
|
495
|
+
/**
|
|
496
|
+
* {@link DescendSyncOptions} under `onUnreadable: "record"`.
|
|
497
|
+
*
|
|
498
|
+
* @public
|
|
499
|
+
*/
|
|
500
|
+
interface DescendSyncRecordOptions extends DescendRecordOptions, WalkerSyncOps {}
|
|
501
|
+
/**
|
|
502
|
+
* `descend` under `onUnreadable: "record"`, synchronously over a caller-supplied port.
|
|
503
|
+
*
|
|
504
|
+
* @public
|
|
505
|
+
*/
|
|
506
|
+
export declare function descendSync(pattern: GlobPattern, options: DescendSyncRecordOptions): Result.Result<DescendResult<unknown>, DescendError>;
|
|
507
|
+
/**
|
|
508
|
+
* `descend`, synchronously, over a caller-supplied file port: matching FILE
|
|
509
|
+
* paths relative to `cwd` (POSIX separators), sorted.
|
|
510
|
+
*
|
|
511
|
+
* @remarks
|
|
512
|
+
* The same walk as `descend` — it drives the same traversal, so every rule
|
|
513
|
+
* holds identically: the literal fast path, the enumeration prefix, `prune`,
|
|
514
|
+
* `boundaries`, the symlink posture and per-branch cycle guard under
|
|
515
|
+
* `followSymlinks`, `onUnreadable` and the `maxDepth` failure. Failures are a
|
|
516
|
+
* `Result` failure typed as `DescendError`; an invalid `maxDepth` is a defect
|
|
517
|
+
* and throws. For a caller with no Effect runtime (config evaluation, a Node
|
|
518
|
+
* SEA sidecar); inside Effect, use `descend`.
|
|
519
|
+
*
|
|
520
|
+
* @example
|
|
521
|
+
* ```ts
|
|
522
|
+
* import { GlobPattern } from "@effected/glob";
|
|
523
|
+
* import { descendSync } from "@effected/walker";
|
|
524
|
+
* import { nodeWalkerSyncOps } from "@effected/walker/node-sync";
|
|
525
|
+
* import { Result } from "effect";
|
|
526
|
+
*
|
|
527
|
+
* const pattern = Result.getOrThrow(GlobPattern.compileResult("src/**"));
|
|
528
|
+
* const files = descendSync(pattern, { cwd: "/repo", boundaries: ["package.json"], ...nodeWalkerSyncOps });
|
|
529
|
+
* ```
|
|
530
|
+
*
|
|
531
|
+
* @public
|
|
532
|
+
*/
|
|
533
|
+
export declare function descendSync(pattern: GlobPattern, options: DescendSyncOptions): Result.Result<ReadonlyArray<string>, DescendError>;
|
|
534
|
+
/**
|
|
535
|
+
* `descendEntries` under `onUnreadable: "record"`, synchronously over a caller-supplied port.
|
|
536
|
+
*
|
|
537
|
+
* @public
|
|
538
|
+
*/
|
|
539
|
+
export declare function descendEntriesSync(pattern: GlobPattern, options: DescendSyncRecordOptions): Result.Result<DescendEntriesResult<WalkerSyncStats, unknown>, DescendError>;
|
|
540
|
+
/**
|
|
541
|
+
* `descendEntries`, synchronously: `descendSync`'s matches, each with
|
|
542
|
+
* the stat the walk already read for it.
|
|
543
|
+
*
|
|
544
|
+
* @public
|
|
545
|
+
*/
|
|
546
|
+
export declare function descendEntriesSync(pattern: GlobPattern, options: DescendSyncOptions): Result.Result<ReadonlyArray<DescendEntry<WalkerSyncStats>>, DescendError>;
|
|
335
547
|
/**
|
|
336
548
|
* Options for {@link compileAndExpand}: every {@link DescendOptions} field,
|
|
337
549
|
* plus the glob options the pattern compiles under.
|
|
@@ -530,7 +742,7 @@ export declare class Walker {
|
|
|
530
742
|
* // Requires `Path` from the platform layer.
|
|
531
743
|
* ```
|
|
532
744
|
*/
|
|
533
|
-
static
|
|
745
|
+
static ascend(start: string, options?: AscendOptions): Effect.Effect<ReadonlyArray<string>, never, Path.Path>;
|
|
534
746
|
/**
|
|
535
747
|
* {@link Walker.ascend} bounded by a PHYSICAL ceiling: stops at the nearest
|
|
536
748
|
* directory whose real path is the ceiling's real path, inclusive.
|
|
@@ -558,7 +770,7 @@ export declare class Walker {
|
|
|
558
770
|
* A present ceiling must be absolute; a relative one **dies**, for the
|
|
559
771
|
* reasons {@link AscendOptions.stopAt} gives.
|
|
560
772
|
*/
|
|
561
|
-
static
|
|
773
|
+
static ascendWithin(start: string, ceiling: Option.Option<string>, options?: Pick<AscendOptions, "maxDepth">): Effect.Effect<ReadonlyArray<string>, never, FileSystem.FileSystem | Path.Path>;
|
|
562
774
|
/**
|
|
563
775
|
* The first candidate whose `predicate` reports true, or `Option.none()`.
|
|
564
776
|
*
|
|
@@ -573,7 +785,7 @@ export declare class Walker {
|
|
|
573
785
|
* programmer error and surfaces as a defect. Do not change this to
|
|
574
786
|
* `Effect.catchCause` — the distinction is load-bearing.
|
|
575
787
|
*/
|
|
576
|
-
static
|
|
788
|
+
static firstMatch<E, R>(candidates: ReadonlyArray<string>, predicate: (candidate: string) => Effect.Effect<boolean, E, R>): Effect.Effect<Option.Option<string>, never, R>;
|
|
577
789
|
/**
|
|
578
790
|
* The first existing path among the candidates `candidatesFor` produces for each
|
|
579
791
|
* directory in `dirs`, scanned in order. Nearer directories win.
|
|
@@ -582,7 +794,7 @@ export declare class Walker {
|
|
|
582
794
|
* Candidates materialize up front, bounded by `dirs.length × candidatesFor`'s
|
|
583
795
|
* output — a few hundred strings under the default `maxDepth`.
|
|
584
796
|
*/
|
|
585
|
-
static
|
|
797
|
+
static findUpward(dirs: ReadonlyArray<string>, candidatesFor: (dir: string) => ReadonlyArray<string>): Effect.Effect<Option.Option<string>, never, FileSystem.FileSystem>;
|
|
586
798
|
/**
|
|
587
799
|
* The first directory in `dirs` that `isRoot` accepts.
|
|
588
800
|
*
|
|
@@ -591,7 +803,7 @@ export declare class Walker {
|
|
|
591
803
|
* identity. `isRoot` is a caller-supplied marker test (a `.git` entry, a
|
|
592
804
|
* `pnpm-workspace.yaml`), and its failures are absorbed per directory.
|
|
593
805
|
*/
|
|
594
|
-
static
|
|
806
|
+
static findRoot<E, R>(dirs: ReadonlyArray<string>, isRoot: (dir: string) => Effect.Effect<boolean, E, R>): Effect.Effect<Option.Option<string>, never, R>;
|
|
595
807
|
}
|
|
596
|
-
export type { AscendOptions, CompileAndExpandOptions, DescendOptions, DescendRecordOptions, DescendResult, UnreadableDirectory };
|
|
808
|
+
export type { AscendOptions, CompileAndExpandOptions, DescendEntriesResult, DescendEntry, DescendOptions, DescendRecordOptions, DescendResult, DescendSyncOptions, DescendSyncRecordOptions, UnreadableDirectory, WalkerSyncFileSystem, WalkerSyncOps, WalkerSyncPath, WalkerSyncStats };
|
|
597
809
|
//# sourceMappingURL=index.d.ts.map
|
package/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PathEscapeError, PathEscapeReason, confineRelative, confineRelativeResult } from "./Confine.js";
|
|
2
|
-
import { DescendError, descend } from "./Descend.js";
|
|
2
|
+
import { DescendError, descend, descendEntries } from "./Descend.js";
|
|
3
|
+
import { descendEntriesSync, descendSync } from "./DescendSync.js";
|
|
3
4
|
import { GlobExpansionError, compileAndExpand } from "./Expand.js";
|
|
4
5
|
import { Walker } from "./Walker.js";
|
|
5
6
|
|
|
6
|
-
export { DescendError, GlobExpansionError, PathEscapeError, PathEscapeReason, Walker, compileAndExpand, confineRelative, confineRelativeResult, descend };
|
|
7
|
+
export { DescendError, GlobExpansionError, PathEscapeError, PathEscapeReason, Walker, compileAndExpand, confineRelative, confineRelativeResult, descend, descendEntries, descendEntriesSync, descendSync };
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { resolveSegments } from "./segments.js";
|
|
2
|
+
import { trimTrailing } from "./trimTrailing.js";
|
|
3
|
+
|
|
4
|
+
// src/internal/descendMachine.ts
|
|
5
|
+
/** Directory names never descended into unless the caller overrides `prune`. */
|
|
6
|
+
const DEFAULT_PRUNE = ["node_modules", ".git"];
|
|
7
|
+
/** A glob pattern's one separator: its backslash is an escape, never a separator. */
|
|
8
|
+
const isSlash = (character) => character === "/";
|
|
9
|
+
/** Shared empty ancestor chain: frames under `followSymlinks: false` never consult theirs. */
|
|
10
|
+
const NO_ANCESTORS = Object.freeze([]);
|
|
11
|
+
/** The walk. */
|
|
12
|
+
function* descendMachine(pattern, options, join) {
|
|
13
|
+
/** Yields one request; the answer is typed by the request's tag. */
|
|
14
|
+
function* ask(request) {
|
|
15
|
+
return yield request;
|
|
16
|
+
}
|
|
17
|
+
const maxDepth = options.maxDepth ?? 256;
|
|
18
|
+
if (!Number.isInteger(maxDepth) || maxDepth < 1) return {
|
|
19
|
+
_tag: "Defect",
|
|
20
|
+
message: `Walker.descend: maxDepth must be a positive integer, received ${maxDepth}`
|
|
21
|
+
};
|
|
22
|
+
const prune = new Set(options.prune ?? DEFAULT_PRUNE);
|
|
23
|
+
const boundaries = options.boundaries ?? [];
|
|
24
|
+
const onUnreadable = options.onUnreadable ?? "fail";
|
|
25
|
+
const followSymlinks = options.followSymlinks ?? false;
|
|
26
|
+
const unreadable = [];
|
|
27
|
+
const done = (entries) => {
|
|
28
|
+
entries.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
29
|
+
return {
|
|
30
|
+
_tag: "Done",
|
|
31
|
+
entries,
|
|
32
|
+
unreadable
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
const unreadableFail = (path) => ({
|
|
36
|
+
_tag: "Fail",
|
|
37
|
+
reason: "unreadableDirectory",
|
|
38
|
+
path
|
|
39
|
+
});
|
|
40
|
+
/**
|
|
41
|
+
* Whether the directory at `absolute` is a boundary: it holds a marker
|
|
42
|
+
* FILE. `listing`, when the directory was just read, spares the stat for
|
|
43
|
+
* every marker it does not name; a directory named like a marker is not one.
|
|
44
|
+
*/
|
|
45
|
+
function* isBoundary(absolute, listing) {
|
|
46
|
+
for (const marker of boundaries) {
|
|
47
|
+
if (listing !== void 0 && !listing.includes(marker)) continue;
|
|
48
|
+
if ((yield* ask({
|
|
49
|
+
_tag: "Stat",
|
|
50
|
+
path: join(absolute, marker)
|
|
51
|
+
}))?.kind === "File") return true;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Whether the way down `segments` (below `cwd`) is closed: a directory among
|
|
57
|
+
* the first `boundaryCount` is a boundary, or — without followSymlinks,
|
|
58
|
+
* where a symlinked directory is never entered — a link is among the first
|
|
59
|
+
* `directoryCount`. Either way the answer is nothing.
|
|
60
|
+
*/
|
|
61
|
+
function* blocked(segments, directoryCount, boundaryCount) {
|
|
62
|
+
if (boundaries.length === 0 && followSymlinks) return false;
|
|
63
|
+
let absolute = options.cwd;
|
|
64
|
+
for (let index = 0; index < directoryCount; index += 1) {
|
|
65
|
+
absolute = join(absolute, segments[index] ?? "");
|
|
66
|
+
if (index < boundaryCount && (yield* isBoundary(absolute, void 0))) return true;
|
|
67
|
+
if (!followSymlinks && (yield* ask({
|
|
68
|
+
_tag: "Lstat",
|
|
69
|
+
path: absolute
|
|
70
|
+
}))?.kind === "SymbolicLink") return true;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
/** The real path of a directory the cycle guard must identify, or `undefined` when the walk must not enter it. */
|
|
75
|
+
function* realPathOf(absolute, relative) {
|
|
76
|
+
const answer = yield* ask({
|
|
77
|
+
_tag: "RealPath",
|
|
78
|
+
path: absolute
|
|
79
|
+
});
|
|
80
|
+
if (answer._tag === "Ok") return answer.value;
|
|
81
|
+
if (answer._tag === "NotFound") return void 0;
|
|
82
|
+
if (onUnreadable === "fail") return { fail: true };
|
|
83
|
+
if (onUnreadable === "record") unreadable.push({
|
|
84
|
+
path: relative,
|
|
85
|
+
cause: answer.cause
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (!pattern.hasMagic && !pattern.negated && !/[{}]/.test(pattern.source)) {
|
|
89
|
+
const segments = resolveSegments(pattern.source.split("/"));
|
|
90
|
+
if (segments === void 0) return done([]);
|
|
91
|
+
if (yield* blocked(segments, segments.length - 1, segments.length - 1)) return done([]);
|
|
92
|
+
const answer = yield* ask({
|
|
93
|
+
_tag: "Stat",
|
|
94
|
+
path: join(options.cwd, pattern.source)
|
|
95
|
+
});
|
|
96
|
+
return done(answer?.kind === "File" ? [{
|
|
97
|
+
path: pattern.source,
|
|
98
|
+
info: answer.info
|
|
99
|
+
}] : []);
|
|
100
|
+
}
|
|
101
|
+
const results = [];
|
|
102
|
+
/**
|
|
103
|
+
* Walks one root: the literal directory `base` and everything below it a
|
|
104
|
+
* match can lie in — one level, or all of it when `deep`. An absent base,
|
|
105
|
+
* one that climbs above `cwd`, or one behind a boundary or a link is
|
|
106
|
+
* skipped; a `Fail` aborts the walk.
|
|
107
|
+
*
|
|
108
|
+
* The walk itself is canonical (`cwd` is `""`, never `.`), so each directory
|
|
109
|
+
* is read once however many spellings name it. What it answers is spelled
|
|
110
|
+
* the way the pattern spells it: a file matches when its path, re-spelled
|
|
111
|
+
* through one of the root's spellings, matches the pattern, and it is
|
|
112
|
+
* answered ONCE, in the first such spelling — the earliest brace
|
|
113
|
+
* alternative's, since spellings come in pattern order. A directory the
|
|
114
|
+
* walk reports (unreadable, too deep) is spelled through the root's own
|
|
115
|
+
* earliest spelling.
|
|
116
|
+
*/
|
|
117
|
+
function* walkRoot(rawBase, deep, spellings) {
|
|
118
|
+
const spelled = spellings.map((spelling) => {
|
|
119
|
+
const canonical = resolveSegments(spelling.split("/"))?.join("/") ?? "";
|
|
120
|
+
return {
|
|
121
|
+
spelling,
|
|
122
|
+
canonical: canonical === "" ? "" : `${canonical}/`
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
/** The file at canonical `relative`, in the first spelling the pattern matches it under, or `undefined`. */
|
|
126
|
+
const matchedAs = (relative) => {
|
|
127
|
+
for (const { spelling, canonical } of spelled) {
|
|
128
|
+
if (canonical !== "" && !relative.startsWith(canonical)) continue;
|
|
129
|
+
const candidate = spelling + relative.slice(canonical.length);
|
|
130
|
+
if (pattern.matches(candidate)) return candidate;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
const baseSegments = resolveSegments(rawBase.split("/"));
|
|
134
|
+
if (baseSegments === void 0) return void 0;
|
|
135
|
+
const base = baseSegments.join("/");
|
|
136
|
+
const baseCanonical = base === "" ? "" : `${base}/`;
|
|
137
|
+
const own = spelled.find(({ canonical }) => canonical === baseCanonical)?.spelling ?? baseCanonical;
|
|
138
|
+
const shown = (relative) => relative === base ? trimTrailing(own, isSlash) : own + relative.slice(baseCanonical.length);
|
|
139
|
+
const absoluteBase = base === "" ? options.cwd : join(options.cwd, base);
|
|
140
|
+
if ((yield* ask({
|
|
141
|
+
_tag: "Stat",
|
|
142
|
+
path: absoluteBase
|
|
143
|
+
}))?.kind !== "Directory") return void 0;
|
|
144
|
+
if (yield* blocked(baseSegments, baseSegments.length, baseSegments.length - 1)) return void 0;
|
|
145
|
+
let baseAncestors = NO_ANCESTORS;
|
|
146
|
+
if (followSymlinks) {
|
|
147
|
+
const real = yield* realPathOf(absoluteBase, shown(base));
|
|
148
|
+
if (typeof real === "object") return unreadableFail(shown(base));
|
|
149
|
+
if (real === void 0) return void 0;
|
|
150
|
+
baseAncestors = [real];
|
|
151
|
+
}
|
|
152
|
+
const frames = [{
|
|
153
|
+
relative: base,
|
|
154
|
+
absolute: absoluteBase,
|
|
155
|
+
depth: 0,
|
|
156
|
+
ancestors: baseAncestors
|
|
157
|
+
}];
|
|
158
|
+
for (let head = 0; head < frames.length; head += 1) {
|
|
159
|
+
const frame = frames[head];
|
|
160
|
+
if (frame === void 0) break;
|
|
161
|
+
const read = yield* ask({
|
|
162
|
+
_tag: "ReadDirectory",
|
|
163
|
+
path: frame.absolute
|
|
164
|
+
});
|
|
165
|
+
let entries = [];
|
|
166
|
+
if (read._tag === "Ok") entries = read.value;
|
|
167
|
+
else if (read._tag === "Failed") {
|
|
168
|
+
if (onUnreadable === "fail") return unreadableFail(shown(frame.relative));
|
|
169
|
+
if (onUnreadable === "record") unreadable.push({
|
|
170
|
+
path: shown(frame.relative),
|
|
171
|
+
cause: read.cause
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
if (frame.relative !== "" && (yield* isBoundary(frame.absolute, entries))) continue;
|
|
175
|
+
for (const entry of entries) {
|
|
176
|
+
const relative = frame.relative === "" ? entry : `${frame.relative}/${entry}`;
|
|
177
|
+
const absolute = join(frame.absolute, entry);
|
|
178
|
+
const own = yield* ask({
|
|
179
|
+
_tag: "Lstat",
|
|
180
|
+
path: absolute
|
|
181
|
+
});
|
|
182
|
+
const link = own?.kind === "SymbolicLink";
|
|
183
|
+
const stat = link ? yield* ask({
|
|
184
|
+
_tag: "Stat",
|
|
185
|
+
path: absolute
|
|
186
|
+
}) : own;
|
|
187
|
+
if (stat === void 0) continue;
|
|
188
|
+
if (stat.kind === "File") {
|
|
189
|
+
const path = matchedAs(relative);
|
|
190
|
+
if (path !== void 0) results.push({
|
|
191
|
+
path,
|
|
192
|
+
info: stat.info
|
|
193
|
+
});
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (stat.kind !== "Directory" || !deep) continue;
|
|
197
|
+
if (prune.has(entry)) continue;
|
|
198
|
+
let ancestors = frame.ancestors;
|
|
199
|
+
if (followSymlinks) {
|
|
200
|
+
const parentReal = frame.ancestors[frame.ancestors.length - 1] ?? frame.absolute;
|
|
201
|
+
const real = link ? yield* realPathOf(absolute, shown(relative)) : join(parentReal, entry);
|
|
202
|
+
if (typeof real === "object") return unreadableFail(shown(relative));
|
|
203
|
+
if (real === void 0 || frame.ancestors.includes(real)) continue;
|
|
204
|
+
ancestors = [...frame.ancestors, real];
|
|
205
|
+
} else if (link) continue;
|
|
206
|
+
if (frame.depth + 1 > maxDepth) return {
|
|
207
|
+
_tag: "Fail",
|
|
208
|
+
reason: "depthExceeded",
|
|
209
|
+
path: shown(frame.relative),
|
|
210
|
+
limit: maxDepth
|
|
211
|
+
};
|
|
212
|
+
frames.push({
|
|
213
|
+
relative,
|
|
214
|
+
absolute,
|
|
215
|
+
depth: frame.depth + 1,
|
|
216
|
+
ancestors
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const roots = pattern.negated ? [{
|
|
222
|
+
prefix: "",
|
|
223
|
+
deep: true,
|
|
224
|
+
spellings: [""]
|
|
225
|
+
}] : pattern.enumerationRoots;
|
|
226
|
+
for (const root of roots) {
|
|
227
|
+
const failed = yield* walkRoot(trimTrailing(root.prefix, isSlash), root.deep, root.spellings);
|
|
228
|
+
if (failed !== void 0) return failed;
|
|
229
|
+
}
|
|
230
|
+
return done(results);
|
|
231
|
+
}
|
|
232
|
+
/** The typed error a `Fail` result describes — the one conversion both drivers share. */
|
|
233
|
+
const failureOf = (pattern, result) => ({
|
|
234
|
+
pattern: pattern.source,
|
|
235
|
+
reason: result.reason,
|
|
236
|
+
path: result.path,
|
|
237
|
+
...result.limit === void 0 ? {} : { limit: result.limit }
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
export { DEFAULT_PRUNE, descendMachine, failureOf };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// src/internal/trimTrailing.ts
|
|
2
|
+
/**
|
|
3
|
+
* `text` without its trailing run of characters `isTrailing` accepts, in time linear in the run.
|
|
4
|
+
*
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
const trimTrailing = (text, isTrailing) => {
|
|
8
|
+
let end = text.length;
|
|
9
|
+
while (end > 0 && isTrailing(text.charAt(end - 1))) end -= 1;
|
|
10
|
+
return end === text.length ? text : text.slice(0, end);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export { trimTrailing };
|
package/node-sync.d.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import "@effected/glob";
|
|
2
|
+
import "effect";
|
|
3
|
+
/**
|
|
4
|
+
* The stats {@link WalkerSyncFileSystem} answers — a structural subset of
|
|
5
|
+
* `node:fs` `Stats`, so `statSync` / `lstatSync` satisfy it verbatim.
|
|
6
|
+
*
|
|
7
|
+
* @public
|
|
8
|
+
*/
|
|
9
|
+
interface WalkerSyncStats {
|
|
10
|
+
/** Whether the entry is a regular file. */
|
|
11
|
+
isFile(): boolean;
|
|
12
|
+
/** Whether the entry is a directory. */
|
|
13
|
+
isDirectory(): boolean;
|
|
14
|
+
/** Whether the entry is a symbolic link (only ever true for `lstat`). */
|
|
15
|
+
isSymbolicLink(): boolean;
|
|
16
|
+
/** Modification time, epoch milliseconds. */
|
|
17
|
+
readonly mtimeMs: number;
|
|
18
|
+
/** Size in bytes. */
|
|
19
|
+
readonly size: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The synchronous file operations the sync walk needs, supplied by the
|
|
23
|
+
* consumer — `node:fs` satisfies it (`nodeWalkerFileSystem` from
|
|
24
|
+
* `@effected/walker/node-sync`).
|
|
25
|
+
*
|
|
26
|
+
* @remarks
|
|
27
|
+
* Every member reports failure by throwing a node-shaped error. A `code` of
|
|
28
|
+
* `"ENOENT"` is the benign vanished-path race (empty, never recorded); any
|
|
29
|
+
* other throw from `readDirectory` or `realPath` is an unreadable directory,
|
|
30
|
+
* handled per `onUnreadable`. A throwing `stat` or `lstat` reads as "no
|
|
31
|
+
* entry", exactly as the `Effect` walk reads its failed `stat` and `lstat`.
|
|
32
|
+
*
|
|
33
|
+
* @public
|
|
34
|
+
*/
|
|
35
|
+
interface WalkerSyncFileSystem {
|
|
36
|
+
/** The entry names inside the directory at `path` (`readdirSync`). */
|
|
37
|
+
readonly readDirectory: (path: string) => ReadonlyArray<string>;
|
|
38
|
+
/** Stats of `path`, following links (`statSync`). */
|
|
39
|
+
readonly stat: (path: string) => WalkerSyncStats;
|
|
40
|
+
/** Stats of `path` itself, not following a final link (`lstatSync`). */
|
|
41
|
+
readonly lstat: (path: string) => WalkerSyncStats;
|
|
42
|
+
/** The real path of `path`, links resolved (`realpathSync`). */
|
|
43
|
+
readonly realPath: (path: string) => string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The synchronous path operation the sync walk needs — a structural subset of
|
|
47
|
+
* `node:path`, so the module itself (or its `win32` / `posix` variant)
|
|
48
|
+
* satisfies it. It shapes only the absolute paths handed to the
|
|
49
|
+
* {@link WalkerSyncFileSystem}; matching is POSIX-relative by the glob
|
|
50
|
+
* contract.
|
|
51
|
+
*
|
|
52
|
+
* @public
|
|
53
|
+
*/
|
|
54
|
+
interface WalkerSyncPath {
|
|
55
|
+
/** Join segments with the implementation's separator (like `path.join`). */
|
|
56
|
+
readonly join: (...segments: ReadonlyArray<string>) => string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The platform half of every sync walk.
|
|
60
|
+
*
|
|
61
|
+
* @public
|
|
62
|
+
*/
|
|
63
|
+
interface WalkerSyncOps {
|
|
64
|
+
/** File operations. */
|
|
65
|
+
readonly fileSystem: WalkerSyncFileSystem;
|
|
66
|
+
/** Path joining. */
|
|
67
|
+
readonly path: WalkerSyncPath;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* `WalkerSyncFileSystem` over `node:fs`: `readdirSync`, `statSync`,
|
|
71
|
+
* `lstatSync`, `realpathSync`.
|
|
72
|
+
*
|
|
73
|
+
* @public
|
|
74
|
+
*/
|
|
75
|
+
export declare const nodeWalkerFileSystem: WalkerSyncFileSystem;
|
|
76
|
+
/**
|
|
77
|
+
* `WalkerSyncPath` over `node:path` (the host platform's separator).
|
|
78
|
+
*
|
|
79
|
+
* @public
|
|
80
|
+
*/
|
|
81
|
+
export declare const nodeWalkerPath: WalkerSyncPath;
|
|
82
|
+
/**
|
|
83
|
+
* Both bindings, to spread into `descendSync` options.
|
|
84
|
+
*
|
|
85
|
+
* @public
|
|
86
|
+
*/
|
|
87
|
+
export declare const nodeWalkerSyncOps: WalkerSyncOps;
|
|
88
|
+
export type { WalkerSyncFileSystem, WalkerSyncOps, WalkerSyncPath, WalkerSyncStats };
|
|
89
|
+
//# sourceMappingURL=node-sync.d.ts.map
|
package/node-sync.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { lstatSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
// src/node-sync.ts
|
|
5
|
+
/**
|
|
6
|
+
* The Node.js binding for the synchronous walk — a ready-made
|
|
7
|
+
* `WalkerSyncFileSystem` over `node:fs` and `WalkerSyncPath` over `node:path`,
|
|
8
|
+
* so adopting `descendSync` is one import.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately a **separate subpath** (`@effected/walker/node-sync`), not part
|
|
11
|
+
* of the main entry: the main entry takes its platform through `R` (or, for the
|
|
12
|
+
* sync forms, from the caller) and imports nothing platform-shaped.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { GlobPattern } from "@effected/glob";
|
|
17
|
+
* import { descendSync } from "@effected/walker";
|
|
18
|
+
* import { nodeWalkerSyncOps } from "@effected/walker/node-sync";
|
|
19
|
+
* import { Result } from "effect";
|
|
20
|
+
*
|
|
21
|
+
* const pattern = Result.getOrThrow(GlobPattern.compileResult("src/**"));
|
|
22
|
+
* const files = descendSync(pattern, { cwd: process.cwd(), ...nodeWalkerSyncOps });
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @packageDocumentation
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* `WalkerSyncFileSystem` over `node:fs`: `readdirSync`, `statSync`,
|
|
29
|
+
* `lstatSync`, `realpathSync`.
|
|
30
|
+
*
|
|
31
|
+
* @public
|
|
32
|
+
*/
|
|
33
|
+
const nodeWalkerFileSystem = {
|
|
34
|
+
readDirectory: (p) => readdirSync(p),
|
|
35
|
+
stat: (p) => statSync(p),
|
|
36
|
+
lstat: (p) => lstatSync(p),
|
|
37
|
+
realPath: (p) => realpathSync(p)
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* `WalkerSyncPath` over `node:path` (the host platform's separator).
|
|
41
|
+
*
|
|
42
|
+
* @public
|
|
43
|
+
*/
|
|
44
|
+
const nodeWalkerPath = { join: path.join };
|
|
45
|
+
/**
|
|
46
|
+
* Both bindings, to spread into `descendSync` options.
|
|
47
|
+
*
|
|
48
|
+
* @public
|
|
49
|
+
*/
|
|
50
|
+
const nodeWalkerSyncOps = {
|
|
51
|
+
fileSystem: nodeWalkerFileSystem,
|
|
52
|
+
path: nodeWalkerPath
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export { nodeWalkerFileSystem, nodeWalkerPath, nodeWalkerSyncOps };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/walker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.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": [
|
|
@@ -34,10 +34,15 @@
|
|
|
34
34
|
"import": "./index.js",
|
|
35
35
|
"default": "./index.js"
|
|
36
36
|
},
|
|
37
|
+
"./node-sync": {
|
|
38
|
+
"types": "./node-sync.d.ts",
|
|
39
|
+
"import": "./node-sync.js",
|
|
40
|
+
"default": "./node-sync.js"
|
|
41
|
+
},
|
|
37
42
|
"./package.json": "./package.json"
|
|
38
43
|
},
|
|
39
44
|
"peerDependencies": {
|
|
40
|
-
"@effected/glob": "^0.
|
|
45
|
+
"@effected/glob": "^0.15.0",
|
|
41
46
|
"effect": "^4.0.0"
|
|
42
47
|
},
|
|
43
48
|
"engines": {
|