@effected/walker 0.22.0 → 0.24.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/ConfineReal.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { PathEscapeError } from "./Confine.js";
2
2
  import { runUpward, runUpwardSync } from "./internal/upwardDrivers.js";
3
3
  import { confineRealMachine } from "./internal/upwardMachine.js";
4
- import { Effect, Path, Result } from "effect";
4
+ import { Effect, Option, Path, Result } from "effect";
5
5
 
6
6
  // src/ConfineReal.ts
7
7
  /** The typed error a refusal describes — the one conversion both forms share. */
@@ -119,5 +119,65 @@ const confineReal = Effect.fn("Walker.confineReal")(function* (base, candidate)
119
119
  * @public
120
120
  */
121
121
  const confineRealSync = (base, candidate, ops) => toResult(candidate, runUpwardSync(confineRealMachine("Walker.confineRealSync", base, candidate, ops.path), ops.fileSystem));
122
+ const within = (options) => ({ requireExisting: options?.missing === "refuse" });
123
+ /**
124
+ * The posix, base-relative path of an absolute path that lies inside a base
125
+ * directory PHYSICALLY, or `None` when it does not.
126
+ *
127
+ * @remarks
128
+ * The question `confineReal` does not answer: where, under `base`, does this
129
+ * absolute path sit? It resolves links the way `confineReal` does, and a path
130
+ * or tail that does not exist yet is resolved lexically from its deepest
131
+ * existing ancestor, so a destination can be placed before it is created.
132
+ * The base itself answers `""`. `None` covers every refusal: a relative or
133
+ * NUL-bearing candidate, a real location outside the base, and a resolution
134
+ * that cannot be verified (a permission error, a loop, a dangling link) —
135
+ * containment that cannot be proved is never assumed. A relative `base` is a
136
+ * **defect**, as it is for `confineReal`.
137
+ *
138
+ * @example
139
+ * ```ts
140
+ * import { relativeWithin } from "@effected/walker";
141
+ * import { Effect } from "effect";
142
+ *
143
+ * const inside = relativeWithin("/repo", "/repo/docs/new.md");
144
+ * // Requires `FileSystem` and `Path`; succeeds with Option.some("docs/new.md").
145
+ * ```
146
+ *
147
+ * @param base - The absolute directory the path must stay inside.
148
+ * @param absolute - The absolute path to place under `base`.
149
+ * @param options - See {@link RelativeWithinOptions}.
150
+ * @returns An `Effect` succeeding with the base-relative posix path, or `None`.
151
+ *
152
+ * @public
153
+ */
154
+ const relativeWithin = Effect.fn("Walker.relativeWithin")(function* (base, absolute, options) {
155
+ const path = yield* Path.Path;
156
+ const outcome = yield* runUpward(confineRealMachine("Walker.relativeWithin", base, absolute, path, within(options)));
157
+ return outcome._tag === "Ok" ? Option.some(outcome.value) : Option.none();
158
+ });
159
+ /**
160
+ * {@link relativeWithin}, synchronously, over a caller-supplied port.
161
+ *
162
+ * @example
163
+ * ```ts
164
+ * import { relativeWithinSync } from "@effected/walker";
165
+ * import { nodeWalkerSyncOps } from "@effected/walker/node-sync";
166
+ *
167
+ * const inside = relativeWithinSync("/repo", "/repo/docs/new.md", nodeWalkerSyncOps);
168
+ * ```
169
+ *
170
+ * @param base - The absolute directory the path must stay inside.
171
+ * @param absolute - The absolute path to place under `base`.
172
+ * @param ops - The platform: spread or pass `nodeWalkerSyncOps`.
173
+ * @param options - See {@link RelativeWithinOptions}.
174
+ * @returns The base-relative posix path, or `None`.
175
+ *
176
+ * @public
177
+ */
178
+ const relativeWithinSync = (base, absolute, ops, options) => {
179
+ const outcome = runUpwardSync(confineRealMachine("Walker.relativeWithinSync", base, absolute, ops.path, within(options)), ops.fileSystem);
180
+ return outcome._tag === "Ok" ? Option.some(outcome.value) : Option.none();
181
+ };
122
182
 
123
- export { confineReal, confineRealSync };
183
+ export { confineReal, confineRealSync, relativeWithin, relativeWithinSync };
package/README.md CHANGED
@@ -8,10 +8,10 @@
8
8
  Path traversal as Effect primitives. `Walker.ascend` gives you the directory chain from a starting path to the filesystem root; `Walker.findUpward` returns the nearest existing file among per-directory candidates; `Walker.findRoot` returns the nearest directory a marker predicate accepts. Every probe absorbs its own failure, so a single unreadable ancestor cannot hide a valid `.git` or `pnpm-workspace.yaml` above it. Going the other way, `descend` expands a compiled [`@effected/glob`](https://www.npmjs.com/package/@effected/glob) pattern under a directory into the matching file paths — sorted, symlink-safe, and typed about unreadable subtrees instead of silently swallowing them. `FileSystem` and `Path` arrive from `effect` core, so no platform package is pulled in — not even in tests.
9
9
 
10
10
  > **Pre-`1.0.0`.** This package is part of the `@effected/*` kit, built on stable
11
- > Effect v4 (`effect` `^4.0.0`) and still in `0.x` development. Stable Effect
11
+ > Effect v4 (`effect` `~4.1.0`) and still in `0.x` development. Stable Effect
12
12
  > makes a kit `1.0.0` possible, not automatic. To keep your `effect` and
13
13
  > `@effect/*` versions on the line the kit is built and tested against, install
14
- > [`@effected/pnpm-plugin-effect`](https://www.npmjs.com/package/@effected/pnpm-plugin-effect).
14
+ > [`@effected/pnpm-plugin-effected`](https://www.npmjs.com/package/@effected/pnpm-plugin-effected).
15
15
  >
16
16
  > **Stability: unstable.** This package's API surface is not yet considered
17
17
  > complete and may change across `0.x` releases. Pin an exact version — even a
package/index.d.ts CHANGED
@@ -707,6 +707,72 @@ export declare const confineReal: (base: string, candidate: string) => Effect.Ef
707
707
  * @public
708
708
  */
709
709
  export declare const confineRealSync: (base: string, candidate: string, ops: WalkerSyncOps) => Result.Result<string, PathEscapeError>;
710
+ /**
711
+ * Options for {@link relativeWithin}.
712
+ *
713
+ * @public
714
+ */
715
+ interface RelativeWithinOptions {
716
+ /**
717
+ * What a path that does not exist yet means. `"ancestor"` (the default)
718
+ * resolves the missing tail from the deepest existing ancestor, so a
719
+ * destination can be placed before it is created; `"refuse"` answers `None`
720
+ * for any path that does not exist.
721
+ */
722
+ readonly missing?: "ancestor" | "refuse";
723
+ }
724
+ /**
725
+ * The posix, base-relative path of an absolute path that lies inside a base
726
+ * directory PHYSICALLY, or `None` when it does not.
727
+ *
728
+ * @remarks
729
+ * The question `confineReal` does not answer: where, under `base`, does this
730
+ * absolute path sit? It resolves links the way `confineReal` does, and a path
731
+ * or tail that does not exist yet is resolved lexically from its deepest
732
+ * existing ancestor, so a destination can be placed before it is created.
733
+ * The base itself answers `""`. `None` covers every refusal: a relative or
734
+ * NUL-bearing candidate, a real location outside the base, and a resolution
735
+ * that cannot be verified (a permission error, a loop, a dangling link) —
736
+ * containment that cannot be proved is never assumed. A relative `base` is a
737
+ * **defect**, as it is for `confineReal`.
738
+ *
739
+ * @example
740
+ * ```ts
741
+ * import { relativeWithin } from "@effected/walker";
742
+ * import { Effect } from "effect";
743
+ *
744
+ * const inside = relativeWithin("/repo", "/repo/docs/new.md");
745
+ * // Requires `FileSystem` and `Path`; succeeds with Option.some("docs/new.md").
746
+ * ```
747
+ *
748
+ * @param base - The absolute directory the path must stay inside.
749
+ * @param absolute - The absolute path to place under `base`.
750
+ * @param options - See {@link RelativeWithinOptions}.
751
+ * @returns An `Effect` succeeding with the base-relative posix path, or `None`.
752
+ *
753
+ * @public
754
+ */
755
+ export declare const relativeWithin: (base: string, absolute: string, options?: RelativeWithinOptions) => Effect.Effect<Option.Option<string>, never, FileSystem.FileSystem | Path.Path>;
756
+ /**
757
+ * {@link relativeWithin}, synchronously, over a caller-supplied port.
758
+ *
759
+ * @example
760
+ * ```ts
761
+ * import { relativeWithinSync } from "@effected/walker";
762
+ * import { nodeWalkerSyncOps } from "@effected/walker/node-sync";
763
+ *
764
+ * const inside = relativeWithinSync("/repo", "/repo/docs/new.md", nodeWalkerSyncOps);
765
+ * ```
766
+ *
767
+ * @param base - The absolute directory the path must stay inside.
768
+ * @param absolute - The absolute path to place under `base`.
769
+ * @param ops - The platform: spread or pass `nodeWalkerSyncOps`.
770
+ * @param options - See {@link RelativeWithinOptions}.
771
+ * @returns The base-relative posix path, or `None`.
772
+ *
773
+ * @public
774
+ */
775
+ export declare const relativeWithinSync: (base: string, absolute: string, ops: WalkerSyncOps, options?: RelativeWithinOptions) => Option.Option<string>;
710
776
  /**
711
777
  * Options for {@link compileAndExpand}: every {@link DescendOptions} field,
712
778
  * plus the glob options the pattern compiles under.
@@ -1165,5 +1231,5 @@ export declare class Walker {
1165
1231
  */
1166
1232
  static findUpwardAllSync(dirs: ReadonlyArray<string>, candidatesFor: (dir: string) => ReadonlyArray<string>, ops: WalkerSyncOps): Option.Option<UpwardMatches>;
1167
1233
  }
1168
- export type { AscendOptions, AscendSyncOptions, AscendWithinSyncOptions, CompileAndExpandOptions, DescendEntriesResult, DescendEntry, DescendOptions, DescendRecordOptions, DescendResult, DescendSyncOptions, DescendSyncRecordOptions, UnreadableDirectory, UpwardMatches, WalkerSyncFileSystem, WalkerSyncOps, WalkerSyncPath, WalkerSyncStats };
1234
+ export type { AscendOptions, AscendSyncOptions, AscendWithinSyncOptions, CompileAndExpandOptions, DescendEntriesResult, DescendEntry, DescendOptions, DescendRecordOptions, DescendResult, DescendSyncOptions, DescendSyncRecordOptions, RelativeWithinOptions, UnreadableDirectory, UpwardMatches, WalkerSyncFileSystem, WalkerSyncOps, WalkerSyncPath, WalkerSyncStats };
1169
1235
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { PathEscapeError, PathEscapeReason, confineRelative, confineRelativeResult } from "./Confine.js";
2
- import { confineReal, confineRealSync } from "./ConfineReal.js";
2
+ import { confineReal, confineRealSync, relativeWithin, relativeWithinSync } from "./ConfineReal.js";
3
3
  import { DescendError, descend, descendEntries } from "./Descend.js";
4
4
  import { descendEntriesSync, descendSync } from "./DescendSync.js";
5
5
  import { GlobExpansionError, compileAndExpand } from "./Expand.js";
6
6
  import { Walker } from "./Walker.js";
7
7
 
8
- export { DescendError, GlobExpansionError, PathEscapeError, PathEscapeReason, Walker, compileAndExpand, confineReal, confineRealSync, confineRelative, confineRelativeResult, descend, descendEntries, descendEntriesSync, descendSync };
8
+ export { DescendError, GlobExpansionError, PathEscapeError, PathEscapeReason, Walker, compileAndExpand, confineReal, confineRealSync, confineRelative, confineRelativeResult, descend, descendEntries, descendEntriesSync, descendSync, relativeWithin, relativeWithinSync };
@@ -147,7 +147,7 @@ const isWin32Reserved = (segment) => segment.includes(":") || DOTS_AND_SPACES.te
147
147
  * two requests) is unresolvable, its target unverified. Any failure other
148
148
  * than `NotFound` makes the path unresolvable too. Fail closed throughout.
149
149
  */
150
- function* resolvePhysical(absolute, path, win32, checkSegments) {
150
+ function* resolvePhysical(absolute, path, win32, checkSegments, requireExisting = false) {
151
151
  let root = absolute;
152
152
  for (let parent = path.dirname(root); parent !== root; parent = path.dirname(root)) root = parent;
153
153
  const segments = (absolute.startsWith(root) ? absolute.slice(root.length) : absolute).split(win32 ? /[\\/]/ : "/").filter((segment) => segment !== "");
@@ -198,6 +198,10 @@ function* resolvePhysical(absolute, path, win32, checkSegments) {
198
198
  _tag: "Unresolvable",
199
199
  cause: answer.cause
200
200
  };
201
+ if (requireExisting) return {
202
+ _tag: "Refused",
203
+ reason: "outside"
204
+ };
201
205
  current = next;
202
206
  }
203
207
  return {
@@ -209,7 +213,7 @@ function* resolvePhysical(absolute, path, win32, checkSegments) {
209
213
  * `confineReal`: the candidate's real path when it lies strictly inside the
210
214
  * base's real path, or why not. A relative `base` is a wiring defect.
211
215
  */
212
- function* confineRealMachine(name, base, candidate, path) {
216
+ function* confineRealMachine(name, base, candidate, path, within = false) {
213
217
  if (base.includes("\0") || !isPinnedAbsolute(base, path)) return defect(`${name}: base must be an absolute path, received ${JSON.stringify(base)}`);
214
218
  const refuse = (reason) => done({
215
219
  _tag: "Refused",
@@ -217,6 +221,7 @@ function* confineRealMachine(name, base, candidate, path) {
217
221
  });
218
222
  if (candidate.includes("\0")) return refuse("nulByte");
219
223
  if (candidate === "") return refuse("empty");
224
+ if (within && !path.isAbsolute(candidate)) return refuse("absolute");
220
225
  const win32 = path.sep === "\\";
221
226
  const isSep = (character) => character === path.sep || win32 && character === "/";
222
227
  if (win32 && DRIVE.test(candidate) && !path.isAbsolute(candidate)) return refuse("absolute");
@@ -224,12 +229,20 @@ function* confineRealMachine(name, base, candidate, path) {
224
229
  const full = path.isAbsolute(candidate) ? candidate : `${trimTrailing(base, isSep)}${path.sep}${candidate}`;
225
230
  const baseReal = yield* resolvePhysical(base, path, win32, false);
226
231
  if (baseReal._tag !== "Ok") return done(baseReal);
227
- const candidateReal = yield* resolvePhysical(full, path, win32, true);
232
+ const candidateReal = yield* resolvePhysical(full, path, win32, true, within !== false && within.requireExisting);
228
233
  if (candidateReal._tag !== "Ok") return done(candidateReal);
229
234
  const root = trimTrailing(baseReal.value, isSep);
230
235
  const resolved = trimTrailing(candidateReal.value, isSep);
236
+ if (within && resolved === root) return done({
237
+ _tag: "Ok",
238
+ value: ""
239
+ });
231
240
  if (resolved === root) return refuse("empty");
232
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
+ });
233
246
  return done({
234
247
  _tag: "Ok",
235
248
  value: candidateReal.value
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/walker",
3
- "version": "0.22.0",
3
+ "version": "0.24.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,8 +42,8 @@
42
42
  "./package.json": "./package.json"
43
43
  },
44
44
  "peerDependencies": {
45
- "@effected/glob": "^0.16.0",
46
- "effect": "^4.0.0"
45
+ "@effected/glob": "^0.17.0",
46
+ "effect": "~4.1.0"
47
47
  },
48
48
  "engines": {
49
49
  "node": ">=24.11.0"
@@ -5,7 +5,7 @@
5
5
  "toolPackages": [
6
6
  {
7
7
  "packageName": "@microsoft/api-extractor",
8
- "packageVersion": "7.59.4"
8
+ "packageVersion": "7.60.0"
9
9
  }
10
10
  ]
11
11
  }