@effected/walker 0.21.0 → 0.22.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.
@@ -0,0 +1,71 @@
1
+ import { Effect, FileSystem } from "effect";
2
+
3
+ // src/internal/upwardDrivers.ts
4
+ /** Runs a machine against core's `FileSystem`. A machine's `Defect` dies. */
5
+ const runUpward = (machine) => Effect.gen(function* () {
6
+ const fs = yield* FileSystem.FileSystem;
7
+ const answer = (request) => {
8
+ if (request._tag === "Exists") return fs.exists(request.path).pipe(Effect.orElseSucceed(() => false));
9
+ if (request._tag === "Present") return fs.lstat(request.path).pipe(Effect.map(() => ({ _tag: "Present" })), Effect.catch((error) => Effect.succeed(error.reason._tag === "NotFound" ? { _tag: "NotFound" } : {
10
+ _tag: "Failed",
11
+ cause: error
12
+ })));
13
+ return fs.realPath(request.path).pipe(Effect.map((value) => ({
14
+ _tag: "Ok",
15
+ value
16
+ })), Effect.catch((error) => Effect.succeed(error.reason._tag === "NotFound" ? {
17
+ _tag: "NotFound",
18
+ cause: error
19
+ } : {
20
+ _tag: "Failed",
21
+ cause: error
22
+ })));
23
+ };
24
+ let step = machine.next();
25
+ while (step.done !== true) step = machine.next(yield* answer(step.value));
26
+ const result = step.value;
27
+ if (result._tag === "Defect") return yield* Effect.die(new Error(result.message));
28
+ return result.value;
29
+ });
30
+ const isNotFound = (error) => typeof error === "object" && error !== null && error.code === "ENOENT";
31
+ /** Runs a machine against a synchronous port. A machine's `Defect` throws. */
32
+ const runUpwardSync = (machine, fileSystem) => {
33
+ const answer = (request) => {
34
+ if (request._tag === "Exists") try {
35
+ fileSystem.stat(request.path);
36
+ return true;
37
+ } catch {
38
+ return false;
39
+ }
40
+ if (request._tag === "Present") try {
41
+ fileSystem.lstat(request.path);
42
+ return { _tag: "Present" };
43
+ } catch (cause) {
44
+ return isNotFound(cause) ? { _tag: "NotFound" } : {
45
+ _tag: "Failed",
46
+ cause
47
+ };
48
+ }
49
+ try {
50
+ return {
51
+ _tag: "Ok",
52
+ value: fileSystem.realPath(request.path)
53
+ };
54
+ } catch (cause) {
55
+ return isNotFound(cause) ? {
56
+ _tag: "NotFound",
57
+ cause
58
+ } : {
59
+ _tag: "Failed",
60
+ cause
61
+ };
62
+ }
63
+ };
64
+ let step = machine.next();
65
+ while (step.done !== true) step = machine.next(answer(step.value));
66
+ const result = step.value;
67
+ if (result._tag === "Defect") throw new Error(result.message);
68
+ return result.value;
69
+ };
70
+
71
+ export { runUpward, runUpwardSync };
@@ -0,0 +1,239 @@
1
+ import { trimTrailing } from "./trimTrailing.js";
2
+ import { isCurrentDriveRooted, isPinnedAbsolute } from "./absolute.js";
3
+
4
+ // src/internal/upwardMachine.ts
5
+ /** Asks for the real path at `path`. */
6
+ function* realPath(path) {
7
+ return yield {
8
+ _tag: "RealPath",
9
+ path
10
+ };
11
+ }
12
+ /** Asks whether anything — a dangling link included — lives at `path` itself. */
13
+ function* present(path) {
14
+ return yield {
15
+ _tag: "Present",
16
+ path
17
+ };
18
+ }
19
+ /** Asks whether `path` exists. */
20
+ function* exists(path) {
21
+ return yield {
22
+ _tag: "Exists",
23
+ path
24
+ };
25
+ }
26
+ const done = (value) => ({
27
+ _tag: "Done",
28
+ value
29
+ });
30
+ const defect = (message) => ({
31
+ _tag: "Defect",
32
+ message
33
+ });
34
+ /**
35
+ * The lexical chain from `start` toward the root, nearest first — `ascend`'s
36
+ * whole algorithm. Pure: it reads no filesystem, so it is not a generator.
37
+ * `name` prefixes the defect messages, so each public form reports itself.
38
+ */
39
+ const ascendChain = (name, start, options, path) => {
40
+ const maxDepth = options?.maxDepth ?? 256;
41
+ if (!Number.isInteger(maxDepth) || maxDepth < 1) return defect(`${name}: maxDepth must be a positive integer, received ${maxDepth}`);
42
+ if (options?.stopAt !== void 0 && !isPinnedAbsolute(options.stopAt, path)) return defect(`${name}: stopAt must be an absolute path, received ${JSON.stringify(options.stopAt)} (ascending from ${JSON.stringify(start)})`);
43
+ const ceiling = options?.stopAt === void 0 ? void 0 : path.resolve(options.stopAt);
44
+ const dirs = [];
45
+ let current = start;
46
+ for (let depth = 0; depth < maxDepth; depth++) {
47
+ dirs.push(current);
48
+ if (ceiling !== void 0 && (current === ceiling || path.resolve(current) === ceiling)) break;
49
+ const parent = path.dirname(current);
50
+ if (parent === current) break;
51
+ current = parent;
52
+ }
53
+ return done(dirs);
54
+ };
55
+ /** The real path at `path`, or `undefined` when it cannot be resolved for any reason. */
56
+ function* realPathOrUndefined(path) {
57
+ const answer = yield* realPath(path);
58
+ return answer._tag === "Ok" ? answer.value : void 0;
59
+ }
60
+ /**
61
+ * `ascendWithin`: the lexical chain, stopped at the nearest ancestor whose real
62
+ * path is the ceiling's. `ceiling === undefined` is `Option.none()` — the chain
63
+ * runs to the root exactly as `ascend` gives it.
64
+ */
65
+ function* ascendWithinMachine(name, start, ceiling, options, path) {
66
+ const maxDepth = options?.maxDepth === void 0 ? {} : { maxDepth: options.maxDepth };
67
+ if (ceiling === void 0) return ascendChain(name, start, maxDepth, path);
68
+ if (!isPinnedAbsolute(ceiling, path)) return defect(`${name}: ceiling must be an absolute path, received ${JSON.stringify(ceiling)} (ascending from ${JSON.stringify(start)})`);
69
+ const lexical = ascendChain(name, start, {
70
+ ...maxDepth,
71
+ stopAt: ceiling
72
+ }, path);
73
+ if (lexical._tag === "Defect") return lexical;
74
+ const dirs = lexical.value;
75
+ const last = dirs.at(-1);
76
+ if (last !== void 0 && path.resolve(last) === path.resolve(ceiling)) return lexical;
77
+ const physical = yield* realPathOrUndefined(ceiling);
78
+ if (physical === void 0) return lexical;
79
+ for (let index = 0; index < dirs.length; index += 1) if ((yield* realPathOrUndefined(dirs[index] ?? "")) === physical) return done(dirs.slice(0, index + 1));
80
+ return lexical;
81
+ }
82
+ /**
83
+ * `ascendToMarker`: the lexical chain, stopped at the nearest directory that
84
+ * holds any of `markers`, inclusive. Probes stop at the hit — nothing above it
85
+ * is ever touched.
86
+ */
87
+ function* ascendToMarkerMachine(name, start, markers, options, path) {
88
+ if (markers.length === 0) return defect(`${name}: markers must name at least one entry (ascending from ${JSON.stringify(start)})`);
89
+ if (markers.find((marker) => marker === "") !== void 0) return defect(`${name}: a marker must be a non-empty entry name (ascending from ${JSON.stringify(start)})`);
90
+ const chain = ascendChain(name, start, options?.maxDepth === void 0 ? {} : { maxDepth: options.maxDepth }, path);
91
+ if (chain._tag === "Defect") return chain;
92
+ const dirs = chain.value;
93
+ for (let index = 0; index < dirs.length; index += 1) {
94
+ const dir = dirs[index] ?? "";
95
+ for (const marker of markers) if (yield* exists(path.join(dir, marker))) return done(dirs.slice(0, index + 1));
96
+ }
97
+ return chain;
98
+ }
99
+ /** `findUpward`: the first existing candidate, directory-major, or `undefined`. */
100
+ function* findUpwardMachine(dirs, candidatesFor) {
101
+ const candidates = [];
102
+ for (const dir of dirs) candidates.push(...candidatesFor(dir));
103
+ for (const candidate of candidates) if (yield* exists(candidate)) return done(candidate);
104
+ return done(void 0);
105
+ }
106
+ /**
107
+ * `findUpwardAll`: every existing candidate of the nearest directory with any
108
+ * hit, in candidate order — that directory's candidates are ALL probed, so a
109
+ * second hit (an ambiguity) is never hidden; nothing above it is probed.
110
+ */
111
+ function* findUpwardAllMachine(dirs, candidatesFor) {
112
+ const expanded = dirs.map((dir) => ({
113
+ dir,
114
+ candidates: candidatesFor(dir)
115
+ }));
116
+ for (const { dir, candidates } of expanded) {
117
+ const found = [];
118
+ for (const candidate of candidates) if (yield* exists(candidate)) found.push(candidate);
119
+ const [first, ...rest] = found;
120
+ if (first !== void 0) return done({
121
+ dir,
122
+ found: [first, ...rest]
123
+ });
124
+ }
125
+ return done(void 0);
126
+ }
127
+ /** A segment of dots and spaces only, which win32 trims to nothing or to `..`. */
128
+ const DOTS_AND_SPACES = /^[. ]+$/;
129
+ /** A win32 reserved device name, with or without an extension, in any case. */
130
+ const DEVICE_NAME = /^(?:con|prn|aux|nul|com[0-9\u00b9\u00b2\u00b3]|lpt[0-9\u00b9\u00b2\u00b3]|conin\$|conout\$) *(?:\..*)?$/i;
131
+ /** A win32 drive designator at the start of a path: `C:`, `C:x`, `C:\x`. */
132
+ const DRIVE = /^[A-Za-z]:/;
133
+ /** Whether win32 reads a segment as something other than a plain entry (`.` and `..` are handled before). */
134
+ const isWin32Reserved = (segment) => segment.includes(":") || DOTS_AND_SPACES.test(segment) || DEVICE_NAME.test(segment);
135
+ /**
136
+ * Resolve `absolute` physically, segment by segment, the way the operating
137
+ * system would: a `..` after a link climbs out of the link's TARGET, never
138
+ * lexically out of the link. A first `RealPath` on the whole path settles the
139
+ * common case in one request when no `..` is present (a lexical-first
140
+ * `realpath` — node's JS `fs.realpath` — would otherwise collapse `link/..`
141
+ * before resolving the link). A path, or tail, that does not exist yet is
142
+ * resolved lexically from its real existing ancestor — but `NotFound` from a
143
+ * real-path request is NOT proof of absence: a dangling link answers it too,
144
+ * and a write through one creates its target wherever it points. So every
145
+ * `NotFound` segment is checked with `Present`, and only "nothing lives here"
146
+ * continues lexically; a dangling link (or an entry that appeared between the
147
+ * two requests) is unresolvable, its target unverified. Any failure other
148
+ * than `NotFound` makes the path unresolvable too. Fail closed throughout.
149
+ */
150
+ function* resolvePhysical(absolute, path, win32, checkSegments) {
151
+ let root = absolute;
152
+ for (let parent = path.dirname(root); parent !== root; parent = path.dirname(root)) root = parent;
153
+ const segments = (absolute.startsWith(root) ? absolute.slice(root.length) : absolute).split(win32 ? /[\\/]/ : "/").filter((segment) => segment !== "");
154
+ if (checkSegments && win32) {
155
+ for (const segment of segments) if (segment !== "." && segment !== ".." && isWin32Reserved(segment)) return {
156
+ _tag: "Refused",
157
+ reason: "reservedName"
158
+ };
159
+ }
160
+ const isSep = (character) => character === path.sep || win32 && character === "/";
161
+ const real = (target) => realPath(target);
162
+ if (!segments.includes("..")) {
163
+ const whole = yield* real(absolute);
164
+ if (whole._tag === "Ok") return whole;
165
+ if (whole._tag === "Failed") return {
166
+ _tag: "Unresolvable",
167
+ cause: whole.cause
168
+ };
169
+ }
170
+ const rootAnswer = yield* real(root);
171
+ if (rootAnswer._tag === "Failed") return {
172
+ _tag: "Unresolvable",
173
+ cause: rootAnswer.cause
174
+ };
175
+ let current = rootAnswer._tag === "Ok" ? rootAnswer.value : root;
176
+ for (const segment of segments) {
177
+ if (segment === ".") continue;
178
+ if (segment === "..") {
179
+ current = path.dirname(current);
180
+ continue;
181
+ }
182
+ const next = `${trimTrailing(current, isSep)}${path.sep}${segment}`;
183
+ const answer = yield* real(next);
184
+ if (answer._tag === "Failed") return {
185
+ _tag: "Unresolvable",
186
+ cause: answer.cause
187
+ };
188
+ if (answer._tag === "Ok") {
189
+ current = answer.value;
190
+ continue;
191
+ }
192
+ const entry = yield* present(next);
193
+ if (entry._tag === "Failed") return {
194
+ _tag: "Unresolvable",
195
+ cause: entry.cause
196
+ };
197
+ if (entry._tag === "Present") return {
198
+ _tag: "Unresolvable",
199
+ cause: answer.cause
200
+ };
201
+ current = next;
202
+ }
203
+ return {
204
+ _tag: "Ok",
205
+ value: current
206
+ };
207
+ }
208
+ /**
209
+ * `confineReal`: the candidate's real path when it lies strictly inside the
210
+ * base's real path, or why not. A relative `base` is a wiring defect.
211
+ */
212
+ function* confineRealMachine(name, base, candidate, path) {
213
+ if (base.includes("\0") || !isPinnedAbsolute(base, path)) return defect(`${name}: base must be an absolute path, received ${JSON.stringify(base)}`);
214
+ const refuse = (reason) => done({
215
+ _tag: "Refused",
216
+ reason
217
+ });
218
+ if (candidate.includes("\0")) return refuse("nulByte");
219
+ if (candidate === "") return refuse("empty");
220
+ const win32 = path.sep === "\\";
221
+ const isSep = (character) => character === path.sep || win32 && character === "/";
222
+ if (win32 && DRIVE.test(candidate) && !path.isAbsolute(candidate)) return refuse("absolute");
223
+ if (isCurrentDriveRooted(candidate, path.sep)) return refuse("absolute");
224
+ const full = path.isAbsolute(candidate) ? candidate : `${trimTrailing(base, isSep)}${path.sep}${candidate}`;
225
+ const baseReal = yield* resolvePhysical(base, path, win32, false);
226
+ if (baseReal._tag !== "Ok") return done(baseReal);
227
+ const candidateReal = yield* resolvePhysical(full, path, win32, true);
228
+ if (candidateReal._tag !== "Ok") return done(candidateReal);
229
+ const root = trimTrailing(baseReal.value, isSep);
230
+ const resolved = trimTrailing(candidateReal.value, isSep);
231
+ if (resolved === root) return refuse("empty");
232
+ if (!resolved.startsWith(`${root}${path.sep}`)) return refuse("outside");
233
+ return done({
234
+ _tag: "Ok",
235
+ value: candidateReal.value
236
+ });
237
+ }
238
+
239
+ export { ascendChain, ascendToMarkerMachine, ascendWithinMachine, confineRealMachine, findUpwardAllMachine, findUpwardMachine };
package/node-sync.d.ts CHANGED
@@ -43,17 +43,30 @@ interface WalkerSyncFileSystem {
43
43
  readonly realPath: (path: string) => string;
44
44
  }
