@oazmi/superbuild 0.1.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.
Files changed (118) hide show
  1. package/esm/_dnt.shims.d.ts +2 -0
  2. package/esm/_dnt.shims.d.ts.map +1 -0
  3. package/esm/_dnt.shims.js +57 -0
  4. package/esm/deps/jsr.io/@oazmi/esbuild-types/0.28.0/src/mod.d.ts +598 -0
  5. package/esm/deps/jsr.io/@oazmi/esbuild-types/0.28.0/src/mod.d.ts.map +1 -0
  6. package/esm/deps/jsr.io/@oazmi/esbuild-types/0.28.0/src/mod.js +1 -0
  7. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/alias.d.ts +617 -0
  8. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/alias.d.ts.map +1 -0
  9. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/alias.js +671 -0
  10. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/array1d.d.ts +487 -0
  11. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/array1d.d.ts.map +1 -0
  12. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/array1d.js +536 -0
  13. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/array2d.d.ts +266 -0
  14. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/array2d.d.ts.map +1 -0
  15. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/array2d.js +318 -0
  16. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/binder.d.ts +368 -0
  17. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/binder.d.ts.map +1 -0
  18. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/binder.js +380 -0
  19. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/crossenv.d.ts +711 -0
  20. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/crossenv.d.ts.map +1 -0
  21. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/crossenv.js +1173 -0
  22. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/cryptoman.d.ts +171 -0
  23. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/cryptoman.d.ts.map +1 -0
  24. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/cryptoman.js +308 -0
  25. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/deps.d.ts +10 -0
  26. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/deps.d.ts.map +1 -0
  27. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/deps.js +10 -0
  28. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/eightpack.d.ts +168 -0
  29. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/eightpack.d.ts.map +1 -0
  30. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/eightpack.js +236 -0
  31. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/eightpack_varint.d.ts +90 -0
  32. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/eightpack_varint.d.ts.map +1 -0
  33. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/eightpack_varint.js +237 -0
  34. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/numericarray.d.ts +213 -0
  35. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/numericarray.d.ts.map +1 -0
  36. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/numericarray.js +363 -0
  37. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/numericmethods.d.ts +233 -0
  38. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/numericmethods.d.ts.map +1 -0
  39. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/numericmethods.js +256 -0
  40. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/pathman.d.ts +1436 -0
  41. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/pathman.d.ts.map +1 -0
  42. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/pathman.js +1730 -0
  43. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/promiseman.d.ts +647 -0
  44. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/promiseman.d.ts.map +1 -0
  45. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/promiseman.js +762 -0
  46. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/stringman.d.ts +538 -0
  47. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/stringman.d.ts.map +1 -0
  48. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/stringman.js +626 -0
  49. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/struct.d.ts +734 -0
  50. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/struct.d.ts.map +1 -0
  51. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/struct.js +764 -0
  52. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/typedbuffer.d.ts +137 -0
  53. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/typedbuffer.d.ts.map +1 -0
  54. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/typedbuffer.js +234 -0
  55. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/typedefs.d.ts +391 -0
  56. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/typedefs.d.ts.map +1 -0
  57. package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/typedefs.js +8 -0
  58. package/esm/deps.d.ts +56 -0
  59. package/esm/deps.d.ts.map +1 -0
  60. package/esm/deps.js +37 -0
  61. package/esm/esbuild/metafile.d.ts +90 -0
  62. package/esm/esbuild/metafile.d.ts.map +1 -0
  63. package/esm/esbuild/metafile.js +275 -0
  64. package/esm/esbuild/native.d.ts +95 -0
  65. package/esm/esbuild/native.d.ts.map +1 -0
  66. package/esm/esbuild/native.js +127 -0
  67. package/esm/esbuild/outputfile.d.ts +86 -0
  68. package/esm/esbuild/outputfile.d.ts.map +1 -0
  69. package/esm/esbuild/outputfile.js +221 -0
  70. package/esm/esbuild/strongtypes.d.ts +90 -0
  71. package/esm/esbuild/strongtypes.d.ts.map +1 -0
  72. package/esm/esbuild/strongtypes.js +8 -0
  73. package/esm/esbuild/typedefs.d.ts +121 -0
  74. package/esm/esbuild/typedefs.d.ts.map +1 -0
  75. package/esm/esbuild/typedefs.js +55 -0
  76. package/esm/esbuild/weaktypes.d.ts +46 -0
  77. package/esm/esbuild/weaktypes.d.ts.map +1 -0
  78. package/esm/esbuild/weaktypes.js +8 -0
  79. package/esm/funcdefs.d.ts +89 -0
  80. package/esm/funcdefs.d.ts.map +1 -0
  81. package/esm/funcdefs.js +145 -0
  82. package/esm/mod.d.ts +4 -0
  83. package/esm/mod.d.ts.map +1 -0
  84. package/esm/mod.js +2 -0
  85. package/esm/package.json +3 -0
  86. package/esm/plugins/emissions_driver.d.ts +33 -0
  87. package/esm/plugins/emissions_driver.d.ts.map +1 -0
  88. package/esm/plugins/emissions_driver.js +260 -0
  89. package/esm/plugins/long_build.d.ts +138 -0
  90. package/esm/plugins/long_build.d.ts.map +1 -0
  91. package/esm/plugins/long_build.js +288 -0
  92. package/esm/plugins/native_replica.d.ts +48 -0
  93. package/esm/plugins/native_replica.d.ts.map +1 -0
  94. package/esm/plugins/native_replica.js +122 -0
  95. package/esm/super/build.d.ts +53 -0
  96. package/esm/super/build.d.ts.map +1 -0
  97. package/esm/super/build.js +39 -0
  98. package/esm/super/build_context.d.ts +83 -0
  99. package/esm/super/build_context.d.ts.map +1 -0
  100. package/esm/super/build_context.js +124 -0
  101. package/esm/super/mod.d.ts +12 -0
  102. package/esm/super/mod.d.ts.map +1 -0
  103. package/esm/super/mod.js +9 -0
  104. package/esm/super/plugin.d.ts +23 -0
  105. package/esm/super/plugin.d.ts.map +1 -0
  106. package/esm/super/plugin.js +31 -0
  107. package/esm/super/plugin_build.d.ts +38 -0
  108. package/esm/super/plugin_build.d.ts.map +1 -0
  109. package/esm/super/plugin_build.js +198 -0
  110. package/esm/super/typedefs.d.ts +323 -0
  111. package/esm/super/typedefs.d.ts.map +1 -0
  112. package/esm/super/typedefs.js +10 -0
  113. package/esm/typedefs.d.ts +21 -0
  114. package/esm/typedefs.d.ts.map +1 -0
  115. package/esm/typedefs.js +5 -0
  116. package/license.md +37 -0
  117. package/package.json +88 -0
  118. package/readme.md +181 -0
