@effected/walker 0.21.0 → 0.23.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 +16 -4
- package/ConfineReal.js +183 -0
- package/Descend.js +5 -1
- package/DescendSync.js +5 -1
- package/Walker.js +193 -38
- package/index.d.ts +441 -15
- package/index.js +2 -1
- package/internal/absolute.js +19 -0
- package/internal/descendMachine.js +50 -8
- package/internal/upwardDrivers.js +71 -0
- package/internal/upwardMachine.js +252 -0
- package/node-sync.d.ts +15 -2
- package/node-sync.js +7 -1
- package/package.json +2 -2
- package/tsdoc-metadata.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolveSegments } from "./segments.js";
|
|
2
2
|
import { trimTrailing } from "./trimTrailing.js";
|
|
3
|
+
import { isPinnedAbsolute } from "./absolute.js";
|
|
3
4
|
|
|
4
5
|
// src/internal/descendMachine.ts
|
|
5
6
|
/** Directory names never descended into unless the caller overrides `prune`. */
|
|
@@ -9,7 +10,8 @@ const isSlash = (character) => character === "/";
|
|
|
9
10
|
/** Shared empty ancestor chain: frames under `followSymlinks: false` never consult theirs. */
|
|
10
11
|
const NO_ANCESTORS = Object.freeze([]);
|
|
11
12
|
/** The walk. */
|
|
12
|
-
function* descendMachine(pattern, options,
|
|
13
|
+
function* descendMachine(pattern, options, path) {
|
|
14
|
+
const join = path.join;
|
|
13
15
|
/** Yields one request; the answer is typed by the request's tag. */
|
|
14
16
|
function* ask(request) {
|
|
15
17
|
return yield request;
|
|
@@ -19,6 +21,19 @@ function* descendMachine(pattern, options, join) {
|
|
|
19
21
|
_tag: "Defect",
|
|
20
22
|
message: `Walker.descend: maxDepth must be a positive integer, received ${maxDepth}`
|
|
21
23
|
};
|
|
24
|
+
if (options.cwd.includes("\0") || !isPinnedAbsolute(options.cwd, path)) return {
|
|
25
|
+
_tag: "Defect",
|
|
26
|
+
message: `Walker.descend: cwd must be an absolute path, received ${JSON.stringify(options.cwd)}`
|
|
27
|
+
};
|
|
28
|
+
const match = options.match ?? "file";
|
|
29
|
+
if (match !== "file" && match !== "directory" && match !== "any") return {
|
|
30
|
+
_tag: "Defect",
|
|
31
|
+
message: `Walker.descend: match must be "file", "directory" or "any", received ${JSON.stringify(match)}`
|
|
32
|
+
};
|
|
33
|
+
const matchFiles = match !== "directory";
|
|
34
|
+
const matchDirectories = match !== "file";
|
|
35
|
+
/** Canonical paths of the directories already answered: a root's base may also be an entry of another root. */
|
|
36
|
+
const answeredDirectories = /* @__PURE__ */ new Set();
|
|
22
37
|
const prune = new Set(options.prune ?? DEFAULT_PRUNE);
|
|
23
38
|
const boundaries = options.boundaries ?? [];
|
|
24
39
|
const onUnreadable = options.onUnreadable ?? "fail";
|
|
@@ -93,7 +108,8 @@ function* descendMachine(pattern, options, join) {
|
|
|
93
108
|
_tag: "Stat",
|
|
94
109
|
path: join(options.cwd, pattern.source)
|
|
95
110
|
});
|
|
96
|
-
|
|
111
|
+
const wanted = answer?.kind === "File" && matchFiles || answer?.kind === "Directory" && matchDirectories && segments.length > 0;
|
|
112
|
+
return done(answer !== void 0 && wanted ? [{
|
|
97
113
|
path: pattern.source,
|
|
98
114
|
info: answer.info
|
|
99
115
|
}] : []);
|
|
@@ -122,7 +138,7 @@ function* descendMachine(pattern, options, join) {
|
|
|
122
138
|
canonical: canonical === "" ? "" : `${canonical}/`
|
|
123
139
|
};
|
|
124
140
|
});
|
|
125
|
-
/** The
|
|
141
|
+
/** The entry at canonical `relative`, in the first spelling the pattern matches it under, or `undefined`. */
|
|
126
142
|
const matchedAs = (relative) => {
|
|
127
143
|
for (const { spelling, canonical } of spelled) {
|
|
128
144
|
if (canonical !== "" && !relative.startsWith(canonical)) continue;
|
|
@@ -130,6 +146,17 @@ function* descendMachine(pattern, options, join) {
|
|
|
130
146
|
if (pattern.matches(candidate)) return candidate;
|
|
131
147
|
}
|
|
132
148
|
};
|
|
149
|
+
/**
|
|
150
|
+
* The directory at canonical `relative`, in the first spelling the pattern
|
|
151
|
+
* matches it under — bare, or with a trailing `/` (a glob's directory-only
|
|
152
|
+
* form) — or `undefined`. Each directory is answered once.
|
|
153
|
+
*/
|
|
154
|
+
const matchedDirectoryAs = (relative) => {
|
|
155
|
+
if (answeredDirectories.has(relative)) return void 0;
|
|
156
|
+
const answer = matchedAs(relative) ?? matchedAs(`${relative}/`);
|
|
157
|
+
if (answer !== void 0) answeredDirectories.add(relative);
|
|
158
|
+
return answer;
|
|
159
|
+
};
|
|
133
160
|
const baseSegments = resolveSegments(rawBase.split("/"));
|
|
134
161
|
if (baseSegments === void 0) return void 0;
|
|
135
162
|
const base = baseSegments.join("/");
|
|
@@ -137,10 +164,11 @@ function* descendMachine(pattern, options, join) {
|
|
|
137
164
|
const own = spelled.find(({ canonical }) => canonical === baseCanonical)?.spelling ?? baseCanonical;
|
|
138
165
|
const shown = (relative) => relative === base ? trimTrailing(own, isSlash) : own + relative.slice(baseCanonical.length);
|
|
139
166
|
const absoluteBase = base === "" ? options.cwd : join(options.cwd, base);
|
|
140
|
-
|
|
167
|
+
const baseStat = yield* ask({
|
|
141
168
|
_tag: "Stat",
|
|
142
169
|
path: absoluteBase
|
|
143
|
-
})
|
|
170
|
+
});
|
|
171
|
+
if (baseStat?.kind !== "Directory") return void 0;
|
|
144
172
|
if (yield* blocked(baseSegments, baseSegments.length, baseSegments.length - 1)) return void 0;
|
|
145
173
|
let baseAncestors = NO_ANCESTORS;
|
|
146
174
|
if (followSymlinks) {
|
|
@@ -149,6 +177,13 @@ function* descendMachine(pattern, options, join) {
|
|
|
149
177
|
if (real === void 0) return void 0;
|
|
150
178
|
baseAncestors = [real];
|
|
151
179
|
}
|
|
180
|
+
if (matchDirectories && base !== "") {
|
|
181
|
+
const answer = matchedDirectoryAs(base);
|
|
182
|
+
if (answer !== void 0) results.push({
|
|
183
|
+
path: answer,
|
|
184
|
+
info: baseStat.info
|
|
185
|
+
});
|
|
186
|
+
}
|
|
152
187
|
const frames = [{
|
|
153
188
|
relative: base,
|
|
154
189
|
absolute: absoluteBase,
|
|
@@ -186,13 +221,20 @@ function* descendMachine(pattern, options, join) {
|
|
|
186
221
|
}) : own;
|
|
187
222
|
if (stat === void 0) continue;
|
|
188
223
|
if (stat.kind === "File") {
|
|
189
|
-
const
|
|
190
|
-
if (
|
|
191
|
-
path,
|
|
224
|
+
const answer = matchFiles ? matchedAs(relative) : void 0;
|
|
225
|
+
if (answer !== void 0) results.push({
|
|
226
|
+
path: answer,
|
|
192
227
|
info: stat.info
|
|
193
228
|
});
|
|
194
229
|
continue;
|
|
195
230
|
}
|
|
231
|
+
if (stat.kind === "Directory" && matchDirectories) {
|
|
232
|
+
const answer = matchedDirectoryAs(relative);
|
|
233
|
+
if (answer !== void 0) results.push({
|
|
234
|
+
path: answer,
|
|
235
|
+
info: stat.info
|
|
236
|
+
});
|
|
237
|
+
}
|
|
196
238
|
if (stat.kind !== "Directory" || !deep) continue;
|
|
197
239
|
if (prune.has(entry)) continue;
|
|
198
240
|
let ancestors = frame.ancestors;
|
|
@@ -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,252 @@
|
|
|
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, requireExisting = false) {
|
|
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
|
+
if (requireExisting) return {
|
|
202
|
+
_tag: "Refused",
|
|
203
|
+
reason: "outside"
|
|
204
|
+
};
|
|
205
|
+
current = next;
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
_tag: "Ok",
|
|
209
|
+
value: current
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* `confineReal`: the candidate's real path when it lies strictly inside the
|
|
214
|
+
* base's real path, or why not. A relative `base` is a wiring defect.
|
|
215
|
+
*/
|
|
216
|
+
function* confineRealMachine(name, base, candidate, path, within = false) {
|
|
217
|
+
if (base.includes("\0") || !isPinnedAbsolute(base, path)) return defect(`${name}: base must be an absolute path, received ${JSON.stringify(base)}`);
|
|
218
|
+
const refuse = (reason) => done({
|
|
219
|
+
_tag: "Refused",
|
|
220
|
+
reason
|
|
221
|
+
});
|
|
222
|
+
if (candidate.includes("\0")) return refuse("nulByte");
|
|
223
|
+
if (candidate === "") return refuse("empty");
|
|
224
|
+
if (within && !path.isAbsolute(candidate)) return refuse("absolute");
|
|
225
|
+
const win32 = path.sep === "\\";
|
|
226
|
+
const isSep = (character) => character === path.sep || win32 && character === "/";
|
|
227
|
+
if (win32 && DRIVE.test(candidate) && !path.isAbsolute(candidate)) return refuse("absolute");
|
|
228
|
+
if (isCurrentDriveRooted(candidate, path.sep)) return refuse("absolute");
|
|
229
|
+
const full = path.isAbsolute(candidate) ? candidate : `${trimTrailing(base, isSep)}${path.sep}${candidate}`;
|
|
230
|
+
const baseReal = yield* resolvePhysical(base, path, win32, false);
|
|
231
|
+
if (baseReal._tag !== "Ok") return done(baseReal);
|
|
232
|
+
const candidateReal = yield* resolvePhysical(full, path, win32, true, within !== false && within.requireExisting);
|
|
233
|
+
if (candidateReal._tag !== "Ok") return done(candidateReal);
|
|
234
|
+
const root = trimTrailing(baseReal.value, isSep);
|
|
235
|
+
const resolved = trimTrailing(candidateReal.value, isSep);
|
|
236
|
+
if (within && resolved === root) return done({
|
|
237
|
+
_tag: "Ok",
|
|
238
|
+
value: ""
|
|
239
|
+
});
|
|
240
|
+
if (resolved === root) return refuse("empty");
|
|
241
|
+
if (!resolved.startsWith(`${root}${path.sep}`)) return refuse("outside");
|
|
242
|
+
if (within) return done({
|
|
243
|
+
_tag: "Ok",
|
|
244
|
+
value: resolved.slice(root.length + 1).split(path.sep).join("/")
|
|
245
|
+
});
|
|
246
|
+
return done({
|
|
247
|
+
_tag: "Ok",
|
|
248
|
+
value: candidateReal.value
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
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
|
|
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 = {
|
|
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.
|
|
3
|
+
"version": "0.23.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.
|
|
45
|
+
"@effected/glob": "^0.16.0",
|
|
46
46
|
"effect": "^4.0.0"
|
|
47
47
|
},
|
|
48
48
|
"engines": {
|