45
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)
46
+ * The synchronous path operations the sync forms need — a structural subset
47
+ * of `node:path`, so the module itself (or its `win32` / `posix` variant)
48
48
  * satisfies it. It shapes only the absolute paths handed to the
49
49
  * {@link WalkerSyncFileSystem}; matching is POSIX-relative by the glob
50
50
  * contract.
51
51
  *
52
+ * @remarks
53
+ * The members mirror core's `Path` service, so a sync form judges
54
+ * absoluteness, ascends and compares ceilings exactly as its `Effect` twin
55
+ * does under the `Path` layer of the same platform.
56
+ *
52
57
  * @public
53
58
  */
54
59
  interface WalkerSyncPath {
60
+ /** The implementation's separator (`path.sep`). */
61
+ readonly sep: string;
55
62
  /** Join segments with the implementation's separator (like `path.join`). */
56
63
  readonly join: (...segments: ReadonlyArray<string>) => string;
64
+ /** The parent directory of a path (like `path.dirname`); a fixpoint at the root. */
65
+ readonly dirname: (path: string) => string;
66
+ /** Resolve segments to a normalized path (like `path.resolve`). */
67
+ readonly resolve: (...segments: ReadonlyArray<string>) => string;
68
+ /** Whether a path is absolute under this implementation (like `path.isAbsolute`). */
69
+ readonly isAbsolute: (path: string) => boolean;
57
70
  }
58
71
  /**
59
72
  * The platform half of every sync walk.
package/node-sync.js CHANGED
@@ -41,7 +41,13 @@ const nodeWalkerFileSystem = {
41
41
  *
42
42
  * @public
43
43
  */
44
- const nodeWalkerPath = { join: path.join };
44
+ const nodeWalkerPath = {
45
+ sep: path.sep,
46
+ join: path.join,
47
+ dirname: path.dirname,
48
+ resolve: path.resolve,
49
+ isAbsolute: path.isAbsolute
50
+ };
45
51
  /**
46
52
  * Both bindings, to spread into `descendSync` options.
47
53
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/walker",
3
- "version": "0.21.0",
3
+ "version": "0.22.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": [
@@ -42,7 +42,7 @@
42
42
  "./package.json": "./package.json"
43
43
  },
44
44
  "peerDependencies": {
45
- "@effected/glob": "^0.15.0",
45
+ "@effected/glob": "^0.16.0",
46
46
  "effect": "^4.0.0"
47
47
  },
48
48
  "engines": {