| 1 | /** |
| 2 | * Provides path operations through the Effect environment. |
| 3 | * |
| 4 | * The `Path` service works with file system paths without tying code to one |
| 5 | * concrete platform module. It exposes common operations such as joining, |
| 6 | * normalizing, parsing, formatting, resolving, and converting paths to or from |
| 7 | * file URLs. This module includes the service interface, parsed path type, |
| 8 | * service tag, runtime marker, and built-in POSIX path layer. |
| 9 | * |
| 10 | * @since 4.0.0 |
| 11 | */ |
| 12 | import * as Context from "./Context.ts" |
| 13 | import * as Effect from "./Effect.ts" |
| 14 | import { identity } from "./Function.ts" |
| 15 | import * as Layer from "./Layer.ts" |
| 16 | import { BadArgument } from "./PlatformError.ts" |
| 17 | |
| 18 | /** |
| 19 | * Runtime type identifier used to mark implementations of the `Path` service. |
| 20 | * |
| 21 | * **Details** |
| 22 | * |
| 23 | * The marker is the exact string stored on `Path` service implementations. |
| 24 | * Most code should depend on the `Path` service instead of inspecting this |
| 25 | * value directly. |
| 26 | * |
| 27 | * @see {@link layer} for the built-in POSIX `Path` service layer |
| 28 | * |
| 29 | * @category type IDs |
| 30 | * @since 4.0.0 |
| 31 | */ |
| 32 | export const TypeId = "~effect/Path" |
| 33 | |
| 34 | /** |
| 35 | * Defines the service interface for platform-specific path manipulation. |
| 36 | * |
| 37 | * **When to use** |
| 38 | * |
| 39 | * Use to depend on path operations through the Effect environment instead of a |
| 40 | * concrete host path module. |
| 41 | * |
| 42 | * **Details** |
| 43 | * |
| 44 | * The service exposes operations for joining, normalizing, parsing, |
| 45 | * formatting, and converting file system paths. URL conversion methods return |
| 46 | * `Effect`s because invalid file URLs or paths can fail with `BadArgument`. |
| 47 | * |
| 48 | * **Example** (Using path operations) |
| 49 | * |
| 50 | * ```ts import.meta.vitest |
| 51 | * import { Effect, Path } from "effect" |
| 52 | * |
| 53 | * const program = Effect.gen(function*() { |
| 54 | * const path = yield* Path.Path |
| 55 | * |
| 56 | * return { |
| 57 | * joined: path.join("home", "user", "documents"), |
| 58 | * normalized: path.normalize("./path/../to/file.txt"), |
| 59 | * basename: path.basename("/path/to/file.txt"), |
| 60 | * dirname: path.dirname("/path/to/file.txt"), |
| 61 | * extname: path.extname("file.txt"), |
| 62 | * isAbsolute: path.isAbsolute("/absolute/path"), |
| 63 | * name: path.parse("/path/to/file.txt").name, |
| 64 | * relative: path.relative("/from/path", "/to/path"), |
| 65 | * resolved: path.resolve("/base", "relative", "path") |
| 66 | * } |
| 67 | * }) |
| 68 | * |
| 69 | * const result = Effect.runSync(Effect.provide(program, Path.layer)) |
| 70 | * result.joined // => "home/user/documents" |
| 71 | * result.normalized // => "to/file.txt" |
| 72 | * result.basename // => "file.txt" |
| 73 | * result.dirname // => "/path/to" |
| 74 | * result.extname // => ".txt" |
| 75 | * result.isAbsolute // => true |
| 76 | * result.name // => "file" |
| 77 | * result.relative // => "../../to/path" |
| 78 | * result.resolved // => "/base/relative/path" |
| 79 | * ``` |
| 80 | * |
| 81 | * @category services |
| 82 | * @since 4.0.0 |
| 83 | */ |
| 84 | export interface Path { |
| 85 | readonly [TypeId]: typeof TypeId |
| 86 | readonly sep: string |
| 87 | readonly basename: (path: string, suffix?: string) => string |
| 88 | readonly dirname: (path: string) => string |
| 89 | readonly extname: (path: string) => string |
| 90 | readonly format: (pathObject: Partial<Path.Parsed>) => string |
| 91 | readonly fromFileUrl: (url: URL) => Effect.Effect<string, BadArgument> |
| 92 | readonly isAbsolute: (path: string) => boolean |
| 93 | readonly join: (...paths: ReadonlyArray<string>) => string |
| 94 | readonly normalize: (path: string) => string |
| 95 | readonly parse: (path: string) => Path.Parsed |
| 96 | readonly relative: (from: string, to: string) => string |
| 97 | readonly resolve: (...pathSegments: ReadonlyArray<string>) => string |
| 98 | readonly toFileUrl: (path: string) => Effect.Effect<URL, BadArgument> |
| 99 | readonly toNamespacedPath: (path: string) => string |
| 100 | } |
| 101 | |
| 102 | /** |
| 103 | * Namespace containing types associated with the `Path` service. |
| 104 | * |
| 105 | * **When to use** |
| 106 | * |
| 107 | * Use to reference types associated with path parsing and formatting. |
| 108 | * |
| 109 | * **Example** (Working with parsed paths) |
| 110 | * |
| 111 | * ```ts import.meta.vitest |
| 112 | * import { Effect, Path } from "effect" |
| 113 | * |
| 114 | * // Access types and utilities in the Path namespace |
| 115 | * const program = Effect.gen(function*() { |
| 116 | * const path = yield* Path.Path |
| 117 | * |
| 118 | * // Parse a path and get a Path.Parsed object |
| 119 | * const parsed = path.parse("/home/user/file.txt") |
| 120 | * |
| 121 | * // The parsed object conforms to the Path.Parsed interface |
| 122 | * const exampleParsed = { |
| 123 | * root: "/", |
| 124 | * dir: "/home/user", |
| 125 | * base: "file.txt", |
| 126 | * ext: ".txt", |
| 127 | * name: "file" |
| 128 | * } |
| 129 | * |
| 130 | * return [parsed.base, exampleParsed.base] |
| 131 | * }) |
| 132 | * |
| 133 | * Effect.runSync(Effect.provide(program, Path.layer)) // => ["file.txt", "file.txt"] |
| 134 | * ``` |
| 135 | * |
| 136 | * @since 4.0.0 |
| 137 | */ |
| 138 | export declare namespace Path { |
| 139 | /** |
| 140 | * Structured representation of a parsed file system path. |
| 141 | * |
| 142 | * **When to use** |
| 143 | * |
| 144 | * Use to model the object form produced by `Path.parse` and consumed by |
| 145 | * `Path.format`. |
| 146 | * |
| 147 | * **Details** |
| 148 | * |
| 149 | * The fields correspond to the path root, directory, base filename, |
| 150 | * extension, and filename without extension, matching the shape consumed by |
| 151 | * `Path.format`. |
| 152 | * |
| 153 | * **Example** (Parsing and formatting paths) |
| 154 | * |
| 155 | * ```ts import.meta.vitest |
| 156 | * import { Effect, Path } from "effect" |
| 157 | * |
| 158 | * const program = Effect.gen(function*() { |
| 159 | * const path = yield* Path.Path |
| 160 | * |
| 161 | * // Parse a path into its components |
| 162 | * const parsed = path.parse("/home/user/documents/file.txt") |
| 163 | * // Format a path from its components |
| 164 | * const formatted = path.format({ |
| 165 | * dir: "/home/user", |
| 166 | * name: "newfile", |
| 167 | * ext: ".ts" |
| 168 | * }) |
| 169 | * return { dir: parsed.dir, base: parsed.base, formatted } |
| 170 | * }) |
| 171 | * |
| 172 | * const result = Effect.runSync(Effect.provide(program, Path.layer)) |
| 173 | * result.dir // => "/home/user/documents" |
| 174 | * result.base // => "file.txt" |
| 175 | * result.formatted // => "/home/user/newfile.ts" |
| 176 | * ``` |
| 177 | * |
| 178 | * @category models |
| 179 | * @since 4.0.0 |
| 180 | */ |
| 181 | export interface Parsed { |
| 182 | readonly root: string |
| 183 | readonly dir: string |
| 184 | readonly base: string |
| 185 | readonly ext: string |
| 186 | readonly name: string |
| 187 | } |
| 188 | } |
| 189 | |
| 190 | /** |
| 191 | * Service tag for accessing the current `Path` implementation. |
| 192 | * |
| 193 | * **When to use** |
| 194 | * |
| 195 | * Use when you need path operations supplied by an effect's environment. |
| 196 | * |
| 197 | * **Example** (Providing a custom Path service) |
| 198 | * |
| 199 | * ```ts import.meta.vitest |
| 200 | * import { Effect, Layer, Path } from "effect" |
| 201 | * |
| 202 | * // Create a custom path implementation |
| 203 | * const customPath: Path.Path = { |
| 204 | * [Path.TypeId]: Path.TypeId, |
| 205 | * sep: "/", |
| 206 | * basename: (path: string, suffix?: string) => { |
| 207 | * const base = path.split("/").pop() || "" |
| 208 | * return suffix && base.endsWith(suffix) |
| 209 | * ? base.slice(0, -suffix.length) |
| 210 | * : base |
| 211 | * }, |
| 212 | * dirname: (path: string) => path.split("/").slice(0, -1).join("/") || "/", |
| 213 | * extname: (path: string) => { |
| 214 | * const match = path.match(/\.[^.]*$/) |
| 215 | * return match ? match[0] : "" |
| 216 | * }, |
| 217 | * format: (pathObject) => { |
| 218 | * const dir = pathObject.dir || "" |
| 219 | * const name = pathObject.name || "" |
| 220 | * const ext = pathObject.ext || "" |
| 221 | * return dir ? `${dir}/${name}${ext}` : `${name}${ext}` |
| 222 | * }, |
| 223 | * fromFileUrl: (url: URL) => Effect.succeed(url.pathname), |
| 224 | * isAbsolute: (path: string) => path.startsWith("/"), |
| 225 | * join: (...paths: ReadonlyArray<string>) => paths.join("/"), |
| 226 | * normalize: (path: string) => path.replace(/\/+/g, "/"), |
| 227 | * parse: (path: string) => ({ |
| 228 | * root: path.startsWith("/") ? "/" : "", |
| 229 | * dir: path.split("/").slice(0, -1).join("/") || "/", |
| 230 | * base: path.split("/").pop() || "", |
| 231 | * ext: path.match(/\.[^.]*$/)?.[0] || "", |
| 232 | * name: path.split("/").pop()?.replace(/\.[^.]*$/, "") || "" |
| 233 | * }), |
| 234 | * relative: (from: string, to: string) => to.replace(from, ""), |
| 235 | * resolve: (...pathSegments: ReadonlyArray<string>) => pathSegments.join("/"), |
| 236 | * toFileUrl: (path: string) => Effect.succeed(new URL(`file://${path}`)), |
| 237 | * toNamespacedPath: (path: string) => path |
| 238 | * } |
| 239 | * |
| 240 | * // Provide the path service |
| 241 | * const customPathLayer = Layer.succeed(Path.Path)(customPath) |
| 242 | * |
| 243 | * const program = Effect.gen(function*() { |
| 244 | * const path = yield* Path.Path |
| 245 | * return path.join("home", "user", "file.txt") |
| 246 | * }) |
| 247 | * |
| 248 | * // Run with custom path implementation |
| 249 | * Effect.runSync(Effect.provide(program, customPathLayer)) // => "home/user/file.txt" |
| 250 | * ``` |
| 251 | * |
| 252 | * @category services |
| 253 | * @since 4.0.0 |
| 254 | */ |
| 255 | export const Path: Context.Service<Path, Path> = Context.Service("effect/Path") |
| 256 | |
| 257 | /** |
| 258 | * The following functions are adapted from the Node.js source code: |
| 259 | * https://github.com/nodejs/node/blob/main/lib/internal/url.js |
| 260 | * |
| 261 | * The following license applies to these functions: |
| 262 | * - MIT |
| 263 | */ |
| 264 | |
| 265 | // Resolves . and .. elements in a path with directory names |
| 266 | function normalizeStringPosix(path: string, allowAboveRoot: boolean) { |
| 267 | let res = "" |
| 268 | let lastSegmentLength = 0 |
| 269 | let lastSlash = -1 |
| 270 | let dots = 0 |
| 271 | let code |
| 272 | for (let i = 0; i <= path.length; ++i) { |
| 273 | if (i < path.length) { |
| 274 | code = path.charCodeAt(i) |
| 275 | } else if (code === 47 /*/*/) { |
| 276 | break |
| 277 | } else { |
| 278 | code = 47 /*/*/ |
| 279 | } |
| 280 | if (code === 47 /*/*/) { |
| 281 | if (lastSlash === i - 1 || dots === 1) { |
| 282 | // NOOP |
| 283 | } else if (lastSlash !== i - 1 && dots === 2) { |
| 284 | if ( |
| 285 | res.length < 2 || lastSegmentLength !== 2 || res.charCodeAt(res.length - 1) !== 46 /*.*/ || |
| 286 | res.charCodeAt(res.length - 2) !== 46 /*.*/ |
| 287 | ) { |
| 288 | if (res.length > 2) { |
| 289 | const lastSlashIndex = res.lastIndexOf("/") |
| 290 | if (lastSlashIndex !== res.length - 1) { |
| 291 | if (lastSlashIndex === -1) { |
| 292 | res = "" |
| 293 | lastSegmentLength = 0 |
| 294 | } else { |
| 295 | res = res.slice(0, lastSlashIndex) |
| 296 | lastSegmentLength = res.length - 1 - res.lastIndexOf("/") |
| 297 | } |
| 298 | lastSlash = i |
| 299 | dots = 0 |
| 300 | continue |
| 301 | } |
| 302 | } else if (res.length === 2 || res.length === 1) { |
| 303 | res = "" |
| 304 | lastSegmentLength = 0 |
| 305 | lastSlash = i |
| 306 | dots = 0 |
| 307 | continue |
| 308 | } |
| 309 | } |
| 310 | if (allowAboveRoot) { |
| 311 | if (res.length > 0) { |
| 312 | res += "/.." |
| 313 | } else { |
| 314 | res = ".." |
| 315 | } |
| 316 | lastSegmentLength = 2 |
| 317 | } |
| 318 | } else { |
| 319 | if (res.length > 0) { |
| 320 | res += "/" + path.slice(lastSlash + 1, i) |
| 321 | } else { |
| 322 | res = path.slice(lastSlash + 1, i) |
| 323 | } |
| 324 | lastSegmentLength = i - lastSlash - 1 |
| 325 | } |
| 326 | lastSlash = i |
| 327 | dots = 0 |
| 328 | } else if (code === 46 /*.*/ && dots !== -1) { |
| 329 | ++dots |
| 330 | } else { |
| 331 | dots = -1 |
| 332 | } |
| 333 | } |
| 334 | return res |
| 335 | } |
| 336 | |
| 337 | function _format(sep: string, pathObject: Partial<Path.Parsed>) { |
| 338 | const dir = pathObject.dir || pathObject.root |
| 339 | const base = pathObject.base || (pathObject.name || "") + (pathObject.ext || "") |
| 340 | if (!dir) { |
| 341 | return base |
| 342 | } |
| 343 | if (dir === pathObject.root) { |
| 344 | return dir + base |
| 345 | } |
| 346 | return dir + sep + base |
| 347 | } |
| 348 | |
| 349 | function fromFileUrl(url: URL): Effect.Effect<string, BadArgument> { |
| 350 | if (url.protocol !== "file:") { |
| 351 | return Effect.fail( |
| 352 | new BadArgument({ |
| 353 | module: "Path", |
| 354 | method: "fromFileUrl", |
| 355 | description: "URL must be of scheme file" |
| 356 | }) |
| 357 | ) |
| 358 | } else if (url.hostname !== "") { |
| 359 | return Effect.fail( |
| 360 | new BadArgument({ |
| 361 | module: "Path", |
| 362 | method: "fromFileUrl", |
| 363 | description: "Invalid file URL host" |
| 364 | }) |
| 365 | ) |
| 366 | } |
| 367 | const pathname = url.pathname |
| 368 | for (let n = 0; n < pathname.length; n++) { |
| 369 | if (pathname[n] === "%") { |
| 370 | const third = pathname.codePointAt(n + 2)! | 0x20 |
| 371 | if (pathname[n + 1] === "2" && third === 102) { |
| 372 | return Effect.fail( |
| 373 | new BadArgument({ |
| 374 | module: "Path", |
| 375 | method: "fromFileUrl", |
| 376 | description: "must not include encoded / characters" |
| 377 | }) |
| 378 | ) |
| 379 | } |
| 380 | } |
| 381 | } |
| 382 | return Effect.succeed(decodeURIComponent(pathname)) |
| 383 | } |
| 384 | |
| 385 | const resolve: Path["resolve"] = function resolve() { |
| 386 | let resolvedPath = "" |
| 387 | let resolvedAbsolute = false |
| 388 | let cwd: string | undefined = undefined |
| 389 | |
| 390 | for (let i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) { |
| 391 | let path: string |
| 392 | if (i >= 0) { |
| 393 | path = arguments[i] |
| 394 | } else { |
| 395 | const process = (globalThis as any).process |
| 396 | if ( |
| 397 | cwd === undefined && "process" in globalThis && |
| 398 | typeof process === "object" && |
| 399 | process !== null && |
| 400 | typeof process.cwd === "function" |
| 401 | ) { |
| 402 | cwd = process.cwd() |
| 403 | } |
| 404 | path = cwd! |
| 405 | } |
| 406 | |
| 407 | // Skip empty entries |
| 408 | if (path.length === 0) { |
| 409 | continue |
| 410 | } |
| 411 | |
| 412 | resolvedPath = path + "/" + resolvedPath |
| 413 | resolvedAbsolute = path.charCodeAt(0) === 47 /*/*/ |
| 414 | } |
| 415 | |
| 416 | // At this point the path should be resolved to a full absolute path, but |
| 417 | // handle relative paths to be safe (might happen when process.cwd() fails) |
| 418 | |
| 419 | // Normalize the path |
| 420 | resolvedPath = normalizeStringPosix(resolvedPath, !resolvedAbsolute) |
| 421 | |
| 422 | if (resolvedAbsolute) { |
| 423 | if (resolvedPath.length > 0) { |
| 424 | return "/" + resolvedPath |
| 425 | } else { |
| 426 | return "/" |
| 427 | } |
| 428 | } else if (resolvedPath.length > 0) { |
| 429 | return resolvedPath |
| 430 | } else { |
| 431 | return "." |
| 432 | } |
| 433 | } |
| 434 | |
| 435 | const CHAR_FORWARD_SLASH = 47 |
| 436 | |
| 437 | function toFileUrl(filepath: string) { |
| 438 | const outURL = new URL("file://") |
| 439 | let resolved = resolve(filepath) |
| 440 | // path.resolve strips trailing slashes so we must add them back |
| 441 | const filePathLast = filepath.charCodeAt(filepath.length - 1) |
| 442 | if ( |
| 443 | (filePathLast === CHAR_FORWARD_SLASH) && |
| 444 | resolved[resolved.length - 1] !== "/" |
| 445 | ) { |
| 446 | resolved += "/" |
| 447 | } |
| 448 | outURL.pathname = encodePathChars(resolved) |
| 449 | return Effect.succeed(outURL) |
| 450 | } |
| 451 | |
| 452 | const percentRegExp = /%/g |
| 453 | const backslashRegExp = /\\/g |
| 454 | const newlineRegExp = /\n/g |
| 455 | const carriageReturnRegExp = /\r/g |
| 456 | const tabRegExp = /\t/g |
| 457 | |
| 458 | function encodePathChars(filepath: string) { |
| 459 | if (filepath.includes("%")) { |
| 460 | filepath = filepath.replace(percentRegExp, "%25") |
| 461 | } |
| 462 | if (filepath.includes("\\")) { |
| 463 | filepath = filepath.replace(backslashRegExp, "%5C") |
| 464 | } |
| 465 | if (filepath.includes("\n")) { |
| 466 | filepath = filepath.replace(newlineRegExp, "%0A") |
| 467 | } |
| 468 | if (filepath.includes("\r")) { |
| 469 | filepath = filepath.replace(carriageReturnRegExp, "%0D") |
| 470 | } |
| 471 | if (filepath.includes("\t")) { |
| 472 | filepath = filepath.replace(tabRegExp, "%09") |
| 473 | } |
| 474 | return filepath |
| 475 | } |
| 476 | |
| 477 | const posixImpl = Path.of({ |
| 478 | [TypeId]: TypeId, |
| 479 | resolve, |
| 480 | normalize(path) { |
| 481 | if (path.length === 0) return "." |
| 482 | |
| 483 | const isAbsolute = path.charCodeAt(0) === 47 /*/*/ |
| 484 | const trailingSeparator = path.charCodeAt(path.length - 1) === 47 /*/*/ |
| 485 | |
| 486 | // Normalize the path |
| 487 | path = normalizeStringPosix(path, !isAbsolute) |
| 488 | |
| 489 | if (path.length === 0 && !isAbsolute) path = "." |
| 490 | if (path.length > 0 && trailingSeparator) path += "/" |
| 491 | |
| 492 | if (isAbsolute) return "/" + path |
| 493 | return path |
| 494 | }, |
| 495 | |
| 496 | isAbsolute(path) { |
| 497 | return path.length > 0 && path.charCodeAt(0) === 47 /*/*/ |
| 498 | }, |
| 499 | |
| 500 | join() { |
| 501 | if (arguments.length === 0) { |
| 502 | return "." |
| 503 | } |
| 504 | let joined |
| 505 | for (let i = 0; i < arguments.length; ++i) { |
| 506 | const arg = arguments[i] |
| 507 | if (arg.length > 0) { |
| 508 | if (joined === undefined) { |
| 509 | joined = arg |
| 510 | } else { |
| 511 | joined += "/" + arg |
| 512 | } |
| 513 | } |
| 514 | } |
| 515 | if (joined === undefined) { |
| 516 | return "." |
| 517 | } |
| 518 | return posixImpl.normalize(joined) |
| 519 | }, |
| 520 | |
| 521 | relative(from, to) { |
| 522 | if (from === to) return "" |
| 523 | |
| 524 | from = posixImpl.resolve(from) |
| 525 | to = posixImpl.resolve(to) |
| 526 | |
| 527 | if (from === to) return "" |
| 528 | |
| 529 | // Trim any leading backslashes |
| 530 | let fromStart = 1 |
| 531 | for (; fromStart < from.length; ++fromStart) { |
| 532 | if (from.charCodeAt(fromStart) !== 47 /*/*/) { |
| 533 | break |
| 534 | } |
| 535 | } |
| 536 | const fromEnd = from.length |
| 537 | const fromLen = fromEnd - fromStart |
| 538 | |
| 539 | // Trim any leading backslashes |
| 540 | let toStart = 1 |
| 541 | for (; toStart < to.length; ++toStart) { |
| 542 | if (to.charCodeAt(toStart) !== 47 /*/*/) { |
| 543 | break |
| 544 | } |
| 545 | } |
| 546 | const toEnd = to.length |
| 547 | const toLen = toEnd - toStart |
| 548 | |
| 549 | // Compare paths to find the longest common path from root |
| 550 | const length = fromLen < toLen ? fromLen : toLen |
| 551 | let lastCommonSep = -1 |
| 552 | let i = 0 |
| 553 | for (; i <= length; ++i) { |
| 554 | if (i === length) { |
| 555 | if (toLen > length) { |
| 556 | if (to.charCodeAt(toStart + i) === 47 /*/*/) { |
| 557 | // We get here if `from` is the exact base path for `to`. |
| 558 | // For example: from='/foo/bar'; to='/foo/bar/baz' |
| 559 | return to.slice(toStart + i + 1) |
| 560 | } else if (i === 0) { |
| 561 | // We get here if `from` is the root |
| 562 | // For example: from='/'; to='/foo' |
| 563 | return to.slice(toStart + i) |
| 564 | } |
| 565 | } else if (fromLen > length) { |
| 566 | if (from.charCodeAt(fromStart + i) === 47 /*/*/) { |
| 567 | // We get here if `to` is the exact base path for `from`. |
| 568 | // For example: from='/foo/bar/baz'; to='/foo/bar' |
| 569 | lastCommonSep = i |
| 570 | } else if (i === 0) { |
| 571 | // We get here if `to` is the root. |
| 572 | // For example: from='/foo'; to='/' |
| 573 | lastCommonSep = 0 |
| 574 | } |
| 575 | } |
| 576 | break |
| 577 | } |
| 578 | const fromCode = from.charCodeAt(fromStart + i) |
| 579 | const toCode = to.charCodeAt(toStart + i) |
| 580 | if (fromCode !== toCode) { |
| 581 | break |
| 582 | } else if (fromCode === 47 /*/*/) { |
| 583 | lastCommonSep = i |
| 584 | } |
| 585 | } |
| 586 | |
| 587 | let out = "" |
| 588 | // Generate the relative path based on the path difference between `to` |
| 589 | // and `from` |
| 590 | for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i) { |
| 591 | if (i === fromEnd || from.charCodeAt(i) === 47 /*/*/) { |
| 592 | if (out.length === 0) { |
| 593 | out += ".." |
| 594 | } else { |
| 595 | out += "/.." |
| 596 | } |
| 597 | } |
| 598 | } |
| 599 | |
| 600 | // Lastly, append the rest of the destination (`to`) path that comes after |
| 601 | // the common path parts |
| 602 | if (out.length > 0) { |
| 603 | return out + to.slice(toStart + lastCommonSep) |
| 604 | } else { |
| 605 | toStart += lastCommonSep |
| 606 | if (to.charCodeAt(toStart) === 47 /*/*/) { |
| 607 | ++toStart |
| 608 | } |
| 609 | return to.slice(toStart) |
| 610 | } |
| 611 | }, |
| 612 | |
| 613 | dirname(path) { |
| 614 | if (path.length === 0) return "." |
| 615 | let code = path.charCodeAt(0) |
| 616 | const hasRoot = code === 47 /*/*/ |
| 617 | let end = -1 |
| 618 | let matchedSlash = true |
| 619 | for (let i = path.length - 1; i >= 1; --i) { |
| 620 | code = path.charCodeAt(i) |
| 621 | if (code === 47 /*/*/) { |
| 622 | if (!matchedSlash) { |
| 623 | end = i |
| 624 | break |
| 625 | } |
| 626 | } else { |
| 627 | // We saw the first non-path separator |
| 628 | matchedSlash = false |
| 629 | } |
| 630 | } |
| 631 | |
| 632 | if (end === -1) return hasRoot ? "/" : "." |
| 633 | if (hasRoot && end === 1) return "//" |
| 634 | return path.slice(0, end) |
| 635 | }, |
| 636 | |
| 637 | basename(path, ext) { |
| 638 | let start = 0 |
| 639 | let end = -1 |
| 640 | let matchedSlash = true |
| 641 | let i |
| 642 | |
| 643 | if (ext !== undefined && ext.length > 0 && ext.length <= path.length) { |
| 644 | if (ext.length === path.length && ext === path) return "" |
| 645 | let extIdx = ext.length - 1 |
| 646 | let firstNonSlashEnd = -1 |
| 647 | for (i = path.length - 1; i >= 0; --i) { |
| 648 | const code = path.charCodeAt(i) |
| 649 | if (code === 47 /*/*/) { |
| 650 | // If we reached a path separator that was not part of a set of path |
| 651 | // separators at the end of the string, stop now |
| 652 | if (!matchedSlash) { |
| 653 | start = i + 1 |
| 654 | break |
| 655 | } |
| 656 | } else { |
| 657 | if (firstNonSlashEnd === -1) { |
| 658 | // We saw the first non-path separator, remember this index in case |
| 659 | // we need it if the extension ends up not matching |
| 660 | matchedSlash = false |
| 661 | firstNonSlashEnd = i + 1 |
| 662 | } |
| 663 | if (extIdx >= 0) { |
| 664 | // Try to match the explicit extension |
| 665 | if (code === ext.charCodeAt(extIdx)) { |
| 666 | if (--extIdx === -1) { |
| 667 | // We matched the extension, so mark this as the end of our path |
| 668 | // component |
| 669 | end = i |
| 670 | } |
| 671 | } else { |
| 672 | // Extension does not match, so our result is the entire path |
| 673 | // component |
| 674 | extIdx = -1 |
| 675 | end = firstNonSlashEnd |
| 676 | } |
| 677 | } |
| 678 | } |
| 679 | } |
| 680 | |
| 681 | if (start === end) end = firstNonSlashEnd |
| 682 | else if (end === -1) end = path.length |
| 683 | return path.slice(start, end) |
| 684 | } else { |
| 685 | for (i = path.length - 1; i >= 0; --i) { |
| 686 | if (path.charCodeAt(i) === 47 /*/*/) { |
| 687 | // If we reached a path separator that was not part of a set of path |
| 688 | // separators at the end of the string, stop now |
| 689 | if (!matchedSlash) { |
| 690 | start = i + 1 |
| 691 | break |
| 692 | } |
| 693 | } else if (end === -1) { |
| 694 | // We saw the first non-path separator, mark this as the end of our |
| 695 | // path component |
| 696 | matchedSlash = false |
| 697 | end = i + 1 |
| 698 | } |
| 699 | } |
| 700 | |
| 701 | if (end === -1) return "" |
| 702 | return path.slice(start, end) |
| 703 | } |
| 704 | }, |
| 705 | |
| 706 | extname(path) { |
| 707 | let startDot = -1 |
| 708 | let startPart = 0 |
| 709 | let end = -1 |
| 710 | let matchedSlash = true |
| 711 | // Track the state of characters (if any) we see before our first dot and |
| 712 | // after any path separator we find |
| 713 | let preDotState = 0 |
| 714 | for (let i = path.length - 1; i >= 0; --i) { |
| 715 | const code = path.charCodeAt(i) |
| 716 | if (code === 47 /*/*/) { |
| 717 | // If we reached a path separator that was not part of a set of path |
| 718 | // separators at the end of the string, stop now |
| 719 | if (!matchedSlash) { |
| 720 | startPart = i + 1 |
| 721 | break |
| 722 | } |
| 723 | continue |
| 724 | } |
| 725 | if (end === -1) { |
| 726 | // We saw the first non-path separator, mark this as the end of our |
| 727 | // extension |
| 728 | matchedSlash = false |
| 729 | end = i + 1 |
| 730 | } |
| 731 | if (code === 46 /*.*/) { |
| 732 | // If this is our first dot, mark it as the start of our extension |
| 733 | if (startDot === -1) { |
| 734 | startDot = i |
| 735 | } else if (preDotState !== 1) { |
| 736 | preDotState = 1 |
| 737 | } |
| 738 | } else if (startDot !== -1) { |
| 739 | // We saw a non-dot and non-path separator before our dot, so we should |
| 740 | // have a good chance at having a non-empty extension |
| 741 | preDotState = -1 |
| 742 | } |
| 743 | } |
| 744 | |
| 745 | if ( |
| 746 | startDot === -1 || end === -1 || |
| 747 | // We saw a non-dot character immediately before the dot |
| 748 | preDotState === 0 || |
| 749 | // The (right-most) trimmed path component is exactly '..' |
| 750 | preDotState === 1 && startDot === end - 1 && startDot === startPart + 1 |
| 751 | ) { |
| 752 | return "" |
| 753 | } |
| 754 | return path.slice(startDot, end) |
| 755 | }, |
| 756 | |
| 757 | format: function format(pathObject) { |
| 758 | if (pathObject === null || typeof pathObject !== "object") { |
| 759 | throw new TypeError("The \"pathObject\" argument must be of type Object. Received type " + typeof pathObject) |
| 760 | } |
| 761 | return _format("/", pathObject) |
| 762 | }, |
| 763 | |
| 764 | parse(path) { |
| 765 | const ret = { root: "", dir: "", base: "", ext: "", name: "" } |
| 766 | if (path.length === 0) return ret |
| 767 | let code = path.charCodeAt(0) |
| 768 | const isAbsolute = code === 47 /*/*/ |
| 769 | let start |
| 770 | if (isAbsolute) { |
| 771 | ret.root = "/" |
| 772 | start = 1 |
| 773 | } else { |
| 774 | start = 0 |
| 775 | } |
| 776 | let startDot = -1 |
| 777 | let startPart = 0 |
| 778 | let end = -1 |
| 779 | let matchedSlash = true |
| 780 | let i = path.length - 1 |
| 781 | |
| 782 | // Track the state of characters (if any) we see before our first dot and |
| 783 | // after any path separator we find |
| 784 | let preDotState = 0 |
| 785 | |
| 786 | // Get non-dir info |
| 787 | for (; i >= start; --i) { |
| 788 | code = path.charCodeAt(i) |
| 789 | if (code === 47 /*/*/) { |
| 790 | // If we reached a path separator that was not part of a set of path |
| 791 | // separators at the end of the string, stop now |
| 792 | if (!matchedSlash) { |
| 793 | startPart = i + 1 |
| 794 | break |
| 795 | } |
| 796 | continue |
| 797 | } |
| 798 | if (end === -1) { |
| 799 | // We saw the first non-path separator, mark this as the end of our |
| 800 | // extension |
| 801 | matchedSlash = false |
| 802 | end = i + 1 |
| 803 | } |
| 804 | if (code === 46 /*.*/) { |
| 805 | // If this is our first dot, mark it as the start of our extension |
| 806 | if (startDot === -1) startDot = i |
| 807 | else if (preDotState !== 1) preDotState = 1 |
| 808 | } else if (startDot !== -1) { |
| 809 | // We saw a non-dot and non-path separator before our dot, so we should |
| 810 | // have a good chance at having a non-empty extension |
| 811 | preDotState = -1 |
| 812 | } |
| 813 | } |
| 814 | |
| 815 | if ( |
| 816 | startDot === -1 || end === -1 || |
| 817 | // We saw a non-dot character immediately before the dot |
| 818 | preDotState === 0 || |
| 819 | // The (right-most) trimmed path component is exactly '..' |
| 820 | preDotState === 1 && startDot === end - 1 && startDot === startPart + 1 |
| 821 | ) { |
| 822 | if (end !== -1) { |
| 823 | if (startPart === 0 && isAbsolute) ret.base = ret.name = path.slice(1, end) |
| 824 | else ret.base = ret.name = path.slice(startPart, end) |
| 825 | } |
| 826 | } else { |
| 827 | if (startPart === 0 && isAbsolute) { |
| 828 | ret.name = path.slice(1, startDot) |
| 829 | ret.base = path.slice(1, end) |
| 830 | } else { |
| 831 | ret.name = path.slice(startPart, startDot) |
| 832 | ret.base = path.slice(startPart, end) |
| 833 | } |
| 834 | ret.ext = path.slice(startDot, end) |
| 835 | } |
| 836 | |
| 837 | if (startPart > 0) ret.dir = path.slice(0, startPart - 1) |
| 838 | else if (isAbsolute) ret.dir = "/" |
| 839 | |
| 840 | return ret |
| 841 | }, |
| 842 | |
| 843 | sep: "/", |
| 844 | fromFileUrl, |
| 845 | toFileUrl, |
| 846 | toNamespacedPath: identity |
| 847 | }) |
| 848 | |
| 849 | /** |
| 850 | * Layer that provides the built-in POSIX `Path` implementation. |
| 851 | * |
| 852 | * **When to use** |
| 853 | * |
| 854 | * Use when you need an effect that requires the `Path` service to run with the |
| 855 | * built-in POSIX path implementation. |
| 856 | * |
| 857 | * **Details** |
| 858 | * |
| 859 | * The layer provides a static service whose separator is `/` and whose |
| 860 | * operations use POSIX path semantics. |
| 861 | * |
| 862 | * @see {@link Path} for accessing the `Path` service from an effect |
| 863 | * |
| 864 | * @category layers |
| 865 | * @since 4.0.0 |
| 866 | */ |
| 867 | export const layer: Layer.Layer<Path> = Layer.succeed(Path)(posixImpl) |