@@ -0,0 +1,1730 @@
1
+ /** utility tools for manipulating paths and obtaining `URL`s.
2
+ *
3
+ * TODO: write document level examples.
4
+ *
5
+ * url terminology:
6
+ * - urls are a subset of uris
7
+ * - a url protocol is defined as: `[scheme]://`
8
+ * - a url is defined as: `[scheme]://[host]/[path]?[queryString]#[fragmentHash]`
9
+ * - or equivalently, a url is: `[protocol][host]/[path]?[queryString]#[fragmentHash]`
10
+ * - a uri is defined as: `[scheme]:[someIdentifier]`
11
+ *
12
+ * @module
13
+ */
14
+ import { array_from, dom_decodeURI, dom_encodeURI, object_entries } from "./alias.js";
15
+ import { DEBUG } from "./deps.js";
16
+ import { commonPrefix, quote } from "./stringman.js";
17
+ import { isObject, isString } from "./struct.js";
18
+ // DONE: consider if it would be a good idea to export `uri_protocol_and_scheme_mapping` so that the end user can manually modify it to their needs,
19
+ // and then have this whole submodule behave according to their custom uri scheme definitions.
20
+ // of course we're going to encounter issues with the `UriScheme` type, and might even have to turn it into a `string` (there by killing off all typing benefits),
21
+ // but it's still less painful than having the end user having to redefine all path resolution functions via wrappers.
22
+ /** this is global mapping of uri-protocol schemes that are identifiable by {@link getUriScheme} and {@link resolveAsUrl}.
23
+ * you may mutate this 2-tuple array to add or remove custom identifiable uri-schemes.
24
+ *
25
+ * @example
26
+ * adding a new uri protocol scheme named `"inline-scheme"` to our registry:
27
+ * ```ts
28
+ * import { assertEquals, assertThrows } from "jsr:@std/assert"
29
+ *
30
+ * // aliasing our functions for brevity
31
+ * const eq = assertEquals, err = assertThrows
32
+ *
33
+ * // initially, our custom "inline" scheme is unidentifiable, and cannot be used in `resolveAsUrl` as a base url
34
+ * eq(getUriScheme("inline://a/b/c.txt"), "relative")
35
+ * err(() => resolveAsUrl("./w.xyz", "inline://a/b/c.txt")) // "inline://a/b/c.txt" is identified as a relative path, and cannot be used as a base path
36
+ *
37
+ * // registering the custom protocol-scheme mapping.
38
+ * // note that you will have to declare `as any`, since the schemes are tightly defined by the type `UriScheme`.
39
+ * uriProtocolSchemeMap.push(["inline://", "inline-scheme" as any])
40
+ *
41
+ * // and now, our custom "inline" scheme becomes identifiable
42
+ * eq(getUriScheme("inline://a/b/c.txt"), "inline-scheme")
43
+ *
44
+ * // it is also now accepted by `resolveAsUrl` as a base uri
45
+ * eq(resolveAsUrl("./w.xyz", "inline://a/b/c.txt"), new URL("inline://a/b/w.xyz"))
46
+ * ```
47
+ */
48
+ export const uriProtocolSchemeMap = /*@__PURE__*/ object_entries({
49
+ "node:": "node",
50
+ "npm:": "npm",
51
+ "jsr:": "jsr",
52
+ "blob:": "blob",
53
+ "data:": "data",
54
+ "http://": "http",
55
+ "https://": "https",
56
+ "file://": "file",
57
+ "./": "relative",
58
+ "../": "relative",
59
+ });
60
+ /** here, you can specify which uri schemes cannot be used as a base url for resolving a url via the {@link resolveAsUrl} function.
61
+ *
62
+ * @example
63
+ * adding a new uri protocol scheme named `"base64-scheme"` to our registry, and then forbidding it from being used as a base url:
64
+ * ```ts
65
+ * import { assertEquals, assertThrows } from "jsr:@std/assert"
66
+ *
67
+ * // aliasing our functions for brevity
68
+ * const eq = assertEquals, err = assertThrows
69
+ *
70
+ * // initially, our custom "base64" scheme is unidentifiable, and cannot be used in `resolveAsUrl` as a base url
71
+ * eq(getUriScheme("base64://a/b/c.txt"), "relative")
72
+ * err(() => resolveAsUrl("./w.xyz", "base64://a/b/c.txt")) // "base64://a/b/c.txt" is identified as a relative path, and cannot be used as a base path
73
+ *
74
+ * // registering the custom protocol-scheme mapping.
75
+ * // note that you will have to declare `as any`, since the schemes are tightly defined by the type `UriScheme`.
76
+ * uriProtocolSchemeMap.push(["base64://", "base64-scheme" as any])
77
+ *
78
+ * // and now, our custom "base64" scheme becomes identifiable.
79
+ * eq(getUriScheme("base64://a/b/c.txt"), "base64-scheme")
80
+ *
81
+ * // it is also now accepted by `resolveAsUrl` as a base uri
82
+ * eq(resolveAsUrl("./w.xyz", "base64://a/b/c.txt"), new URL("base64://a/b/w.xyz"))
83
+ *
84
+ * // since we don't want to make it possible to have "base64-scheme" as a base uri, so we'll put it in the forbidden list.
85
+ * // once again `as any` is needed, since the `UriScheme` is tightly defined, and its definition cannot be changed.
86
+ * forbiddenBaseUriSchemes.push("base64-scheme" as any)
87
+ * err(() => resolveAsUrl("./w.xyz", "base64://a/b/c.txt")) // "base64://a/b/c.txt" is now amongst the forbidden schemes that cannot be combined with relative paths.
88
+ * eq(resolveAsUrl("base64://a/b/c.txt"), new URL("base64://a/b/c.txt")) // this is of course not stopping us from building urls with the "base64" scheme, so long as no relative path is attached.
89
+ * ```
90
+ */
91
+ export const forbiddenBaseUriSchemes = ["blob", "data", "relative"];
92
+ const packageUriSchemes = ["jsr", "npm", "node"], packageUriProtocols = ["jsr:", "npm:", "node:"];
93
+ const
94
+ // posix directory path separator
95
+ sep = "/",
96
+ // posix relative directory path navigator
97
+ dotslash = "./",
98
+ // posix relative parent directory path navigator
99
+ dotdotslash = "../",
100
+ // regex for attaining windows directory path separator ("\\")
101
+ windows_directory_slash_regex = /\\/g,
102
+ // regex for detecting if a path is an absolute windows path
103
+ windows_absolute_path_regex = /^[a-z]\:[\/\\]/i,
104
+ // regex for correcting an invalid single leading slash in a windows path
105
+ windows_leading_slash_correction_regex = /^[\/\\]([a-z])\:[\/\\]/i,
106
+ // regex for attaining leading consecutive slashes
107
+ leading_slashes_regex = /^\/+/,
108
+ // regex for attaining trailing consecutive slashes, except for those that are preceded by a dotslash ("/./") or a dotdotslash ("/../")
109
+ trailing_slashes_regex = /(?<!\/\.\.?)\/+$/,
110
+ // regex for attaining leading consecutive slashes and dot-slashes
111
+ leading_slashes_and_dot_slashes_regex = /^(\.?\/)+/,
112
+ // regex for attaining a reversed path string's trailing consecutive slashes and dot-slashes, but not the slashes preceded by a dotdotslash ("/..").
113
+ // this regex is a little complex, so you might want to check out the test cases (of reversed path strings) here: "https://regex101.com/r/IV0twv/1"
114
+ reversed_trailing_slashes_and_dot_slashes_regex = /^(\/\.?(?![^\/]))*(\/(?!\.\.\/))?/,
115
+ // regex for attaining the file name of a path, including its leading slash (if there is one)
116
+ filename_regex = /\/?[^\/]+$/,
117
+ // regex for attaining the base name and extension name of a file, from its filename (no directories)
118
+ basename_and_extname_regex = /^(?<basename>.+?)(?<ext>\.[^\.]+)?$/,
119
+ // an npm or jsr package string parsing regex. see the test cases on regex101 link: "https://regex101.com/r/mX3v1z/2"
120
+ package_regex = /^(?<protocol>jsr:|npm:|node:)(\/*(@(?<scope>[^\/\s]+)\/)?(?<pkg>[^@\/\s]+)(@(?<version>[^\/\r\n\t\f\v]+))?)?(?<pathname>\/.*)?$/, string_starts_with = (str, starts_with) => str.startsWith(starts_with), string_ends_with = (str, ends_with) => str.endsWith(ends_with);
121
+ /** test whether a given path is an absolute path (either windows or posix).
122
+ *
123
+ * > [!note]
124
+ * > currently, we do consider the tilde expansion ("~") as an absolute path, even though it is not an os/fs-level path, but rather a shell feature.
125
+ * > this may result in misclassification on windows, since "~" is a valid starting character for a file or folder name
126
+ *
127
+ * @example
128
+ * ```ts
129
+ * import { assertEquals } from "jsr:@std/assert"
130
+ *
131
+ * // aliasing our functions for brevity
132
+ * const eq = assertEquals, fn = isAbsolutePath
133
+ *
134
+ * eq(fn("/a/b/c.txt"), true)
135
+ * eq(fn("~/a/b/c.txt"), true)
136
+ * eq(fn("C:/a/b/c.txt"), true)
137
+ * eq(fn("/c:/a/b/c.txt"), true)
138
+ *
139
+ * eq(fn("a/b/c.txt"), false)
140
+ * eq(fn("./a/b/c.txt"), false)
141
+ * eq(fn("../a/b/c.txt"), false)
142
+ * ```
143
+ */
144
+ export const isAbsolutePath = (path) => {
145
+ return (string_starts_with(path, sep)
146
+ || string_starts_with(path, "~")
147
+ || windows_absolute_path_regex.test(path));
148
+ };
149
+ /** guesses the scheme of a url string. see {@link UriScheme} for more details.
150
+ *
151
+ * @example
152
+ * ```ts
153
+ * import { assertEquals } from "jsr:@std/assert"
154
+ *
155
+ * // aliasing our functions for brevity
156
+ * const eq = assertEquals, fn = getUriScheme
157
+ *
158
+ * eq(fn("C:/Users/me/path/to/file.txt"), "local")
159
+ * eq(fn("~/path/to/file.txt"), "local")
160
+ * eq(fn("/usr/me/path/to/file.txt"), "local")
161
+ * eq(fn("path/to/file.txt"), "relative")
162
+ * eq(fn("./path/to/file.txt"), "relative")
163
+ * eq(fn("../path/to/file.txt"), "relative")
164
+ * eq(fn("file:///c://users/me/path/to/file.txt"), "file")
165
+ * eq(fn("file:///usr/me/path/to/file.txt"), "file")
166
+ * eq(fn("jsr:@user/path/to/file"), "jsr")
167
+ * eq(fn("jsr:/@user/path/to/file"), "jsr")
168
+ * eq(fn("npm:lib/path/to/file"), "npm")
169
+ * eq(fn("npm:/lib/path/to/file"), "npm")
170
+ * eq(fn("npm:/@scope/lib/path/to/file"), "npm")
171
+ * eq(fn("node:http"), "node")
172
+ * eq(fn("node:fs/promises"), "node")
173
+ * eq(fn("data:text/plain;charset=utf-8;base64,aGVsbG8="), "data")
174
+ * eq(fn("blob:https://example.com/4800d2d8-a78c-4895"), "blob")
175
+ * eq(fn("http://google.com/style.css"), "http")
176
+ * eq(fn("https://google.com/style.css"), "https")
177
+ * eq(fn(""), undefined)
178
+ * ```
179
+ */
180
+ export const getUriScheme = (path) => {
181
+ if (!path || path === "") {
182
+ return undefined;
183
+ }
184
+ for (const [protocol, scheme] of uriProtocolSchemeMap) {
185
+ if (string_starts_with(path, protocol)) {
186
+ return scheme;
187
+ }
188
+ }
189
+ return isAbsolutePath(path) ? "local" : "relative";
190
+ };
191
+ /** this function parses npm and jsr package strings, and returns a pseudo URL-like object.
192
+ *
193
+ * the regex we use for parsing the input `href` string is quoted below:
194
+ * > /^(?<protocol>jsr:|npm:|node:)(\/*(@(?<scope>[^\/\s]+)\/)?(?<pkg>[^@\/\s]+)(@(?<version>[^\/\r\n\t\f\v]+))?)?(?<pathname>\/.*)?$/
195
+ *
196
+ * see the regex in action with the test cases on regex101 link: [regex101.com/r/mX3v1z/2](https://regex101.com/r/mX3v1z/2)
197
+ *
198
+ * @throws `Error` an error will be thrown if either the package name (`pkg`), or the `protocol` cannot be deduced by the regex.
199
+ *
200
+ * @example
201
+ * ```ts
202
+ * import { assertEquals, assertThrows } from "jsr:@std/assert"
203
+ *
204
+ * // aliasing our functions for brevity
205
+ * const eq = assertEquals, err = assertThrows, fn = parsePackageUrl
206
+ *
207
+ * // basic breakdown of a package's resource uri
208
+ * eq(fn("jsr:@scope/package@version/pathname/file.ts"), {
209
+ * href: "jsr:/@scope/package@version/pathname/file.ts",
210
+ * protocol: "jsr:",
211
+ * scope: "scope",
212
+ * pkg: "package",
213
+ * version: "version",
214
+ * pathname: "/pathname/file.ts",
215
+ * host: "@scope/package@version",
216
+ * })
217
+ *
218
+ * // showing that jsr package uri's without a scope are perfectly permitted.
219
+ * // even though it isn't actually possible to do so on "jsr.io".
220
+ * // thus it is left up to the end-user to make of it what they will.
221
+ * eq(fn("jsr:package@version/pathname/"), {
222
+ * href: "jsr:/package@version/pathname/",
223
+ * protocol: "jsr:",
224
+ * scope: undefined,
225
+ * pkg: "package",
226
+ * version: "version",
227
+ * pathname: "/pathname/",
228
+ * host: "package@version",
229
+ * })
230
+ *
231
+ * // testing a case with multiple slashes ("/") after the protocol colon (":"), and no trailing slash after the version
232
+ * eq(fn("npm:///@scope/package@version"), {
233
+ * href: "npm:/@scope/package@version/",
234
+ * protocol: "npm:",
235
+ * scope: "scope",
236
+ * pkg: "package",
237
+ * version: "version",
238
+ * pathname: "/",
239
+ * host: "@scope/package@version",
240
+ * })
241
+ *
242
+ * // testing a no-scope and no-version case
243
+ * eq(fn("npm:package"), {
244
+ * href: "npm:/package/",
245
+ * protocol: "npm:",
246
+ * scope: undefined,
247
+ * pkg: "package",
248
+ * version: undefined,
249
+ * pathname: "/",
250
+ * host: "package",
251
+ * })
252
+ *
253
+ * // testing the "node:" protocol
254
+ * eq(fn("node:fs"), {
255
+ * href: "node:/fs/",
256
+ * protocol: "node:",
257
+ * scope: undefined,
258
+ * pkg: "fs",
259
+ * version: undefined,
260
+ * pathname: "/",
261
+ * host: "fs",
262
+ * })
263
+ *
264
+ * // testing the "node:" protocol with a certain pathname
265
+ * eq(fn("node:fs/promises"), {
266
+ * href: "node:/fs/promises",
267
+ * protocol: "node:",
268
+ * scope: undefined,
269
+ * pkg: "fs",
270
+ * version: undefined,
271
+ * pathname: "/promises",
272
+ * host: "fs",
273
+ * })
274
+ *
275
+ * // testing a `version` query string that contains whitespaces and url-encoded characters.
276
+ * // NOTE: the url-encoded characters in vs-code's doc popup appear decoded, so don't be fooled!
277
+ * // but the `host` is always a url-decoded string.
278
+ * eq(fn("jsr:@scope/package@1.0.0 - 1.2.0/pathname/file.ts"), {
279
+ * href: "jsr:/@scope/package@1.0.0%20-%201.2.0/pathname/file.ts",
280
+ * protocol: "jsr:",
281
+ * scope: "scope",
282
+ * pkg: "package",
283
+ * version: "1.0.0 - 1.2.0",
284
+ * pathname: "/pathname/file.ts",
285
+ * host: "@scope/package@1.0.0 - 1.2.0",
286
+ * })
287
+ *
288
+ * // testing a `version` query string that has its some of its characters (such as whitespaces) url-encoded.
289
+ * // NOTE: the url-encoded characters in vs-code's doc popup appear decoded, so don't be fooled!
290
+ * // but the `host` is always a url-decoded string.
291
+ * eq(fn("jsr:@scope/package@^2%20<2.2%20||%20>%202.3/pathname/file.ts"), {
292
+ * href: "jsr:/@scope/package@%5E2%20%3C2.2%20%7C%7C%20%3E%202.3/pathname/file.ts",
293
+ * protocol: "jsr:",
294
+ * scope: "scope",
295
+ * pkg: "package",
296
+ * version: "^2 <2.2 || > 2.3",
297
+ * pathname: "/pathname/file.ts",
298
+ * host: "@scope/package@^2 <2.2 || > 2.3",
299
+ * })
300
+ *
301
+ * // testing cases where an error should be invoked
302
+ * err(() => fn("npm:@scope/")) // missing a package name
303
+ * err(() => fn("npm:@scope//package")) // more than one slash after scope
304
+ * err(() => fn("pnpm:@scope/package@version")) // only "node:", "npm:", and "jsr:" protocols are recognized
305
+ * ```
306
+ */
307
+ export const parsePackageUrl = (url_href) => {
308
+ url_href = dom_decodeURI(isString(url_href) ? url_href : url_href.href);
309
+ const { protocol, scope: scope_str, pkg, version: version_str, pathname: pathname_str } = package_regex.exec(url_href)?.groups ?? {};
310
+ if ((protocol === undefined) || (pkg === undefined)) {
311
+ throw new Error(DEBUG.ERROR ? ("invalid package url format was provided: " + url_href) : "");
312
+ }
313
+ const scope = scope_str ? scope_str : undefined, // turn empty strings into `undefined`
314
+ version = version_str ? version_str : undefined, // turn empty strings into `undefined`
315
+ pathname = pathname_str ? pathname_str : sep, // pathname must always begin with a leading slash, even if it was originally empty
316
+ host = `${scope ? "@" + scope + sep : ""}${pkg}${version ? "@" + version : ""}`, href = dom_encodeURI(`${protocol}/${host}${pathname}`);
317
+ return {
318
+ protocol: protocol,
319
+ scope, pkg, version, pathname, host, href,
320
+ };
321
+ };
322
+ /** convert a url string to an actual `URL` object.
323
+ * your input `path` url can use any scheme supported by the {@link getUriScheme} function.
324
+ * and you may also use paths with windows dir-separators ("\\"), as this function implicitly converts them a posix separator ("/").
325
+ *
326
+ * if you pass a `URL` object, then it will be returned as is.
327
+ *
328
+ * @throws `Error` an error will be thrown if `base` uri is either a relative path, or uses a data uri scheme,
329
+ * or if the provided `path` is relative, but no absolute `base` path is provided.
330
+ *
331
+ * @example
332
+ * ```ts
333
+ * import { assertEquals, assertThrows } from "jsr:@std/assert"
334
+ *
335
+ * // aliasing our functions for brevity
336
+ * const eq = assertEquals, err = assertThrows, fn = resolveAsUrl
337
+ *
338
+ * eq(fn(new URL("some://url:8000/a/b c.txt")), new URL("some://url:8000/a/b%20c.txt"))
339
+ *
340
+ * eq(fn("/a/b/c d e.txt"), new URL("file:///a/b/c%20d%20e.txt"))
341
+ * eq(fn("~/a/b/c.txt"), new URL("file://~/a/b/c.txt"))
342
+ * eq(fn("C:/a/b/c/d/e.txt"), new URL("file:///C:/a/b/c/d/e.txt"))
343
+ * eq(fn("C:\\a\\b\\c\\d\\e.txt"), new URL("file:///C:/a/b/c/d/e.txt"))
344
+ * eq(fn("./e/f g.txt", "C:/a\\b\\c d/"), new URL("file:///C:/a/b/c%20d/e/f%20g.txt"))
345
+ * eq(fn("../c d/e/f g.txt", "C:/a/b/c d/"), new URL("file:///C:/a/b/c%20d/e/f%20g.txt"))
346
+ * eq(fn("d/../.././e.txt", "C:/a/b/c/"), new URL("file:///C:/a/b/e.txt"))
347
+ * eq(fn("d/../.././e.txt", "C:/a/b/c"), new URL("file:///C:/a/e.txt"))
348
+ * eq(fn("D:/a/b.txt", "C:/c/d.txt"), new URL("file:///D:/a/b.txt"))
349
+ * eq(fn("/a/b.txt", "C:/c/d.txt"), new URL("file:///C:/a/b.txt"))
350
+ * eq(fn("/a/b.txt", "/sys/admin/"), new URL("file:///a/b.txt"))
351
+ * eq(fn("/a/b.txt", ""), new URL("file:///a/b.txt"))
352
+ *
353
+ * eq(fn("http://cdn.esm.sh/a/b/c.txt"), new URL("http://cdn.esm.sh/a/b/c.txt"))
354
+ * eq(fn("http://cdn.esm.sh/a.txt", "file:///b/"), new URL("http://cdn.esm.sh/a.txt"))
355
+ * eq(fn("http://cdn.esm.sh/a.txt", "/b/"), new URL("http://cdn.esm.sh/a.txt"))
356
+ * eq(fn("/a/b/c.txt", "http://cdn.esm.sh/"), new URL("http://cdn.esm.sh/a/b/c.txt"))
357
+ *
358
+ * eq(fn("b/c.txt", "http://cdn.esm.sh/a/"), new URL("http://cdn.esm.sh/a/b/c.txt"))
359
+ * eq(fn("b/c.txt", "http://cdn.esm.sh/a"), new URL("http://cdn.esm.sh/b/c.txt"))
360
+ * eq(fn("./b/c.txt", "http://cdn.esm.sh/a/"), new URL("http://cdn.esm.sh/a/b/c.txt"))
361
+ * eq(fn("./b/c.txt", "http://cdn.esm.sh/a"), new URL("http://cdn.esm.sh/b/c.txt"))
362
+ * eq(fn("../b/c.txt", "https://cdn.esm.sh/a/"), new URL("https://cdn.esm.sh/b/c.txt"))
363
+ * eq(fn("../c/d.txt", "https://cdn.esm.sh/a/b"), new URL("https://cdn.esm.sh/c/d.txt"))
364
+ * eq(fn("/c/d.txt", "https://cdn.esm.sh/a/b"), new URL("https://cdn.esm.sh/c/d.txt"))
365
+ *
366
+ * eq(fn("node:fs"), new URL("node:/fs/"))
367
+ * eq(fn("node:fs/promises"), new URL("node:/fs/promises"))
368
+ * eq(fn("promises", "node:fs"), new URL("node:/fs/promises"))
369
+ * eq(fn("./promises", "node:fs"), new URL("node:/fs/promises"))
370
+ * eq(fn("./promises", "node:fs/"), new URL("node:/fs/promises"))
371
+ * eq(fn("mkdir", "node:fs/promises"), new URL("node:/fs/mkdir"))
372
+ * eq(fn("mkdir", "node:fs/promises/"), new URL("node:/fs/promises/mkdir"))
373
+ * eq(fn("./mkdir", "node:fs/promises"), new URL("node:/fs/mkdir"))
374
+ * eq(fn("./mkdir", "node:fs/promises/"), new URL("node:/fs/promises/mkdir"))
375
+ * eq(fn("/sync", "node:fs/promises/mkdir"), new URL("node:/fs/sync"))
376
+ *
377
+ * eq(fn("npm:react"), new URL("npm:/react/"))
378
+ * eq(fn("npm:react/file.txt"), new URL("npm:/react/file.txt"))
379
+ * eq(fn("npm:@facebook/react"), new URL("npm:/@facebook/react/"))
380
+ * eq(fn("./to/file.txt", "npm:react"), new URL("npm:/react/to/file.txt"))
381
+ * eq(fn("./to/file.txt", "npm:react/"), new URL("npm:/react/to/file.txt"))
382
+ * eq(fn("/to/file.txt", "npm:react/native/bin"), new URL("npm:/react/to/file.txt"))
383
+ * eq(fn("npm:react@19/jsx runtime.ts"), new URL("npm:/react@19/jsx%20runtime.ts"))
384
+ * eq(fn("npm:react@^19 <19.5/jsx.ts"), new URL("npm:/react@%5E19%20%3C19.5/jsx.ts"))
385
+ *
386
+ * eq(fn("jsr:@scope/my-lib/b.txt"), new URL("jsr:/@scope/my-lib/b.txt"))
387
+ * eq(fn("a/b.txt", "jsr:///@scope/my-lib"), new URL("jsr:/@scope/my-lib/a/b.txt"))
388
+ * eq(fn("./a/b.txt", "jsr:///@scope/my-lib"), new URL("jsr:/@scope/my-lib/a/b.txt"))
389
+ * eq(fn("a/b.txt", "jsr:///@scope/my-lib/c"), new URL("jsr:/@scope/my-lib/a/b.txt"))
390
+ * eq(fn("./a/b.txt", "jsr:///@scope/my-lib/c"), new URL("jsr:/@scope/my-lib/a/b.txt"))
391
+ * eq(fn("./a/b.txt", "jsr:///@scope/my-lib//c"), new URL("jsr:/@scope/my-lib/a/b.txt"))
392
+ * eq(fn("../a/b.txt", "jsr:/@scope/my-lib///c/"), new URL("jsr:/@scope/my-lib/a/b.txt"))
393
+ * eq(fn("./a/b.txt", "jsr:///@scope/my-lib/c/"), new URL("jsr:/@scope/my-lib/c/a/b.txt"))
394
+ * eq(fn("/a/b.txt", "jsr:my-lib/x/y/"), new URL("jsr:/my-lib/a/b.txt"))
395
+ * eq(fn("/a/b.txt", "jsr:@scope/my-lib/x/y/z"), new URL("jsr:/@scope/my-lib/a/b.txt"))
396
+ * eq(fn("/a/b.txt", "jsr:my-lib@1 || 2/x/y/z"), new URL("jsr:/my-lib@1%20%7C%7C%202/a/b.txt"))
397
+ * eq(fn("/a/b.txt", "jsr:@my/lib@1||2/x/y/z"), new URL("jsr:/@my/lib@1%7C%7C2/a/b.txt"))
398
+ *
399
+ * eq(fn("C:/a/b.txt", "jsr:@my/lib/x/y"), new URL("file:///C:/a/b.txt"))
400
+ * eq(fn("jsr:@my/lib/x/y", "C:/a/b.txt"), new URL("jsr:/@my/lib/x/y"))
401
+ * eq(fn("http://test.io/abc", "C:/a/b.txt"), new URL("http://test.io/abc"))
402
+ *
403
+ * eq(fn("blob:https://example.com/480-a78"), new URL("blob:https://example.com/480-a78"))
404
+ * eq(fn("data:text/plain;utf8,hello"), new URL("data:text/plain;utf8,hello"))
405
+ * eq(fn("data:text/plain;utf8,hello", "C:/a/b/"), new URL("data:text/plain;utf8,hello"))
406
+ *
407
+ * err(() => fn("./a/b.txt", "data:text/plain;charset=utf-8;base64,aGVsbG8="))
408
+ * err(() => fn("./a/b.txt", "blob:https://example.com/4800d2d8-a78c-4895-b68b-3690b69a0d6a"))
409
+ * err(() => fn("./a/b.txt", "./path/")) // a base path must not be relative
410
+ * err(() => fn("./a/b.txt")) // a relative path cannot be resolved on its own without a base path
411
+ * err(() => fn("./a/b.txt", "")) // an empty base path is as good as a non-existing one
412
+ * err(() => fn("fs/promises", "node:")) // the base protocol ("node:") MUST be accompanied with a package name
413
+ * ```
414
+ */
415
+ export const resolveAsUrl = (path, base) => {
416
+ if (!isString(path)) {
417
+ return path;
418
+ }
419
+ path = pathToPosixPath(path);
420
+ let base_url = base;
421
+ if (isString(base) && base !== "") {
422
+ const base_scheme = getUriScheme(base);
423
+ if (forbiddenBaseUriSchemes.includes(base_scheme)) {
424
+ throw new Error(DEBUG.ERROR ? ("the following base scheme (url-protocol) is not supported: " + base_scheme) : "");
425
+ }
426
+ base_url = resolveAsUrl(base);
427
+ }
428
+ const path_scheme = getUriScheme(path), base_protocol = base_url ? base_url.protocol : undefined, path_is_package = packageUriSchemes.includes(path_scheme), base_is_package = packageUriProtocols.includes(base_protocol), path_is_root = string_starts_with(path, "/"), path_is_local = path_scheme === "local", path_is_relative = path_scheme === "relative";
429
+ // handling cases like: `fn("jsr://@hello/world/c/d.txt", "jsr://@scope/lib/a/b.ts") => "jsr://@hello/world/c/d.txt"`
430
+ if (path_is_package) {
431
+ // if the `path`'s protocol scheme is that of a package (i.e. "jsr", "npm", or "node"), then we're going to it handle slightly differently,
432
+ // since it is possible for it to be non-parsable by the `URL` constructor if there is not trailing slash after the "npm:" or "jsr:" protocol.
433
+ // thus we normalize our `path` by passing it to the `parsePackageUrl` function, and acquiring the normalized `URL` compatible `href` representation of the full `path`.
434
+ return new URL(parsePackageUrl(path).href);
435
+ }
436
+ // if the base protocol's scheme is either "jsr" or "npm", then we're going to handle slightly differently, since it is possible for it to be non-parsable by the `URL` constructor if there is not trailing slash after the "npm:" or "jsr:" protocol.
437
+ // the path joining rules of packages is different from an http url, which supports the domain name as the host. such an equivalent construction cannot be made for jsr or npm package strings.
438
+ if (base_url && base_is_package && (path_is_root || path_is_relative)) {
439
+ // to start off, we parse the `protocol`, `host` (= scope + package_name + version), and any existing `pathname` of the `base_url` using the `parsePackageUrl` function.
440
+ // note that `pathname` always starts with a leading "/"
441
+ const { host, protocol, pathname } = parsePackageUrl(base_url);
442
+ // handling cases like: `fn("/c/d.txt", "jsr://@scope/lib/a/b.ts") => "jsr://@scope/lib/c/d.txt"`
443
+ if (path_is_root) {
444
+ return new URL(`${protocol}/${dom_encodeURI(host)}${dom_encodeURI(path)}`);
445
+ }
446
+ // handling cases like: `fn("../c/d.txt", "jsr://@scope/lib/a/b.ts") => "jsr://@scope/lib/a/c/d.txt"`
447
+ if (path_is_relative) {
448
+ // here, we join the pre-existing base url's `pathname` with the relative `paths`, by exploiting the URL constructor to do the joining part for us, by giving it a fake protocol named "x:".
449
+ const full_pathname = (new URL(path, "x:" + pathname)).pathname; // `full_pathname` is now url-encoded
450
+ return new URL(`${protocol}/${dom_encodeURI(host)}${full_pathname}`);
451
+ }
452
+ }
453
+ // handling cases like:
454
+ // - `fn("/c/de.txt", "https://example.com/a/b.txt") => "https://example.com/c/de.txt"`
455
+ // - `fn("./c/d.txt", "https://example.com/a/b.txt") => "https://example.com/a/b/c/d.txt"`
456
+ if (base_url && (path_is_root || path_is_relative)) {
457
+ return new URL(path, base_url);
458
+ }
459
+ // handling cases like:
460
+ // - `fn("/a/b c d.txt") => "file:///a/b%20c%20d.txt"`
461
+ // - `fn("C:/a/b d.txt") => "file:///C:/a/b%20d.txt"`
462
+ // - `fn("~/a/b/cd.txt") => "file://~/a/b%20d.txt"`
463
+ if (path_is_local) {
464
+ return new URL("file://" + dom_encodeURI(path));
465
+ }
466
+ // handling all other situations with absolute `path`, such as `http://`, `file://`, `blob:`, and `data:`
467
+ return new URL(path);
468
+ };
469
+ /** trim the leading forward-slashes at the beginning of a string.
470
+ *
471
+ * @example
472
+ * ```ts
473
+ * import { assertEquals } from "jsr:@std/assert"
474
+ *
475
+ * // aliasing our functions for brevity
476
+ * const eq = assertEquals, fn = trimStartSlashes
477
+ *
478
+ * eq(fn("///a/b.txt//"), "a/b.txt//")
479
+ * eq(fn("/.//a/b.txt//"), ".//a/b.txt//")
480
+ * eq(fn(".///../a/b.txt//"), ".///../a/b.txt//")
481
+ * eq(fn("file:///a/b.txt//"), "file:///a/b.txt//")
482
+ * ```
483
+ */
484
+ export const trimStartSlashes = (str) => {
485
+ return str.replace(leading_slashes_regex, "");
486
+ };
487
+ /** trim the trailing forward-slashes at the end of a string, except for those that are preceded by a dotslash ("/./") or a dotdotslash ("/../")
488
+ *
489
+ * @example
490
+ * ```ts
491
+ * import { assertEquals } from "jsr:@std/assert"
492
+ *
493
+ * // aliasing our functions for brevity
494
+ * const eq = assertEquals, fn = trimEndSlashes
495
+ *
496
+ * eq(fn("///a/b.zip///"), "///a/b.zip")
497
+ * eq(fn("///a/b.zip/.///"), "///a/b.zip/./")
498
+ * eq(fn("///a/b.zip/..///"), "///a/b.zip/../")
499
+ * eq(fn("///a/b.zip/...///"), "///a/b.zip/...")
500
+ * eq(fn("///a/b.zip/wut.///"), "///a/b.zip/wut.")
501
+ * eq(fn("///a/b.zip/wut..///"), "///a/b.zip/wut..")
502
+ * eq(fn("///a/b.zip/wut...///"), "///a/b.zip/wut...")
503
+ * eq(fn(".///../a/b.zip/"), ".///../a/b.zip")
504
+ * eq(fn("file:///a/b.zip//c.txt"), "file:///a/b.zip//c.txt")
505
+ * ```
506
+ */
507
+ export const trimEndSlashes = (str) => {
508
+ return str.replace(trailing_slashes_regex, "");
509
+ };
510
+ /** trim leading and trailing forward-slashes, at the beginning and end of a string.
511
+ * this is a combination of {@link trimStartSlashes} and {@link trimEndSlashes}, so see their doc comments for more precise test cases.
512
+ *
513
+ * @example
514
+ * ```ts
515
+ * import { assertEquals } from "jsr:@std/assert"
516
+ *
517
+ * // aliasing our functions for brevity
518
+ * const eq = assertEquals, fn = trimSlashes
519
+ *
520
+ * eq(fn("///a/b.zip//"), "a/b.zip")
521
+ * eq(fn("///a/b.zip/..///"), "a/b.zip/../")
522
+ * eq(fn("///a/b.zip/...///"), "a/b.zip/...")
523
+ * eq(fn("///a/b.zip/.//..///"), "a/b.zip/.//../")
524
+ * eq(fn("///a/b.zip/.//.///"), "a/b.zip/.//./")
525
+ * eq(fn(".///../a/b.zip//"), ".///../a/b.zip")
526
+ * eq(fn("file:///a/b.zip//c.txt"), "file:///a/b.zip//c.txt")
527
+ * ```
528
+ */
529
+ export const trimSlashes = (str) => {
530
+ return trimEndSlashes(trimStartSlashes(str));
531
+ };
532
+ /** ensure that there is at least one leading slash at the beginning.
533
+ *
534
+ * @example
535
+ * ```ts
536
+ * import { assertEquals } from "jsr:@std/assert"
537
+ *
538
+ * // aliasing our functions for brevity
539
+ * const eq = assertEquals, fn = ensureStartSlash
540
+ *
541
+ * eq(fn("a/b.zip"), "/a/b.zip")
542
+ * eq(fn(".///../a/b.zip/"), "/.///../a/b.zip/")
543
+ * eq(fn("///../a/b.zip/"), "///../a/b.zip/")
544
+ * eq(fn("file:///a/b.zip//c.txt"), "/file:///a/b.zip//c.txt")
545
+ * ```
546
+ */
547
+ export const ensureStartSlash = (str) => {
548
+ return string_starts_with(str, sep) ? str : sep + str;
549
+ };
550
+ /** ensure that there is at least one leading dot-slash at the beginning.
551
+ *
552
+ * @example
553
+ * ```ts
554
+ * import { assertEquals } from "jsr:@std/assert"
555
+ *
556
+ * // aliasing our functions for brevity
557
+ * const eq = assertEquals, fn = ensureStartDotSlash
558
+ *
559
+ * eq(fn("a/b.zip"), "./a/b.zip")
560
+ * eq(fn(".///../a/b.zip/"), ".///../a/b.zip/")
561
+ * eq(fn("///../a/b.zip/"), ".///../a/b.zip/")
562
+ * eq(fn("file:///a/b.zip//c.txt"), "./file:///a/b.zip//c.txt")
563
+ * ```
564
+ */
565
+ export const ensureStartDotSlash = (str) => {
566
+ return string_starts_with(str, dotslash) ? str
567
+ : string_starts_with(str, sep) ? "." + str
568
+ : dotslash + str;
569
+ };
570
+ /** ensure that there is at least one trailing slash at the end.
571
+ *
572
+ * @example
573
+ * ```ts
574
+ * import { assertEquals } from "jsr:@std/assert"
575
+ *
576
+ * // aliasing our functions for brevity
577
+ * const eq = assertEquals, fn = ensureEndSlash
578
+ *
579
+ * eq(fn("///a/b.zip//"), "///a/b.zip//")
580
+ * eq(fn(".///../a/b.zip/"), ".///../a/b.zip/")
581
+ * eq(fn(".///../a/b.zip/."), ".///../a/b.zip/./")
582
+ * eq(fn(".///../a/b.zip/./"), ".///../a/b.zip/./")
583
+ * eq(fn(".///../a/b.zip/.."), ".///../a/b.zip/../")
584
+ * eq(fn(".///../a/b.zip/../"), ".///../a/b.zip/../")
585
+ * eq(fn("file:///a/b.zip//c.txt"), "file:///a/b.zip//c.txt/")
586
+ * ```
587
+ */
588
+ export const ensureEndSlash = (str) => {
589
+ return string_ends_with(str, sep) ? str : str + sep;
590
+ };
591
+ /** trim leading forward-slashes ("/") and dot-slashes ("./"), at the beginning a string.
592
+ * but exclude non-trivial dotdotslash ("/../") from being wrongfully trimmed.
593
+ *
594
+ * @example
595
+ * ```ts
596
+ * import { assertEquals } from "jsr:@std/assert"
597
+ *
598
+ * // aliasing our functions for brevity
599
+ * const eq = assertEquals, fn = trimStartDotSlashes
600
+ *
601
+ * eq(fn("///a/b.zip/c.txt"), "a/b.zip/c.txt")
602
+ * eq(fn("///a/b.zip//"), "a/b.zip//")
603
+ * eq(fn("//..//a/b.zip//"), "..//a/b.zip//")
604
+ * eq(fn("/./..//a/b.zip//"), "..//a/b.zip//")
605
+ * eq(fn("/./.././a/b.zip//"), ".././a/b.zip//")
606
+ * eq(fn("///././///.////a/b.zip//"), "a/b.zip//")
607
+ * eq(fn(".///././///.////a/b.zip//"), "a/b.zip//")
608
+ * eq(fn("./././//././///.////a/b.zip//"), "a/b.zip//")
609
+ * eq(fn("file:///a/b.zip//c.txt"), "file:///a/b.zip//c.txt")
610
+ * ```
611
+ */
612
+ export const trimStartDotSlashes = (str) => {
613
+ return str.replace(leading_slashes_and_dot_slashes_regex, "");
614
+ };
615
+ /** trim all trivial trailing forward-slashes ("/") and dot-slashes ("./"), at the end a string.
616
+ * but exclude non-trivial dotdotslash ("/../") from being wrongfully trimmed.
617
+ *
618
+ * TODO: this operation is somewhat expensive, because:
619
+ * - the implementation uses regex, however it was not possible for me to design a regex that handles the input string as is,
620
+ * so I resort to reversing the input string, and using a slightly easier-to-design regex that discovers trivial (dot)slashes in reverse order,
621
+ * and then after the string replacement, I reverse it again and return it as the output.
622
+ *
623
+ * @example
624
+ * ```ts
625
+ * import { assertEquals } from "jsr:@std/assert"
626
+ *
627
+ * // aliasing our functions for brevity
628
+ * const eq = assertEquals, fn = trimEndDotSlashes
629
+ *
630
+ * eq(fn("a/b.zip/c.txt"), "a/b.zip/c.txt")
631
+ * eq(fn("//a/b.zip//"), "//a/b.zip")
632
+ * eq(fn("/"), "")
633
+ * eq(fn("./"), "")
634
+ * eq(fn("//././//./"), "")
635
+ * eq(fn(".//././//./"), "")
636
+ * eq(fn(".//./..///./"), ".//./../")
637
+ * eq(fn("/a/b.zip/./"), "/a/b.zip")
638
+ * eq(fn("/a/b.zip/../"), "/a/b.zip/../")
639
+ * eq(fn("/a/b.zip/..//"), "/a/b.zip/../")
640
+ * eq(fn("/a/b.zip/.././"), "/a/b.zip/../")
641
+ * eq(fn("a/b.zip///././///.////"), "a/b.zip")
642
+ * eq(fn("/a/b.zip///././///.////"), "/a/b.zip")
643
+ * eq(fn("/a/b.zip/.././/.././///.////"), "/a/b.zip/.././/../")
644
+ * eq(fn("/a/b.zip/././././///.////"), "/a/b.zip")
645
+ * eq(fn("/a/b.zip./././././///.////"), "/a/b.zip.")
646
+ * eq(fn("/a/b.zip../././././///.////"), "/a/b.zip..")
647
+ * eq(fn("/a/b.zip.../././././///.////"), "/a/b.zip...")
648
+ * eq(fn("file:///a/b.zip//c.txt"), "file:///a/b.zip//c.txt")
649
+ * ```
650
+ */
651
+ export const trimEndDotSlashes = (str) => {
652
+ const reversed_str = [...str].toReversed().join(""), trimmed_reversed_str = reversed_str.replace(reversed_trailing_slashes_and_dot_slashes_regex, "");
653
+ // there is a special case when the entirety of the original `str` is made up of only (dot)slashes and ends with one dotslashes "./" (trailing relative path navigation),
654
+ // in which case, we will be left with one single "." as our `trimmed_reversed_str`, instead of an empty string.
655
+ // so we handle this special case below, otherwise we process all other `trimmed_reversed_str` by reversing it once more.
656
+ return trimmed_reversed_str === "."
657
+ ? ""
658
+ : [...trimmed_reversed_str].toReversed().join("");
659
+ };
660
+ /** trim leading and trailing forward-slashes ("/") and dot-slashes ("./"), at the beginning and end of a string, but keep trailing non-trivial ones intact.
661
+ * this is a combination of {@link trimStartDotSlashes} and {@link trimEndDotSlashes}, so see their doc comments for more precise test cases.
662
+ *
663
+ * @example
664
+ * ```ts
665
+ * import { assertEquals } from "jsr:@std/assert"
666
+ *
667
+ * // aliasing our functions for brevity
668
+ * const eq = assertEquals, fn = trimDotSlashes
669
+ *
670
+ * eq(fn("///a/b.zip//"), "a/b.zip")
671
+ * eq(fn(".///../a/b.zip//"), "../a/b.zip")
672
+ * eq(fn("//./././///././//../a/b.zip//"), "../a/b.zip")
673
+ * eq(fn("file:///a/b.zip//c.txt"), "file:///a/b.zip//c.txt")
674
+ * ```
675
+ */
676
+ export const trimDotSlashes = (str) => {
677
+ return trimEndDotSlashes(trimStartDotSlashes(str));
678
+ };
679
+ /** TODO: purge this function in the future, if you absolutely do not use it anywhere.
680
+ * @deprecated
681
+ *
682
+ * > [!note]
683
+ * > you'd probably want to use {@link joinPaths} instead of this function, for any realistic set of path segments.
684
+ * > not only is this more expensive to compute, it does not distinguish between a directory and a file path (intentionally).
685
+ *
686
+ * join path segments with forward-slashes in between, and remove redundant slashes ("/") and dotslashes ("./") around each segment (if any).
687
+ * however, the first segment's leading and trailing slashes are left untouched,
688
+ * because that would potentially strip away location information (such as relative path ("./"), or absolute path ("/"), or some uri ("file:///")).
689
+ *
690
+ * if you want to ensure that your first segment is shortened, use either the {@link normalizePath} or {@link normalizePosixPath} function on it before passing it here.
691
+ *
692
+ * > [!warning]
693
+ * > it is recommended that you use segments with posix path dir-separators ("/").
694
+ *
695
+ * @example
696
+ * ```ts
697
+ * import { assertEquals } from "jsr:@std/assert"
698
+ *
699
+ * // aliasing our functions for brevity
700
+ * const eq = assertEquals, fn = joinSlash
701
+ *
702
+ * eq(fn(".///../a", "b", "c.txt"), ".///../a/b/c.txt")
703
+ * eq(fn("file:///a/", "b.zip//", "./c.txt"), "file:///a/b.zip/c.txt")
704
+ * eq(fn("file:///", "a/", "b.zip//", "./c.txt"), "file:///a/b.zip/c.txt")
705
+ * eq(fn("///a//", "b.//", "zip..//"), "///a//b./zip..")
706
+ * eq(fn("a/", "b.zip", "./c.txt", ""), "a/b.zip/c.txt/")
707
+ * eq(fn("a/", "b.zip", "./c.txt", "."), "a/b.zip/c.txt/")
708
+ * eq(fn("a/", "b.zip", "./c.txt", "./"), "a/b.zip/c.txt/")
709
+ * eq(fn("a/", "b.zip", "./c.txt", ".."), "a/b.zip/c.txt/..")
710
+ * eq(fn("a/", "b.zip", "./c.txt", "..."), "a/b.zip/c.txt/...")
711
+ * eq(fn("", "", ""), "")
712
+ * eq(fn("/", "", ""), "/")
713
+ * eq(fn("/", "/", ""), "/")
714
+ * eq(fn("/", "", "/"), "/")
715
+ * eq(fn("/", "/", "/"), "/")
716
+ * eq(fn("./", "", ""), "./")
717
+ * eq(fn("./", "./", ""), "./")
718
+ * eq(fn("./", "", "./"), "./")
719
+ * eq(fn("./", "./", "./"), "./")
720
+ * eq(fn(
721
+ * "//./././///././//../a/b.zip/.////",
722
+ * "///.////././.././c.txt/./../",
723
+ * "../../d.xyz//.//",
724
+ * ), "//./././///././//../a/b.zip/.////.././c.txt/./../../../d.xyz")
725
+ * ```
726
+ */
727
+ export const joinSlash = (first_segment = "", ...segments) => {
728
+ return segments
729
+ .map(trimDotSlashes)
730
+ .reduce((output, subpath) => ((output === "" ? "" : ensureEndSlash(output)) + subpath), first_segment);
731
+ };
732
+ /** normalize a path by reducing and removing redundant dot-slash ("./" and "../") path navigators from a path.
733
+ *
734
+ * if you provide the optional `config` with the `keepRelative` set to `false`, then in the output, there will no be leading dot-slashes ("./").
735
+ * read more about the option here: {@link NormalizePathConfig.keepRelative}.
736
+ * but note that irrespective of what you set this option to be, leading leading dotdot-slashes ("../") and leading slashes ("/") will not be trimmed.
737
+ *
738
+ * even though `config` should be of {@link NormalizePathConfig} type, it also accepts `number` so that the function's signature becomes compatible with the `Array.prototype.map` method,
739
+ * however, unless you pass the correct config object type, only the default action will be taken.
740
+ *
741
+ * > [!warning]
742
+ * > you MUST provide a posix path (i.e. use "/" for dir-separator).
743
+ * > there will not be any implicit conversion of windows "\\" dir-separator.
744
+ *
745
+ * @example
746
+ * ```ts
747
+ * import { assertEquals } from "jsr:@std/assert"
748
+ *
749
+ * // aliasing our functions for brevity
750
+ * const eq = assertEquals, fn = normalizePosixPath
751
+ * // aliasing the config for disabling the preservation of leading "./"
752
+ * const remove_rel: NormalizePathConfig = { keepRelative: false }
753
+ *
754
+ * eq(fn("../a/./b/../././/c.txt"), "../a//c.txt")
755
+ * eq(fn("./././a/b/.././././//c.txt"), "./a///c.txt")
756
+ * eq(fn("./././a/b/.././././//c.txt", remove_rel), "a///c.txt")
757
+ * eq(fn("/a/b/.././././//c.txt"), "/a///c.txt")
758
+ * eq(fn("///a/b/.././././//c.txt"), "///a///c.txt")
759
+ * eq(fn("///a/b/.././.././//c.txt"), "/////c.txt")
760
+ * eq(fn("file:///./././a/b/.././././c.txt"), "file:///a/c.txt")
761
+ * eq(fn("/a/../"), "/")
762
+ * // NOTICE: the test in the next line may seem like a weird behavior.
763
+ * eq(fn("/a/../../"), "")
764
+ * eq(fn("/a/../../../"), "../")
765
+ * eq(fn("./a/../../"), "../")
766
+ * eq(fn("./a/../../../"), "../../")
767
+ * eq(fn("/a/b/../.."), "/")
768
+ * eq(fn("/a/b/."), "/a/b/")
769
+ * eq(fn("/a/b/./"), "/a/b/")
770
+ * eq(fn("/a/b/c/.."), "/a/b/")
771
+ * eq(fn("/a/b/c/../."), "/a/b/")
772
+ * eq(fn("/a/b/c/d/../.."), "/a/b/")
773
+ * eq(fn("/a/b/c/../.nomedia"), "/a/b/.nomedia")
774
+ * eq(fn(""), "")
775
+ * eq(fn("."), ".")
776
+ * eq(fn(".."), "..")
777
+ * eq(fn("./"), "./")
778
+ * eq(fn("../"), "../")
779
+ * eq(fn("../."), "../")
780
+ * eq(fn("../.."), "../../")
781
+ * eq(fn("./././././"), "./")
782
+ * eq(fn(".././././"), "../")
783
+ * eq(fn("./././.././././"), "../")
784
+ * eq(fn("./././.././.././"), "../../")
785
+ * eq(fn(".", remove_rel), "")
786
+ * eq(fn("./", remove_rel), "")
787
+ * eq(fn("./././././", remove_rel), "")
788
+ * eq(fn("./././.././././", remove_rel), "../")
789
+ * eq(fn("./././.././.././", remove_rel), "../../")
790
+ * ```
791
+ */
792
+ export const normalizePosixPath = (path, config = {}) => {
793
+ const { keepRelative = true } = isObject(config) ? config : {}, segments = path.split(sep), last_segment = segments.at(-1), output_segments = [".."],
794
+ // a flag that specifies whether a "./" should be prepended to final result (assuming that the result does not being with "../")
795
+ prepend_relative_dotslash_to_output_segments = keepRelative && segments[0] === ".",
796
+ // a flag that specifies whether the input path ends with a "/." or "/..", in which case we will need to append a final "/" to the output.
797
+ // this is because our for-loop will strip away the final dot character, without any upcoming replacements.
798
+ // for instance, an input `path = "/a/b/."` would normalize to `"/a/b/"` with this flag. but without it, it will become `"/a/b"`.
799
+ ends_with_dir_navigator_without_a_trailing_slash = (segments.length >= 2) && (last_segment === "." || last_segment === "..");
800
+ if (ends_with_dir_navigator_without_a_trailing_slash) {
801
+ segments.push("");
802
+ }
803
+ for (const segment of segments) {
804
+ if (segment === "..") {
805
+ if (output_segments.at(-1) !== "..") {
806
+ output_segments.pop();
807
+ }
808
+ else {
809
+ output_segments.push(segment);
810
+ }
811
+ }
812
+ else if (segment !== ".") {
813
+ output_segments.push(segment);
814
+ }
815
+ }
816
+ output_segments.shift();
817
+ if (prepend_relative_dotslash_to_output_segments && output_segments[0] !== "..") {
818
+ output_segments.unshift(".");
819
+ }
820
+ return output_segments.join(sep);
821
+ };
822
+ /** normalize a path by reducing and removing redundant dot-slash ("./", "../", ".\\", and "..\\") path navigators from a path.
823
+ * the returned output is always a posix-style path.
824
+ *
825
+ * to read about the optional `config` parameter, refer to the docs of {@link normalizePosixPath}, which is the underlying function that takes care most of the normalization.
826
+ *
827
+ * @example
828
+ * ```ts
829
+ * import { assertEquals } from "jsr:@std/assert"
830
+ *
831
+ * // aliasing our functions for brevity
832
+ * const eq = assertEquals, fn = normalizePath
833
+ *
834
+ * eq(fn("../a/./b/../././/c.txt"), "../a//c.txt")
835
+ * eq(fn("./.\\.\\a\\b\\.././.\\.///c.txt"), "./a///c.txt")
836
+ * eq(fn("/home\\.config/a\\..\\...\\b\\./c.txt"), "/home/.config/.../b/c.txt")
837
+ * eq(fn("file:///./././a\\b/..\\././.\\c.txt"), "file:///a/c.txt")
838
+ * ```
839
+ */
840
+ export const normalizePath = (path, config) => {
841
+ return normalizePosixPath(pathToPosixPath(path), config);
842
+ };
843
+ /** convert windows directory slash "\\" to posix directory slash "/".
844
+ *
845
+ * @example
846
+ * ```ts
847
+ * import { assertEquals } from "jsr:@std/assert"
848
+ *
849
+ * // aliasing our functions for brevity
850
+ * const eq = assertEquals, fn = pathToPosixPath
851
+ *
852
+ * eq(fn("C:\\Users/my name\\file.txt"), "C:/Users/my name/file.txt")
853
+ * eq(fn("~/path/to/file.txt"), "~/path/to/file.txt")
854
+ * eq(fn("/path\\to file.txt"), "/path/to file.txt")
855
+ * ```
856
+ */
857
+ export const pathToPosixPath = (path) => path.replaceAll(windows_directory_slash_regex, sep);
858
+ /** convert an array of paths to cli compatible list of paths, suitable for setting as an environment variable.
859
+ *
860
+ * @example
861
+ * ```ts
862
+ * import { assertEquals } from "jsr:@std/assert"
863
+ *
864
+ * // aliasing our functions for brevity
865
+ * const eq = assertEquals, fn = pathsToCliArg
866
+ *
867
+ * // conversion example with windows separator (";")
868
+ * eq(fn(";", [
869
+ * "./a/b/c.txt",
870
+ * "C:\\Android Studio\\sdk\\",
871
+ * "build\\libs\\"
872
+ * ]), `"./a/b/c.txt;C:/Android Studio/sdk/;build/libs/"`)
873
+ *
874
+ * // conversion example with unix separator (":")
875
+ * eq(fn(":", [
876
+ * "./a/b/c.txt",
877
+ * "~/Android Studio/sdk/",
878
+ * "build/libs/"
879
+ * ]), `"./a/b/c.txt:~/Android Studio/sdk/:build/libs/"`)
880
+ * ```
881
+ */
882
+ export const pathsToCliArg = (separator, paths) => {
883
+ return quote(pathToPosixPath(paths.join(separator)));
884
+ };
885
+ /** find the prefix path directory common to all provided `paths`.
886
+ * > [!warning]
887
+ * > your paths MUST be normalized beforehand, and use posix dir-separators ("/").
888
+ *
889
+ * @example
890
+ * ```ts
891
+ * import { assertEquals } from "jsr:@std/assert"
892
+ *
893
+ * // aliasing our functions for brevity
894
+ * const eq = assertEquals, fn = commonNormalizedPosixPath
895
+ *
896
+ * eq(fn([
897
+ * "C:/Hello/World/This/Is/An/Example/Bla.cs",
898
+ * "C:/Hello/World/This/Is/Not/An/Example/",
899
+ * "C:/Hello/Earth/Bla/Bla/Bla",
900
+ * ]), "C:/Hello/")
901
+ *
902
+ * eq(fn([
903
+ * "C:/Hello/World/This/Is/An/Example/Bla.cs",
904
+ * "C:/Hello/World/This/is/an/example/bla.cs",
905
+ * "C:/Hello/World/This/Is/Not/An/Example/",
906
+ * ]), "C:/Hello/World/This/")
907
+ *
908
+ * eq(fn([
909
+ * "./../Hello/World/Users/This/Is/An/Example/Bla.cs",
910
+ * "./../Hello/World Users/This/Is/An/example/bla.cs",
911
+ * "./../Hello/World-Users/This/Is/Not/An/Example/",
912
+ * ]), "./../Hello/")
913
+ *
914
+ * eq(fn([
915
+ * "./Hello/World/Users/This/Is/An/Example/Bla.cs",
916
+ * "./Hello/World/",
917
+ * "./Hello/World", // the "World" here segment is not treated as a directory
918
+ * ]), "./Hello/")
919
+ *
920
+ * eq(fn([
921
+ * "C:/Hello/World/",
922
+ * "/C:/Hello/World/",
923
+ * "C:/Hello/World/",
924
+ * ]), "") // no common prefix was identified
925
+ * ```
926
+ */
927
+ export const commonNormalizedPosixPath = (paths) => {
928
+ const common_prefix = commonPrefix(paths), common_prefix_length = common_prefix.length;
929
+ for (const path of paths) {
930
+ const remaining_substring = path.slice(common_prefix_length);
931
+ if (!string_starts_with(remaining_substring, sep)) {
932
+ // it looks like the `path`'s common prefix is not followed by an immediate "/" separator.
933
+ // thus, we must now reduce our `common_prefix` to the last available "/" separator.
934
+ // after we do that, we are guaranteed that this newly created `common_dir_prefix` is indeed common to all `paths`, since its superset, the `common_prefix`, was also common to all `paths`.
935
+ // thus we can immediately return and ignore the remaining tests in the loop.
936
+ const common_dir_prefix_length = common_prefix.lastIndexOf(sep) + 1, common_dir_prefix = common_prefix.slice(0, common_dir_prefix_length);
937
+ return common_dir_prefix;
938
+ }
939
+ }
940
+ // if we have made it to here, it would mean that among all paths, the initial `common_prefix` was indeed also the common directory among all of them.
941
+ return common_prefix;
942
+ };
943
+ /** find the prefix path directory common to all provided `paths`.
944
+ * your input `paths` do not need to be normalized nor necessarily use posix-style separator "/".
945
+ * under the hood, this function normalizes and converts all paths to posix-style, then applies the {@link commonNormalizedPosixPath} onto them.
946
+ *
947
+ * @example
948
+ * ```ts
949
+ * import { assertEquals } from "jsr:@std/assert"
950
+ *
951
+ * // aliasing our functions for brevity
952
+ * const eq = assertEquals, fn = commonPath
953
+ *
954
+ * eq(fn([
955
+ * "C:/Hello/World/This/Is/An/Example/Bla.cs",
956
+ * "C:\\Hello\\World\\This\\Is\\Not/An/Example/",
957
+ * "C:/Hello/Earth/Bla/Bla/Bla",
958
+ * ]), "C:/Hello/")
959
+ *
960
+ * eq(fn([
961
+ * "./Hello/World/This/Used/to-be-an/example/../../../Is/An/Example/Bla.cs",
962
+ * ".\\Hello/World/This/Is/an/example/bla.cs",
963
+ * "./Hello/World/This/Is/Not/An/Example/",
964
+ * ]), "./Hello/World/This/Is/")
965
+ *
966
+ * eq(fn([
967
+ * "./../home/Hello/World/Users/This/Is/An/Example/Bla.cs",
968
+ * "././../home\\Hello\\World Users\\This\\Is/An\\example/bla.cs",
969
+ * "./../home/./.\\.\\././Hello/World-Users/./././././This/Is/Not/An/Example/",
970
+ * ]), "../home/Hello/")
971
+ *
972
+ * eq(fn([
973
+ * "\\C:/Hello/World/Users/This/Is/An/Example/Bla.cs",
974
+ * "/C:\\Hello\\World Users\\This\\Is/An\\example/bla.cs",
975
+ * "/C:/Hello/World", // the "World" here segment is not treated as a directory
976
+ * ]), "/C:/Hello/")
977
+ * ```
978
+ */
979
+ export const commonPath = (paths) => {
980
+ return commonNormalizedPosixPath(paths.map(normalizePath));
981
+ };
982
+ /** replace the common path among all provided `paths` by transforming it with a custom `map_fn` function.
983
+ * all `paths` are initially normalized and converted into posix-style (so that no "\\" windows separator is prevelent).
984
+ *
985
+ * the `map_fn` function's first argument (`path_info`), is a 2-tuple of the form `[common_dir: string, subpath: string]`,
986
+ * where `common_dir` represents the directory common to all of the input `paths`, and the `subpath` represents the remaining relative path that comes after common_dir.
987
+ * - the `common_dir` always ends with a trailing slash ("/"), unless there is absolutely no common directory among the `paths` at all.
988
+ * - the `subpath` never begins with any slash (nor any dot-slashes), unless of course, you had initially provided a path containing two or more consecutive slashes.
989
+ *
990
+ * @example
991
+ * ```ts
992
+ * import { assertEquals } from "jsr:@std/assert"
993
+ *
994
+ * // aliasing our functions for brevity
995
+ * const eq = assertEquals, fn = commonPathTransform
996
+ *
997
+ * const subpath_map_fn = ([common_dir, subpath]: [string, string]) => (subpath)
998
+ *
999
+ * eq(fn([
1000
+ * "C:/Hello/World/This/Is/An/Example/Bla.cs",
1001
+ * "C:\\Hello\\World\\This\\Is\\Not/An/Example/",
1002
+ * "C:/Hello/Earth/Bla/Bla/Bla",
1003
+ * ], subpath_map_fn), [
1004
+ * "World/This/Is/An/Example/Bla.cs",
1005
+ * "World/This/Is/Not/An/Example/",
1006
+ * "Earth/Bla/Bla/Bla",
1007
+ * ])
1008
+ *
1009
+ * eq(fn([
1010
+ * "./../././home/Hello/World/This/Used/to-be-an/example/../../../Is/An/Example/Bla.cs",
1011
+ * "./././../home/Hello/World/This/Is/an/example/bla.cs",
1012
+ * "./../home/Hello/World/This/Is/Not/An/Example/",
1013
+ * ], subpath_map_fn), [
1014
+ * "An/Example/Bla.cs",
1015
+ * "an/example/bla.cs",
1016
+ * "Not/An/Example/",
1017
+ * ])
1018
+ *
1019
+ * eq(fn([
1020
+ * "/C:/Hello///World/Users/This/Is/An/Example/Bla.cs",
1021
+ * "/C:\\Hello\\World Users\\This\\Is/An\\example/bla.cs",
1022
+ * "/C:/./.\\.\\././Hello/World-Users/./././././This/Is/Not/An/Example/",
1023
+ * ], subpath_map_fn), [
1024
+ * "//World/Users/This/Is/An/Example/Bla.cs",
1025
+ * "World Users/This/Is/An/example/bla.cs",
1026
+ * "World-Users/This/Is/Not/An/Example/",
1027
+ * ])
1028
+ * ```
1029
+ */
1030
+ export const commonPathTransform = (paths, map_fn) => {
1031
+ const normal_paths = paths.map(normalizePath), common_dir = commonNormalizedPosixPath(normal_paths), common_dir_length = common_dir.length, path_infos = array_from(normal_paths, (normal_path) => {
1032
+ return [common_dir, normal_path.slice(common_dir_length)];
1033
+ });
1034
+ return path_infos.map(map_fn);
1035
+ };
1036
+ /** purge the common path among all provided `paths`, and replace (join) it with a `new_common_dir` path.
1037
+ *
1038
+ * @example
1039
+ * ```ts
1040
+ * import { assertEquals } from "jsr:@std/assert"
1041
+ *
1042
+ * // aliasing our functions for brevity
1043
+ * const eq = assertEquals, fn = commonPathReplace
1044
+ *
1045
+ * eq(fn([
1046
+ * "C:/Hello/World/This/Is/An/Example/Bla.cs",
1047
+ * "C:\\Hello\\World\\This\\Is\\Not/An/Example/",
1048
+ * "C:/Hello/Earth/Bla/Bla/Bla",
1049
+ * ], "D:/"), [
1050
+ * "D:/World/This/Is/An/Example/Bla.cs",
1051
+ * "D:/World/This/Is/Not/An/Example/",
1052
+ * "D:/Earth/Bla/Bla/Bla",
1053
+ * ])
1054
+ *
1055
+ * eq(fn([
1056
+ * "C:/Hello/World/This/Used/to-be-an/example/../../../Is/An/Example/Bla.cs",
1057
+ * "C:/Hello/World/This/Is/an/example/bla.cs",
1058
+ * "C:/Hello/World/This/Is/Not/An/Example/",
1059
+ * ], "D:/temp"), [ // an implicit forward slash is added.
1060
+ * "D:/temp/An/Example/Bla.cs",
1061
+ * "D:/temp/an/example/bla.cs",
1062
+ * "D:/temp/Not/An/Example/",
1063
+ * ])
1064
+ *
1065
+ * eq(fn([
1066
+ * // there is no common ancestor among each of the paths (even "C:/" and "./C:/" are not considered to be equivalent to one another)
1067
+ * "http:/Hello/World.cs",
1068
+ * "./C:/Hello/World.cs",
1069
+ * "C:/Hello/World/file.cs",
1070
+ * ], "D:/temp/"), [
1071
+ * "D:/temp/http:/Hello/World.cs",
1072
+ * "D:/temp/C:/Hello/World.cs",
1073
+ * "D:/temp/C:/Hello/World/file.cs",
1074
+ * ])
1075
+ *
1076
+ * eq(fn([
1077
+ * "/C:/Hello///World/Users/This/Is/An/Example/Bla.cs",
1078
+ * "/C:\\Hello\\World Users\\This\\Is/An\\example/bla.cs",
1079
+ * "/C:/./.\\.\\././Hello/World-Users/./././././This/Is/Not/An/Example/",
1080
+ * ], "file:///./.\\HELLO.\\./../"), [ // the `new_common_dir` is not normalized by this function
1081
+ * "file:///./.\\HELLO.\\./..///World/Users/This/Is/An/Example/Bla.cs",
1082
+ * "file:///./.\\HELLO.\\./../World Users/This/Is/An/example/bla.cs",
1083
+ * "file:///./.\\HELLO.\\./../World-Users/This/Is/Not/An/Example/",
1084
+ * ])
1085
+ * ```
1086
+ */
1087
+ export const commonPathReplace = (paths, new_common_dir) => {
1088
+ new_common_dir = ensureEndSlash(new_common_dir);
1089
+ return commonPathTransform(paths, ([common_dir, subpath]) => {
1090
+ // if there is no common dir among the `paths` (i.e. `common_dir = ""`), then it is possible for
1091
+ // some `subpath`s to contain a leading dotslash ("./"), in which case we must trim it before concatenation
1092
+ subpath = (string_starts_with(subpath, dotslash) ? subpath.slice(2) : subpath);
1093
+ return new_common_dir + subpath;
1094
+ });
1095
+ };
1096
+ /** get the file name from a given normalized posix path.
1097
+ * if the provided path ends with a trailing slash ("/"), then an empty string will be returned, emphasizing the lack of a file name.
1098
+ *
1099
+ * @example
1100
+ * ```ts ignore
1101
+ * import { assertEquals } from "jsr:@std/assert"
1102
+ *
1103
+ * // aliasing our functions for brevity
1104
+ * const eq = assertEquals, fn = parseNormalizedPosixFilename
1105
+ *
1106
+ * eq(fn("/home/user/docs"), "docs")
1107
+ * eq(fn("/home/user/docs.md"), "docs.md")
1108
+ * eq(fn("/home/user/.bashrc"), ".bashrc")
1109
+ * eq(fn("var/log.txt"), "log.txt")
1110
+ * eq(fn("log"), "log")
1111
+ * eq(fn("C:/a/b/c/etc"), "etc")
1112
+ *
1113
+ * eq(fn("/home/user/.config/"), "")
1114
+ * eq(fn("var/log/"), "")
1115
+ * eq(fn("C:/a/b/c/etc/"), "")
1116
+ * eq(fn(""), "")
1117
+ * eq(fn("/"), "")
1118
+ * eq(fn("///"), "")
1119
+ * ```
1120
+ */
1121
+ const parseNormalizedPosixFilename = (file_path) => {
1122
+ return trimStartSlashes(filename_regex.exec(file_path)?.[0] ?? "");
1123
+ };
1124
+ /** get the base name and extension name of a file, from its filename (no directories).
1125
+ *
1126
+ * @example
1127
+ * ```ts ignore
1128
+ * import { assertEquals } from "jsr:@std/assert"
1129
+ *
1130
+ * // aliasing our functions for brevity
1131
+ * const eq = assertEquals, fn = parseBasenameAndExtname_FromFilename
1132
+ *
1133
+ * eq(fn("docs"), ["docs", ""])
1134
+ * eq(fn("docs."), ["docs.", ""])
1135
+ * eq(fn("docs.md"), ["docs", ".md"])
1136
+ * eq(fn(".bashrc"), [".bashrc", ""])
1137
+ * eq(fn("my file.tar.gz"), ["my file.tar", ".gz"])
1138
+ * eq(fn(""), ["", ""])
1139
+ * eq(fn("."), [".", ""])
1140
+ * eq(fn(".."), ["..", ""])
1141
+ * eq(fn("...hello"), ["..", ".hello"])
1142
+ * ```
1143
+ */
1144
+ const parseBasenameAndExtname_FromFilename = (filename) => {
1145
+ const { basename = "", ext = "" } = basename_and_extname_regex.exec(filename)?.groups ?? {};
1146
+ return [basename, ext];
1147
+ };
1148
+ /** parses the provided file path and breaks it down into useful bit described by the interface {@link FilepathInfo}.
1149
+ * note that a file path must never end in a trailing slash ("/"), and conversely,
1150
+ * a folder path must always in a trailing slash ("/"), otherwise it will be parsed as a file.
1151
+ *
1152
+ * @example
1153
+ * ```ts
1154
+ * import { assertEquals } from "jsr:@std/assert"
1155
+ *
1156
+ * // aliasing our functions for brevity
1157
+ * const eq = assertEquals, fn = parseFilepathInfo
1158
+ *
1159
+ * eq(fn("/home\\user/docs"), {
1160
+ * path: "/home/user/docs",
1161
+ * dirpath: "/home/user/",
1162
+ * dirname: "user",
1163
+ * filename: "docs",
1164
+ * basename: "docs",
1165
+ * extname: "",
1166
+ * })
1167
+ *
1168
+ * eq(fn("home\\user/docs/"), {
1169
+ * path: "home/user/docs/",
1170
+ * dirpath: "home/user/docs/",
1171
+ * dirname: "docs",
1172
+ * filename: "",
1173
+ * basename: "",
1174
+ * extname: "",
1175
+ * })
1176
+ *
1177
+ * eq(fn("/home/xyz/.././././user/.bashrc."), {
1178
+ * path: "/home/user/.bashrc.",
1179
+ * dirpath: "/home/user/",
1180
+ * dirname: "user",
1181
+ * filename: ".bashrc.",
1182
+ * basename: ".bashrc.",
1183
+ * extname: "",
1184
+ * })
1185
+ *
1186
+ * eq(fn("C:\\home\\user/.file.tar.gz"), {
1187
+ * path: "C:/home/user/.file.tar.gz",
1188
+ * dirpath: "C:/home/user/",
1189
+ * dirname: "user",
1190
+ * filename: ".file.tar.gz",
1191
+ * basename: ".file.tar",
1192
+ * extname: ".gz", // only the last bit of the extension makes it to here
1193
+ * })
1194
+ *
1195
+ * eq(fn("/home/user///file.txt"), {
1196
+ * path: "/home/user///file.txt",
1197
+ * dirpath: "/home/user///",
1198
+ * dirname: "", // this is because the there is no name attached between the last two slashes of the `dirpath = "/home/user///"`
1199
+ * filename: "file.txt",
1200
+ * basename: "file",
1201
+ * extname: ".txt",
1202
+ * })
1203
+ *
1204
+ * eq(fn("file://C:/home\\hello world.txt"), {
1205
+ * path: "file://C:/home/hello world.txt", // file-urls are not converted, nor is any kind of url
1206
+ * dirpath: "file://C:/home/",
1207
+ * dirname: "home",
1208
+ * filename: "hello world.txt",
1209
+ * basename: "hello world",
1210
+ * extname: ".txt",
1211
+ * })
1212
+ * ```
1213
+ */
1214
+ export const parseFilepathInfo = (file_path) => {
1215
+ const path = normalizePath(file_path), filename = parseNormalizedPosixFilename(path), filename_length = filename.length, dirpath = filename_length > 0 ? path.slice(0, -filename_length) : path,
1216
+ // below, I am purposely using `slice` instead of doing `trimEndSlashes(dirpath)`, because it is possible that two or more consecutive slashes "/" were intentionally placed in the directory separator.
1217
+ dirname = parseNormalizedPosixFilename(dirpath.slice(0, -1)), [basename, extname] = parseBasenameAndExtname_FromFilename(filename);
1218
+ return { path, dirpath, dirname, filename, basename, extname, };
1219
+ };
1220
+ /** convert the input file-url to a filesystem local-path.
1221
+ * however, if the input uri is not a file url (for instance `"C:/x/y/z"`, or `"http://hello.com"`),
1222
+ * then `undefined` will be returned.
1223
+ *
1224
+ * if you are looking to convert any _potential_ file-url back to a filesystem local-path,
1225
+ * then the {@link ensureFileUrlIsLocalPath} function would be better suited for your need.
1226
+ *
1227
+ * @example
1228
+ * ```ts
1229
+ * import { assertEquals } from "jsr:@std/assert"
1230
+ *
1231
+ * // aliasing our functions for brevity
1232
+ * const
1233
+ * fn = fileUrlToLocalPath,
1234
+ * eq = assertEquals
1235
+ *
1236
+ * eq( fn("file:///C:/Users/me/projects/"), "C:/Users/me/projects/")
1237
+ * eq( fn("file:///C:\\Users\\me/projects/"), "C:/Users/me/projects/")
1238
+ * eq( fn("file:///sys/etc/bin/deno.so"), "/sys/etc/bin/deno.so")
1239
+ * eq( fn("file:///sys\\etc/bin\\deno.so"), "/sys/etc/bin/deno.so")
1240
+ * eq( fn("file://localhost/C:/Users/me/projects/"), "C:/Users/me/projects/")
1241
+ * eq( fn("file://localhost/sys/etc/bin/deno.so"), "/sys/etc/bin/deno.so")
1242
+ * eq(fn(new URL("file:///C:/Users/me/projects/")), "C:/Users/me/projects/")
1243
+ * eq(fn(new URL("file:///sys/etc/bin/deno.so")), "/sys/etc/bin/deno.so")
1244
+ * eq(fn(new URL("file://localhost/C:/Users/me/projects/")), "C:/Users/me/projects/")
1245
+ * eq(fn(new URL("file://localhost/sys/etc/bin/deno.so")), "/sys/etc/bin/deno.so")
1246
+ *
1247
+ * // everything below is not a file-url, and therefore cannot be converted.
1248
+ * eq( fn("http://localhost:8000/hello/world/"), undefined)
1249
+ * eq( fn("C:/Users/me/projects/"), undefined)
1250
+ * eq( fn("/sys/etc/bin/deno.so"), undefined)
1251
+ * eq( fn(""), undefined)
1252
+ * ```
1253
+ */
1254
+ export const fileUrlToLocalPath = (file_url) => {
1255
+ if (isString(file_url)) {
1256
+ if (getUriScheme(file_url) !== "file") {
1257
+ return;
1258
+ }
1259
+ file_url = new URL(file_url);
1260
+ }
1261
+ if (!string_starts_with(file_url.protocol, "file:")) {
1262
+ return;
1263
+ }
1264
+ // the `file_url.pathname` always starts with a leading slash, which is invalid for windows.
1265
+ // thus we replace any leading slashes in any windows-looking path that we encounter, without actually consulting what os is being ran.
1266
+ const local_path_with_leading_slash = pathToPosixPath(dom_decodeURI(file_url.pathname)), corrected_local_path = local_path_with_leading_slash.replace(windows_leading_slash_correction_regex, "$1:/");
1267
+ return corrected_local_path;
1268
+ };
1269
+ /** a fault tolerant variant of {@link fileUrlToLocalPath} that assures you that any file-url path will get converted into a filesystem local-path.
1270
+ * otherwise, when a non-file-url is provided, its string representation (href) will be returned if it was a `URL`,
1271
+ * else the original string will be returned back.
1272
+ *
1273
+ * @example
1274
+ * ```ts
1275
+ * import { assertEquals } from "jsr:@std/assert"
1276
+ *
1277
+ * // aliasing our functions for brevity
1278
+ * const
1279
+ * fn = ensureFileUrlIsLocalPath,
1280
+ * eq = assertEquals
1281
+ *
1282
+ * eq( fn("C:/Users/me/projects/"), "C:/Users/me/projects/")
1283
+ * eq( fn("C:\\Users\\me/projects/"), "C:/Users/me/projects/")
1284
+ * eq( fn("/C:/Users\\me/projects/"), "/C:/Users/me/projects/") // note the erroneous leading slash
1285
+ * eq( fn("/sys\\etc/bin\\deno.so"), "/sys/etc/bin/deno.so")
1286
+ * eq( fn("file:///C:/Users/me/projects/"), "C:/Users/me/projects/")
1287
+ * eq( fn("file://////C:\\Users\\me/projects/"), "C:/Users/me/projects/")
1288
+ * eq( fn("file:///sys\\etc/bin\\deno.so"), "/sys/etc/bin/deno.so")
1289
+ * eq( fn("file://localhost/C:/Users/me/projects/"), "C:/Users/me/projects/")
1290
+ * eq(fn(new URL("file://localhost/sys/etc/bin/deno.so")), "/sys/etc/bin/deno.so")
1291
+ * eq( fn("http://localhost:8000/hello/world/"), "http://localhost:8000/hello/world/")
1292
+ * eq( fn("npm:react-jsx"), "npm:react-jsx")
1293
+ * eq( fn("jsr:@std/assert"), "jsr:@std/assert")
1294
+ * eq( fn("./src/mod.ts"), "./src/mod.ts")
1295
+ * eq( fn(""), "")
1296
+ * ```
1297
+ */
1298
+ export const ensureFileUrlIsLocalPath = (path) => {
1299
+ const path_is_string = isString(path), file_uri_to_local_path_conversion = fileUrlToLocalPath(path);
1300
+ return file_uri_to_local_path_conversion ?? (path_is_string
1301
+ ? pathToPosixPath(path)
1302
+ : path.href);
1303
+ };
1304
+ /** find the path `to_path`, relative to `from_path`.
1305
+ *
1306
+ * TODO: the claim below is wrong, because `joinSlash` cannot do "./" traversal correctly. for instance `joinSlash("a/b/c.txt", "./") === "a/b/c.txt/"`, but you'd expect it to be `"a/b/"` if we had correctly resolved the path.
1307
+ * which is why the output from this function will not work with `joinSlash`, but it will work with {@link resolveAsUrl} or `URL.parse` (when you provide `from_path` as the base path, and `from_path` is NOT a relative path, otherwise the `URL` constructor will fail).
1308
+ *
1309
+ * ~~if we call the result `rel_path`, then {@link joinSlash | joining} the `from_path` with `rel_path` and normalizing it should give you back `to_path`.~~
1310
+ *
1311
+ * note that both `from_path` and `to_path` must have a common root folder in their path strings.
1312
+ * for instance, if both paths begin with a relative segment `"./"`, then it will be assumed that both paths are referring to the same common root ancestral directory.
1313
+ * however, if for instance, `from_path` begins with a `"C:/"` segment, while `to_path` begins with either `"./"` or `"D:/"` or `http://` segment, then this function will fail,
1314
+ * as it will not be possible for it to navigate/transcend from one point of reference to a completely different point of reference.
1315
+ *
1316
+ * so, to be safe, wherever you are certain that both paths are of a certain common type:
1317
+ * before passing them here, you should either apply the {@link ensureStartDotSlash} function for relative paths, or apply the {@link ensureStartSlash} for absolute local paths,
1318
+ * or write a custom "ensure" function for your situation. (for example, you could write an "ensureHttp" function that ensures that your path begins with `"http"`).
1319
+ *
1320
+ * @throws `Error` an error will be thrown if there isn't any common ancestral directory between the two provided paths.
1321
+ *
1322
+ * @example
1323
+ * ```ts
1324
+ * import { assertEquals, assertThrows } from "jsr:@std/assert"
1325
+ *
1326
+ * // aliasing our functions for brevity
1327
+ * const eq = assertEquals, err = assertThrows, fn = relativePath
1328
+ *
1329
+ * eq(fn(
1330
+ * "././hello/world/a/b/c/d/g/../e.txt",
1331
+ * "././hello/world/a/b/x/y/w/../z/",
1332
+ * ), "../../x/y/z/")
1333
+ *
1334
+ * eq(fn(
1335
+ * "././hello/world/a/b/c/d/g/../e.txt",
1336
+ * "././hello/world/a/b/x/y/w/../z/e.md",
1337
+ * ), "../../x/y/z/e.md")
1338
+ *
1339
+ * eq(fn(
1340
+ * ".\\./hello\\world\\a/b\\c/d/g/../",
1341
+ * "././hello/world/a/b/x/y/w/../z/e.md",
1342
+ * ), "../../x/y/z/e.md")
1343
+ *
1344
+ * eq(fn(
1345
+ * "././hello/world/a/b/c/d/",
1346
+ * "././hello/world/a/b/x/y/w/../z/e.md",
1347
+ * ), "../../x/y/z/e.md")
1348
+ *
1349
+ * eq(fn(
1350
+ * "././hello/world/a/b/c/d/g/../",
1351
+ * "././hello/world/a/b/x/y/w/../z/e.md",
1352
+ * ), "../../x/y/z/e.md")
1353
+ *
1354
+ * eq(fn(
1355
+ * "././hello/world/a/b/c/d/",
1356
+ * "././hello/world/a/b/x/y/w/../z/",
1357
+ * ), "../../x/y/z/")
1358
+ *
1359
+ * eq(fn(
1360
+ * "./././e.txt",
1361
+ * "./e.md",
1362
+ * ), "./e.md")
1363
+ *
1364
+ * eq(fn(
1365
+ * "/e.txt",
1366
+ * "/e.md",
1367
+ * ), "./e.md")
1368
+ *
1369
+ * eq(fn(
1370
+ * "C:/e.txt",
1371
+ * "C:/e.md",
1372
+ * ), "./e.md")
1373
+ *
1374
+ * eq(fn(
1375
+ * "././hello/world/a/b/c/d/g/../e.txt",
1376
+ * "././hello/world/a/k/../b/q/../c/d/e.md",
1377
+ * ), "./e.md")
1378
+ *
1379
+ * eq(fn(
1380
+ * "./",
1381
+ * "./",
1382
+ * ), "./")
1383
+ *
1384
+ * eq(fn(
1385
+ * "/",
1386
+ * "/",
1387
+ * ), "./")
1388
+ *
1389
+ * // there is no common ancestral root between the two paths (since one is absolute, while the other is relative)
1390
+ * err(() => fn(
1391
+ * "/e.txt",
1392
+ * "./e.md",
1393
+ * ))
1394
+ *
1395
+ * // there is no common ancestral root between the two paths
1396
+ * err(() => fn(
1397
+ * "C:/e.txt",
1398
+ * "D:/e.md",
1399
+ * ))
1400
+ *
1401
+ * // there is no common ancestral root between the two paths
1402
+ * err(() => fn(
1403
+ * "http://e.txt",
1404
+ * "./e.md",
1405
+ * ))
1406
+ *
1407
+ * // there is no common ancestral root between the two paths
1408
+ * err(() => fn(
1409
+ * "file:///C:/e.txt",
1410
+ * "C:/e.md",
1411
+ * ))
1412
+ * ```
1413
+ */
1414
+ export const relativePath = (from_path, to_path) => {
1415
+ const [[common_dir, from_subpath], [, to_subpath],] = commonPathTransform([from_path, to_path], (common_dir_and_subpath) => common_dir_and_subpath);
1416
+ if (common_dir === "") {
1417
+ throw new Error(DEBUG.ERROR ? (`there is no common directory between the two provided paths:\n\t"${from_path}" and\n\t"to_path"`) : "");
1418
+ }
1419
+ const upwards_traversal = Array(from_subpath.split(sep).length).fill("..");
1420
+ upwards_traversal[0] = "."; // we do this because the relative path should always begin with a dotslash ("./")
1421
+ return normalizePosixPath(upwards_traversal.join(sep) + sep + to_subpath);
1422
+ };
1423
+ /** the reduce function below is used by {@link joinPosixPaths}, and it is basically the driving force behind it.
1424
+ *
1425
+ * we could have used a `string` based reduce function (i.e. initial value could have been the string `"/"`, instead of the array `["/"]`),
1426
+ * but we use an `Array` based reduce function, because strings are inherently immutable, so a new string is made during each modification.
1427
+ * and that would use up lots of copy operations if you were joining lots of segments.
1428
+ * it is better to process them as an array and then join it in one go at the end.
1429
+ */
1430
+ const joinPosixPaths_reduce_fn = (concatenatible_full_path, segment) => {
1431
+ const prev_segment = concatenatible_full_path.pop(), prev_segment_is_dir = string_ends_with(prev_segment, sep), prev_segment_as_dir = prev_segment_is_dir ? prev_segment : (prev_segment + sep); // rewriting the previous segment as a dir
1432
+ if (!prev_segment_is_dir) {
1433
+ const segment_is_rel_to_dir = string_starts_with(segment, dotslash), segment_is_rel_to_parent_dir = string_starts_with(segment, dotdotslash);
1434
+ // we now modify the current segment's initial directory navigation commands to give an equivalent navigation supposing that `prev_segment` was a directory instead of a file.
1435
+ // for that, we convert the initial `"./"` to `"../"`, or convert the initial `"../"` to `"../../"`, or
1436
+ // if there is no directory navigation command at the beginning, then no modification to the current segment is needed.
1437
+ if (segment_is_rel_to_dir) {
1438
+ segment = "." + segment;
1439
+ } // convert `"./a/b/c"` to `"../a/b/c"`
1440
+ else if (segment_is_rel_to_parent_dir) {
1441
+ segment = dotdotslash + segment;
1442
+ } // convert `"../a/b/c"` to `"../../a/b/c"`
1443
+ }
1444
+ concatenatible_full_path.push(prev_segment_as_dir, segment);
1445
+ return concatenatible_full_path;
1446
+ };
1447
+ /** joins multiple posix path segments into a single normalized path,
1448
+ * correctly handling files and directories differently when the `"./"` and `"../"` navigation commands are encountered.
1449
+ *
1450
+ * > [!note]
1451
+ * > the `joinPosixPaths` function differs from `joinSlash` in that it treats segments without a trailing slash as files by default.
1452
+ * > furthermore, `joinPosixPaths` is much more quicker to compute, as opposed to `joinSlash`, which uses some complex regex on each segment.
1453
+ * > the only reason you might realistically want to use `joinSlash` is when you desire an non-normalized output.
1454
+ *
1455
+ * @example
1456
+ * ```ts
1457
+ * import { assertEquals } from "jsr:@std/assert"
1458
+ *
1459
+ * // aliasing our functions for brevity
1460
+ * const eq = assertEquals, fn = joinPosixPaths
1461
+ *
1462
+ * eq(fn("a", "b", "c.zip"), "a/b/c.zip")
1463
+ * eq(fn("a", "b", "c/"), "a/b/c/")
1464
+ * eq(fn("a/", "b/", "c/"), "a/b/c/")
1465
+ * eq(fn("a", "b", "c.zip", "./"), "a/b/")
1466
+ * eq(fn("a", "b", "c/", "./"), "a/b/c/")
1467
+ * eq(fn("a", "b", "c", "../"), "a/")
1468
+ * eq(fn("a", "b", "c/d.txt", "./e.txt"), "a/b/c/e.txt")
1469
+ * eq(fn("a", "b", "c/d.txt", "../e.txt"), "a/b/e.txt")
1470
+ * eq(fn("a/b", "c/d.txt", "..", "e.txt"), "a/b/e.txt") // notice that you can use "." instead of "./"
1471
+ * eq(fn("a/", "b/", "c/", "../", "./","d.txt"), "a/b/d.txt")
1472
+ * eq(fn("a/b", "c/", "..", ".","d.txt"), "a/b/d.txt") // notice that you can use ".." instead of "../"
1473
+ * eq(fn("a/b", "c/", ".d.txt"), "a/b/c/.d.txt")
1474
+ * eq(fn("a/b", "c/", "..d.txt"), "a/b/c/..d.txt")
1475
+ * eq(fn("a/b", "c/", ".d"), "a/b/c/.d")
1476
+ * eq(fn("a/b", "c/", ".d."), "a/b/c/.d.")
1477
+ * eq(fn("a/b", "c/", "..d"), "a/b/c/..d")
1478
+ * eq(fn("a/b", "c/", "..d."), "a/b/c/..d.")
1479
+ * eq(fn("a/b", "c/", "..d.."), "a/b/c/..d..")
1480
+ * eq(fn("a/b", "c/", "..d.", "e.txt"), "a/b/c/..d./e.txt")
1481
+ * eq(fn("a/b", "c/", "..d..", "e.txt"), "a/b/c/..d../e.txt")
1482
+ * eq(fn("./a", "./b", "./c"), "./c")
1483
+ * eq(fn("./a", "/b", "/c"), "./a//b//c")
1484
+ * eq(fn("./a/", "/b/", "/c"), "./a//b//c")
1485
+ * eq(fn("/a", "b.zip", "./c.zip"), "/a/c.zip")
1486
+ * eq(fn("/a", "b.zip", "./c.zip/", "../d.txt"), "/a/d.txt")
1487
+ * eq(fn("/a", "b.zip/", "./c.txt"), "/a/b.zip/c.txt")
1488
+ * eq(fn("/a", "b.zip/", "./c.txt/", "../d.txt"), "/a/b.zip/d.txt")
1489
+ * eq(fn("file:", "//", "a/b/c", "./d"), "file:///a/b/d")
1490
+ * eq(fn("file:///", "a/b/c", "./d"), "file:///a/b/d")
1491
+ * ```
1492
+ */
1493
+ export const joinPosixPaths = (...segments) => {
1494
+ // first we ensure that all segments that are purely ill-formed directory commands ("." and ".."), become well-formed ("./" and "../")
1495
+ segments = segments.map((segment) => {
1496
+ return segment === "." ? dotslash
1497
+ : segment === ".." ? dotdotslash
1498
+ : segment;
1499
+ });
1500
+ // for better optimization, I've moved the reduction closure function to the outside scope (`joinPosixPaths_reduce_fn`).
1501
+ const concatenatible_segments = segments.reduce(joinPosixPaths_reduce_fn, [sep]);
1502
+ concatenatible_segments.shift(); // we must remove the initial `"/"` that was used as the initial value of the reduce function
1503
+ return normalizePosixPath(concatenatible_segments.join(""));
1504
+ };
1505
+ /** joins multiple path segments into a single normalized posix path,
1506
+ * correctly handling files and directories differently when the `"./"` and `"../"` navigation commands are encountered.
1507
+ *
1508
+ * for lots of other (posix-only) test cases, see the doc comments of {@link joinPosixPaths}.
1509
+ *
1510
+ * @example
1511
+ * ```ts
1512
+ * import { assertEquals } from "jsr:@std/assert"
1513
+ *
1514
+ * // aliasing our functions for brevity
1515
+ * const eq = assertEquals, fn = joinPaths
1516
+ *
1517
+ * eq(fn("C:\\", "a", "b", "c.zip"), "C:/a/b/c.zip")
1518
+ * eq(fn("a", "b", "c.zip"), "a/b/c.zip")
1519
+ * eq(fn("/a\\b\\", "c/"), "/a/b/c/")
1520
+ * eq(fn("a", "b", "c.zip", "./"), "a/b/")
1521
+ * eq(fn("a", "b", "c.zip", "../"), "a/")
1522
+ * eq(fn("a", "b", "c.zip", "./d.txt"), "a/b/d.txt")
1523
+ * eq(fn("a", "b", "c.zip", "../d.txt"), "a/d.txt")
1524
+ * eq(fn("a/b/c/", "..", "./", "d.txt"), "a/b/d.txt")
1525
+ * eq(fn("a/b/c/", "../", ".", "d.txt"), "a/b/d.txt")
1526
+ * eq(fn("a/b/c", "../", "./", "d.txt"), "a/d.txt")
1527
+ * eq(fn("file:", "\\\\", "a/b/c", "./d"), "file:///a/b/d")
1528
+ * eq(fn("file://\\", "a/b/c", "./d/"), "file:///a/b/d/")
1529
+ * ```
1530
+ */
1531
+ export const joinPaths = (...segments) => {
1532
+ return joinPosixPaths(...segments.map(pathToPosixPath));
1533
+ };
1534
+ /** this is a factory function for creating customizable posix path-resolving functions.
1535
+ * a path-resolving function is one that takes in a list of path-segments, and then computes the absolute normalized path of all the segments combined.
1536
+ *
1537
+ * since it is not possible for this submodule to know the context under which you are computing/resolving paths,
1538
+ * it becomes impossible to give a meaning to a list of path segmensts that begin with a relative path.
1539
+ * which is why you would need to feed this factory function with the (static or dynamic) location of your current working path (directory),
1540
+ * and it will spit out a path-resolver function that is tailored to your specific working-directory's path.
1541
+ *
1542
+ * > [!note]
1543
+ * > if you want to preserve the starting relative segments, (i.e. you don't want an absolute path),
1544
+ * > then you're looking for the {@link joinPosixPaths} (or {@link joinPaths}) function, not this one.
1545
+ *
1546
+ * an important detail to note is that whenever an absolute path segment is encountered, all segments prior to it are discarded
1547
+ * (i.e. not joined with a "/" separator, like the way {@link joinPaths} does).
1548
+ * this behavior is enforced to remain consistent with popular implementations of path-resolvers, like:
1549
+ * - python's pathlib [`Path.resolve`](https://docs.python.org/3/library/pathlib):
1550
+ * > _If a segment is an absolute path, all previous segments are ignored_
1551
+ * - deno-std's path [`resolve`](https://jsr.io/@std/path/doc/~/resolve), from [`jsr:@std/path`](https://jsr.io/@std/path)
1552
+ *
1553
+ * > [!caution]
1554
+ * > this path resolver function works best when only absolute and relative path segments are provided.
1555
+ * > when root (but not necessarily absolute) path segments are encountered, such as `/sys/bin`,
1556
+ * > our function will typically assume that it is referring to an absolute local-filesystem path `/sys/bin`.
1557
+ * >
1558
+ * > but as you may be aware, the "correct" interpretation of the root path depends on the context of the preceding absolute path segment.
1559
+ * > for example:
1560
+ * > - if the preceding absolute path segment was `C:/a/b/c/d.txt`, and the current path segment is `/sys/bin`,
1561
+ * > then our result will be `/sys/bin`, but the result in most implementations (including deno-std's path module) would be `C:/sys/bin`.
1562
+ * > - similarly, if the preceding absolute path segment was `http://test.com/a/b/c/d.txt`, and the current path segment is `/sys/bin`,
1563
+ * > then our result will be `/sys/bin`, but the "correct" url path resolution (via `new URL(...)`) should have been `http://test.com/sys.bin`.
1564
+ * >
1565
+ * > this is why, if you're dealing with ambiguous root paths that are not necessarily tied down to your posix-filesystem's root,
1566
+ * > you should use the {@link resolveAsUrl} function instead, as it is aware of most common types of base contexts/domains for path evaluation.
1567
+ * > but on the other hand, you will be unable to use your custom {@link absolute_segment_test_fn}, nor be able to resolve multiple segments all at oncce.
1568
+ * >
1569
+ * > TODO: contemplate if it would be a good idea to add a configuration interface to this factory function,
1570
+ * > where one will be able to set their custom join rule when a root path segment is discovered, in addition to containing the {@link absolute_segment_test_fn}.
1571
+ * > the signature of the root-join function would look like: `(abs_path_evaluated_up_till_now: string, current_root_path_segment: string) => string`.
1572
+ *
1573
+ * @example
1574
+ * ```ts
1575
+ * import { assertEquals } from "jsr:@std/assert"
1576
+ *
1577
+ * const
1578
+ * cwd = "/x/y/z",
1579
+ * getCwd = () => (cwd),
1580
+ * // we also define a custom absolute segment path tester function that will identify "file://" and "http://" segments as absolute path,
1581
+ * // in addition to the standard filesystem local path absoluteness tester `isAbsolutePath`.
1582
+ * custom_absolute_path_segment_tester = (segment: string) => {
1583
+ * if (isAbsolutePath(segment)) { return true }
1584
+ * if (segment.startsWith("file://")) { return true }
1585
+ * if (segment.startsWith("http://")) { return true }
1586
+ * return false
1587
+ * },
1588
+ * resolvePosixPath = resolvePosixPathFactory(getCwd, custom_absolute_path_segment_tester)
1589
+ *
1590
+ * // aliasing our functions for brevity
1591
+ * const eq = assertEquals, fn = resolvePosixPath
1592
+ *
1593
+ * // relative path resolution
1594
+ * eq(fn("a", "b", "c.zip"), "/x/y/z/a/b/c.zip")
1595
+ * eq(fn("./a", "b", "c/"), "/x/y/z/a/b/c/")
1596
+ * eq(fn("a/", "b/", "c/"), "/x/y/z/a/b/c/")
1597
+ * eq(fn("../a", "../b", "c/"), "/x/b/c/")
1598
+ * eq(fn("../a/", "../b", "c/"), "/x/y/b/c/")
1599
+ * eq(fn("a", "b", "c.zip", "./"), "/x/y/z/a/b/")
1600
+ * eq(fn("a", "b", "c/", "./"), "/x/y/z/a/b/c/")
1601
+ * eq(fn("a", "b", "c", "../"), "/x/y/z/a/")
1602
+ * eq(fn("a", "b", "c/d.txt", "./e.txt"), "/x/y/z/a/b/c/e.txt")
1603
+ * eq(fn("a", "b", "c/d.txt", "../e.txt"), "/x/y/z/a/b/e.txt")
1604
+ * eq(fn("a/b", "c/d.txt", "..", "e.txt"), "/x/y/z/a/b/e.txt") // notice that you can use "." instead of "./"
1605
+ * eq(fn("a/", "b/", "c/", "../", "./","d.txt"), "/x/y/z/a/b/d.txt")
1606
+ * eq(fn("a/b", "c/", "..", ".","d.txt"), "/x/y/z/a/b/d.txt") // notice that you can use ".." instead of "../"
1607
+ * eq(fn("a/b", "c/", ".d.txt"), "/x/y/z/a/b/c/.d.txt")
1608
+ * eq(fn("a/b", "c/", "..d.txt"), "/x/y/z/a/b/c/..d.txt")
1609
+ * eq(fn("a/b", "c/", ".d"), "/x/y/z/a/b/c/.d")
1610
+ * eq(fn("a/b", "c/", ".d."), "/x/y/z/a/b/c/.d.")
1611
+ * eq(fn("a/b", "c/", "..d"), "/x/y/z/a/b/c/..d")
1612
+ * eq(fn("a/b", "c/", "..d."), "/x/y/z/a/b/c/..d.")
1613
+ * eq(fn("a/b", "c/", "..d.."), "/x/y/z/a/b/c/..d..")
1614
+ * eq(fn("a/b", "c/", "..d.", "e.txt"), "/x/y/z/a/b/c/..d./e.txt")
1615
+ * eq(fn("a/b", "c/", "..d..", "e.txt"), "/x/y/z/a/b/c/..d../e.txt")
1616
+ * eq(fn("./a", "./b", "./c"), "/x/y/z/c")
1617
+ *
1618
+ * // pre-existing absolute path resolution
1619
+ * eq(fn("./a", "/b", "/c"), "/c") // both "/b" and "/c" are absolute paths, and so they purge all segments behind them.
1620
+ * eq(fn("./a/", "/b/", "./c"), "/b/c") // "/b/" is an absolute path, hence it negates all segments prior to it.
1621
+ * eq(fn("file:///", "a/b/c", "./d"), "file:///a/b/d") // the first "file:///" segment is an absolute path according to our `custom_absolute_path_segment_tester`
1622
+ * eq(fn("file:/", "a/b/c", "./d"), "/x/y/z/file:/a/b/d") // "file:/" is not considered as an absolute path by `custom_absolute_path_segment_tester`, thus it the cwd will be prepended to the final path
1623
+ * eq(fn("file:", "a/b/c", "./d"), "/x/y/z/file:/a/b/d") // same as before, but we're putting emphasis on the mandatory "/" separator that gets added after the "file:" segment
1624
+ * eq(fn("a/b/c", "http://d/e/f.txt"), "http://d/e/f.txt") // the "http://" segment is identified as an absolute path by our `custom_absolute_path_segment_tester`
1625
+ * ```
1626
+ */
1627
+ export const resolvePosixPathFactory = (absolute_current_dir, absolute_segment_test_fn = isAbsolutePath) => {
1628
+ const getCwdPath = isString(absolute_current_dir)
1629
+ ? (() => absolute_current_dir)
1630
+ : absolute_current_dir;
1631
+ return (...segments) => {
1632
+ // first, we find the index of the last segment that is absolutely defined.
1633
+ // if there isn't one, then we use `getCwdPath` as our base path
1634
+ const last_abs_segment_idx = segments.findLastIndex(absolute_segment_test_fn);
1635
+ if (last_abs_segment_idx >= 0) {
1636
+ segments = segments.slice(last_abs_segment_idx);
1637
+ }
1638
+ else {
1639
+ segments.unshift(ensureEndSlash(getCwdPath()));
1640
+ }
1641
+ return joinPosixPaths(...segments);
1642
+ };
1643
+ };
1644
+ /** this is a factory function for creating customizable path-resolving functions.
1645
+ *
1646
+ * {@inheritDoc resolvePosixPathFactory}
1647
+ *
1648
+ * @example
1649
+ * ```ts
1650
+ * import { assertEquals } from "jsr:@std/assert"
1651
+ *
1652
+ * const
1653
+ * cwd = "x:\\y\\z",
1654
+ * getCwd = () => (cwd),
1655
+ * // we also define a custom absolute segment path tester function that will identify "file://" and "http://" segments as absolute path,
1656
+ * // in addition to the standard filesystem local path absoluteness tester `isAbsolutePath`.
1657
+ * custom_absolute_path_segment_tester = (segment: string) => {
1658
+ * if (isAbsolutePath(segment)) { return true }
1659
+ * if (segment.startsWith("file://")) { return true }
1660
+ * if (segment.startsWith("http://")) { return true }
1661
+ * return false
1662
+ * },
1663
+ * resolvePosixPath = resolvePathFactory(getCwd, custom_absolute_path_segment_tester)
1664
+ *
1665
+ * // aliasing our functions for brevity
1666
+ * const eq = assertEquals, fn = resolvePosixPath
1667
+ *
1668
+ * // relative path resolution
1669
+ * eq(fn("a", "b", "c.zip"), "x:/y/z/a/b/c.zip")
1670
+ * eq(fn(".\\a", "b", "c\\"), "x:/y/z/a/b/c/")
1671
+ * eq(fn("a/", "b/", "c/"), "x:/y/z/a/b/c/")
1672
+ * eq(fn("../a", "../b", "c\\"), "x:/b/c/")
1673
+ * eq(fn("../a/", "../b", "c/"), "x:/y/b/c/")
1674
+ * eq(fn("a", "b", "c.zip", "./"), "x:/y/z/a/b/")
1675
+ *
1676
+ * // pre-existing absolute path resolution
1677
+ * eq(fn("./a", "/b", "c:/"), "c:/") // both "/b" and "c:/" are absolute paths, and so they purge all segments behind them.
1678
+ * eq(fn("./a/", "\\b/", "./c"), "/b/c") // "\\b/" is an absolute path, hence it negates all segments prior to it.
1679
+ * eq(fn("file:///", "a/b/c", "./d"), "file:///a/b/d") // the first "file:///" segment is an absolute path according to our `custom_absolute_path_segment_tester`
1680
+ * eq(fn("file:/", "a/b/c", "./d"), "x:/y/z/file:/a/b/d") // "file:/" is not considered as an absolute path by `custom_absolute_path_segment_tester`, thus it the cwd will be prepended to the final path
1681
+ * eq(fn("file:", "a/b\\c", ".\\d"), "x:/y/z/file:/a/b/d") // same as before, but we're putting emphasis on the mandatory "/" separator that gets added after the "file:" segment
1682
+ * eq(fn("a/b/c", "http://d/e/f.txt"), "http://d/e/f.txt") // the "http://" segment is identified as an absolute path by our `custom_absolute_path_segment_tester`
1683
+ * ```
1684
+ *
1685
+ * for lots of other (posix-only) test cases, see the doc comments of {@link resolvePosixPathFactory}.
1686
+ */
1687
+ export const resolvePathFactory = (absolute_current_dir, absolute_segment_test_fn = isAbsolutePath) => {
1688
+ if (isString(absolute_current_dir)) {
1689
+ absolute_current_dir = pathToPosixPath(absolute_current_dir);
1690
+ }
1691
+ const getCwdPath = isString(absolute_current_dir)
1692
+ ? (() => absolute_current_dir)
1693
+ : (() => pathToPosixPath(absolute_current_dir())), posix_path_resolver = resolvePosixPathFactory(getCwdPath, absolute_segment_test_fn);
1694
+ return (...segments) => posix_path_resolver(...segments.map(pathToPosixPath));
1695
+ };
1696
+ const glob_pattern_to_regex_escape_control_chars = /[\.\+\^\$\{\}\(\)\|\[\]\\]/g, glob_starstar_wildcard_token = "<<<StarStarWildcard>>>";
1697
+ /** convert a glob string to a regex object.
1698
+ *
1699
+ * TODO: purge the info below:
1700
+ * in this implementation, only the wildcards `"*"`, `"**"`, and the optional `"?"` is given meaning.
1701
+ * all else, including parenthesis, brackets, dots, and backslash, are escaped when being converted into a regex.
1702
+ *
1703
+ * TODO: test it
1704
+ * TODO: also implement a `isGlobPattern` function
1705
+ * TODO: make it so that the user can configure which features to turn on or off
1706
+ *
1707
+ * @beta
1708
+ */
1709
+ export const globPatternToRegex = (glob_pattern) => {
1710
+ const
1711
+ // first, convert windows path separator to posix path separator
1712
+ posix_pattern = pathToPosixPath(glob_pattern), regex_str = posix_pattern
1713
+ // escape regex special characters, except for "*", "?", "[", "]", "{", and "}"
1714
+ .replace(glob_pattern_to_regex_escape_control_chars, "\\$&")
1715
+ // replace "**/" or "**" directory wildcard with a temporary `glob_starstar_wildcard_token`, and later we will convert to ".*"
1716
+ .replace(/\*\*\/?/g, glob_starstar_wildcard_token)
1717
+ // replace single "*" with "[^/]*" to match anything except directory separators
1718
+ .replace(/\*/g, "[^\/]*")
1719
+ // replace "?" with "." to match any single character
1720
+ .replace(/\?/g, ".")
1721
+ // convert negated glob ranges (like "[!abc]") to regex negation ("[^abc]")
1722
+ .replace(/\[!(.*)\]/g, "[^$1]")
1723
+ // support for character ranges like "[a-z]"
1724
+ .replace(/\[(.*)\]/g, "[$1]")
1725
+ // support for braces like "{js,ts}"
1726
+ .replace(/\{([^,}]+),([^}]+)\}/g, "($1|$2)")
1727
+ // put back the ".*" wildcards where they belong
1728
+ .replace(glob_starstar_wildcard_token, ".*");
1729
+ return new RegExp("^" + regex_str + "$");
1730
+ };