@gesslar/toolkit 3.9.0 → 3.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/browser/lib/Data.js +89 -5
- package/src/browser/lib/TypeSpec.js +3 -1
- package/src/lib/CappedDirectoryObject.js +62 -484
- package/src/lib/DirectoryObject.js +86 -170
- package/src/lib/FS.js +221 -70
- package/src/lib/FileObject.js +80 -111
- package/src/lib/TempDirectoryObject.js +92 -141
- package/src/lib/Valid.js +1 -1
- package/src/types/browser/lib/Data.d.ts +46 -2
- package/src/types/browser/lib/Data.d.ts.map +1 -1
- package/src/types/browser/lib/TypeSpec.d.ts.map +1 -1
- package/src/types/lib/CappedDirectoryObject.d.ts +30 -55
- package/src/types/lib/CappedDirectoryObject.d.ts.map +1 -1
- package/src/types/lib/DirectoryObject.d.ts +8 -60
- package/src/types/lib/DirectoryObject.d.ts.map +1 -1
- package/src/types/lib/FS.d.ts +115 -15
- package/src/types/lib/FS.d.ts.map +1 -1
- package/src/types/lib/FileObject.d.ts +6 -10
- package/src/types/lib/FileObject.d.ts.map +1 -1
- package/src/types/lib/TempDirectoryObject.d.ts +15 -62
- package/src/types/lib/TempDirectoryObject.d.ts.map +1 -1
package/src/lib/FS.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file FS.js
|
|
3
3
|
*
|
|
4
|
-
* File system utilities for path manipulation, file discovery, and path
|
|
5
|
-
*
|
|
4
|
+
* File system utilities for path manipulation, file discovery, and path
|
|
5
|
+
* resolution.
|
|
6
|
+
*
|
|
7
|
+
* Provides glob-based file search, URI conversion, and intelligent path
|
|
8
|
+
* merging.
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
|
-
import {globby} from "globby"
|
|
9
11
|
import path from "node:path"
|
|
10
12
|
import url from "node:url"
|
|
11
13
|
|
|
12
14
|
import Collection from "../browser/lib/Collection.js"
|
|
13
|
-
import
|
|
15
|
+
import Data from "../browser/lib/Data.js"
|
|
14
16
|
import Valid from "./Valid.js"
|
|
15
17
|
|
|
16
18
|
/** @typedef {import("./FileObject.js").default} FileObject */
|
|
@@ -58,7 +60,7 @@ export default class FS {
|
|
|
58
60
|
* @returns {string} The fixed path
|
|
59
61
|
*/
|
|
60
62
|
static fixSlashes(pathName) {
|
|
61
|
-
return pathName.replace(/\\/g, "/")
|
|
63
|
+
return path.normalize(pathName.replace(/\\/g, "/"))
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
/**
|
|
@@ -68,12 +70,10 @@ export default class FS {
|
|
|
68
70
|
* @param {string} pathName - The path to convert
|
|
69
71
|
* @returns {string} The URI
|
|
70
72
|
*/
|
|
71
|
-
static
|
|
73
|
+
static pathToUrl(pathName) {
|
|
72
74
|
try {
|
|
73
75
|
return url.pathToFileURL(pathName).href
|
|
74
|
-
} catch
|
|
75
|
-
void e // stfu linter
|
|
76
|
-
|
|
76
|
+
} catch {
|
|
77
77
|
return pathName
|
|
78
78
|
}
|
|
79
79
|
}
|
|
@@ -85,61 +85,19 @@ export default class FS {
|
|
|
85
85
|
* @param {string} pathName - The URI to convert
|
|
86
86
|
* @returns {string} The path
|
|
87
87
|
*/
|
|
88
|
-
static
|
|
88
|
+
static urlToPath(pathName) {
|
|
89
89
|
try {
|
|
90
|
-
return url.fileURLToPath(pathName)
|
|
91
|
-
} catch
|
|
90
|
+
return url.fileURLToPath(new URL(pathName).pathName)
|
|
91
|
+
} catch {
|
|
92
92
|
return pathName
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
/**
|
|
97
|
-
* Retrieve all files matching a specific glob pattern.
|
|
98
|
-
*
|
|
99
|
-
* @static
|
|
100
|
-
* @param {string|Array<string>} glob - The glob pattern(s) to search.
|
|
101
|
-
* @returns {Promise<Array<FileObject>>} A promise that resolves to an array of file objects
|
|
102
|
-
* @throws {Sass} If the input is not a string or array of strings.
|
|
103
|
-
* @throws {Sass} If the glob pattern array is empty or for other search failures.
|
|
104
|
-
*/
|
|
105
|
-
static async getFiles(glob) {
|
|
106
|
-
const isString = typeof glob === "string"
|
|
107
|
-
const isArray = Array.isArray(glob)
|
|
108
|
-
const isStringArray = isArray && glob.every(item => typeof item === "string")
|
|
109
|
-
|
|
110
|
-
Valid.assert(
|
|
111
|
-
(isString && glob.length > 0) ||
|
|
112
|
-
(isStringArray && glob.length > 0),
|
|
113
|
-
"glob must be a non-empty string or array of strings.",
|
|
114
|
-
1
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
const globbyArray = (
|
|
118
|
-
isString
|
|
119
|
-
? glob.split("|").map(g => g.trim()).filter(Boolean)
|
|
120
|
-
: glob
|
|
121
|
-
).map(g => FS.fixSlashes(g))
|
|
122
|
-
|
|
123
|
-
if(isArray && !globbyArray.length)
|
|
124
|
-
throw Sass.new(
|
|
125
|
-
`Invalid glob pattern: Array cannot be empty. Got ${JSON.stringify(glob)}`,
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
// Use Globby to fetch matching files
|
|
129
|
-
const {default: FileObject} = await import("./FileObject.js")
|
|
130
|
-
|
|
131
|
-
const filesArray = await globby(globbyArray)
|
|
132
|
-
const files = filesArray.map(file => new FileObject(file))
|
|
133
|
-
|
|
134
|
-
// Flatten the result and remove duplicates
|
|
135
|
-
return files
|
|
136
|
-
}
|
|
137
|
-
|
|
138
96
|
/**
|
|
139
97
|
* Computes the relative path from one file or directory to another.
|
|
140
98
|
*
|
|
141
|
-
* If the target is outside the source (i.e., the relative path starts with
|
|
142
|
-
* returns the absolute path to the target instead.
|
|
99
|
+
* If the target is outside the source (i.e., the relative path starts with
|
|
100
|
+
* ".."), returns the absolute path to the target instead.
|
|
143
101
|
*
|
|
144
102
|
* @static
|
|
145
103
|
* @param {FileObject|DirectoryObject} from - The source file or directory object
|
|
@@ -159,7 +117,8 @@ export default class FS {
|
|
|
159
117
|
}
|
|
160
118
|
|
|
161
119
|
/**
|
|
162
|
-
* Merge two paths by finding overlapping segments and combining them
|
|
120
|
+
* Merge two paths by finding overlapping segments and combining them
|
|
121
|
+
* efficiently
|
|
163
122
|
*
|
|
164
123
|
* @static
|
|
165
124
|
* @param {string} path1 - The first path
|
|
@@ -169,8 +128,8 @@ export default class FS {
|
|
|
169
128
|
*/
|
|
170
129
|
static mergeOverlappingPaths(path1, path2, sep=path.sep) {
|
|
171
130
|
const isAbsolutePath1 = path.isAbsolute(path1)
|
|
172
|
-
const from = path1.split(sep).filter(Boolean)
|
|
173
|
-
const to = path2.split(sep).filter(Boolean)
|
|
131
|
+
const from = path.normalize(path1).split(sep).filter(Boolean)
|
|
132
|
+
const to = path.normalize(path2).split(sep).filter(Boolean)
|
|
174
133
|
|
|
175
134
|
// If they're the same, just return path1
|
|
176
135
|
if(to.length === from.length && from.every((f, i) => to[i] === f))
|
|
@@ -203,9 +162,16 @@ export default class FS {
|
|
|
203
162
|
* @returns {string} The resolved path
|
|
204
163
|
*/
|
|
205
164
|
static resolvePath(fromPath, toPath) {
|
|
165
|
+
Valid.type(fromPath, "String")
|
|
166
|
+
Valid.type(toPath, "String")
|
|
167
|
+
|
|
206
168
|
// Normalize inputs
|
|
207
|
-
const from = fromPath?.trim() ?? ""
|
|
208
|
-
const to = toPath?.trim() ?? ""
|
|
169
|
+
const from = this.fixSlashes(fromPath?.trim() ?? "")
|
|
170
|
+
const to = this.fixSlashes(toPath?.trim() ?? "")
|
|
171
|
+
|
|
172
|
+
// Are they the same? What's the resolve?
|
|
173
|
+
if(from === to)
|
|
174
|
+
return from
|
|
209
175
|
|
|
210
176
|
// Handle empty cases
|
|
211
177
|
if(!from && !to)
|
|
@@ -217,20 +183,205 @@ export default class FS {
|
|
|
217
183
|
if(!to)
|
|
218
184
|
return from
|
|
219
185
|
|
|
220
|
-
const normalizedTo = /^\.\//.test(to)
|
|
221
|
-
? path.normalize(to)
|
|
222
|
-
: to
|
|
223
|
-
|
|
224
186
|
// Strategy 1: If 'to' is absolute, it's standalone
|
|
225
|
-
if(path.isAbsolute(
|
|
226
|
-
return
|
|
187
|
+
if(path.isAbsolute(to))
|
|
188
|
+
return path.resolve(to)
|
|
227
189
|
|
|
228
190
|
// Strategy 2: If 'to' contains relative navigation
|
|
229
|
-
if(to.startsWith("../"))
|
|
230
|
-
return path.resolve(from,
|
|
191
|
+
if(to.startsWith(this.fixSlashes("../")))
|
|
192
|
+
return path.resolve(from, to)
|
|
231
193
|
|
|
232
194
|
// Strategy 3: Try overlap-based merging, which will default to a basic
|
|
233
195
|
// join if no overlap
|
|
234
|
-
return FS.mergeOverlappingPaths(from,
|
|
196
|
+
return FS.mergeOverlappingPaths(from, to)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Check if a candidate path is contained within a container path.
|
|
201
|
+
*
|
|
202
|
+
* @static
|
|
203
|
+
* @param {string} container - The container path to check against
|
|
204
|
+
* @param {string} candidate - The candidate path that might be contained
|
|
205
|
+
* @returns {boolean} True if candidate is within container, false otherwise
|
|
206
|
+
* @throws {Sass} If container is not a non-empty string
|
|
207
|
+
* @throws {Sass} If candidate is not a non-empty string
|
|
208
|
+
* @example
|
|
209
|
+
* FS.pathContains("/home/user", "/home/user/docs") // true
|
|
210
|
+
* FS.pathContains("/home/user", "/home/other") // false
|
|
211
|
+
*/
|
|
212
|
+
static pathContains(container, candidate) {
|
|
213
|
+
Valid.type(container, "String", {allowEmpty: false})
|
|
214
|
+
Valid.type(candidate, "String", {allowEmpty: false})
|
|
215
|
+
|
|
216
|
+
const realPath = Data.append(container, "/") // bookend this mofo
|
|
217
|
+
|
|
218
|
+
return candidate.startsWith(realPath)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Convert an absolute path to a relative path by finding overlapping segments.
|
|
223
|
+
* Returns the relative portion of the 'to' path after the last occurrence
|
|
224
|
+
* of the final segment from the 'from' path.
|
|
225
|
+
*
|
|
226
|
+
* @static
|
|
227
|
+
* @param {string} from - The base path to calculate relative from
|
|
228
|
+
* @param {string} to - The target path to make relative
|
|
229
|
+
* @param {string} [sep=path.sep] - The path separator to use (defaults to system separator)
|
|
230
|
+
* @returns {string|null} The relative path, empty string if paths are identical, or null if no overlap found
|
|
231
|
+
* @example
|
|
232
|
+
* FS.toRelativePath("/projects/toolkit", "/projects/toolkit/src") // "src"
|
|
233
|
+
* FS.toRelativePath("/home/user", "/home/user") // ""
|
|
234
|
+
* FS.toRelativePath("/projects/app", "/other/path") // null
|
|
235
|
+
*/
|
|
236
|
+
static toRelativePath(from, to, sep=path.sep) {
|
|
237
|
+
// If they're the same, just return ""
|
|
238
|
+
if(from === to)
|
|
239
|
+
return ""
|
|
240
|
+
|
|
241
|
+
const fromTrail = from.split(sep)
|
|
242
|
+
const toTrail = to.split(sep)
|
|
243
|
+
const overlapIndex = toTrail.findIndex(curr => curr === fromTrail.at(-1))
|
|
244
|
+
|
|
245
|
+
// If overlap is found, slice and join
|
|
246
|
+
if(overlapIndex !== -1) {
|
|
247
|
+
const relative = toTrail.slice(overlapIndex+1)
|
|
248
|
+
|
|
249
|
+
return relative.join(sep)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// If no overlap, we got nothing, soz.
|
|
253
|
+
return null
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Find the common root path between two paths by identifying overlapping segments.
|
|
258
|
+
* Returns the portion of 'from' that matches up to the overlap point in 'to'.
|
|
259
|
+
*
|
|
260
|
+
* @static
|
|
261
|
+
* @param {string} from - The first path to compare
|
|
262
|
+
* @param {string} to - The second path to find common root with
|
|
263
|
+
* @param {string} [sep=path.sep] - The path separator to use (defaults to system separator)
|
|
264
|
+
* @returns {string|null} The common root path, the original path if identical, or null if no overlap found
|
|
265
|
+
* @throws {Sass} If from is not a non-empty string
|
|
266
|
+
* @throws {Sass} If to is not a non-empty string
|
|
267
|
+
* @example
|
|
268
|
+
* FS.getCommonRootPath("/projects/toolkit/src", "/projects/toolkit/tests") // "/projects/toolkit"
|
|
269
|
+
* FS.getCommonRootPath("/home/user", "/home/user") // "/home/user"
|
|
270
|
+
* FS.getCommonRootPath("/projects/app", "/other/path") // null
|
|
271
|
+
*/
|
|
272
|
+
static getCommonRootPath(from, to, sep=path.sep) {
|
|
273
|
+
Valid.type(from, "String", {allowEmpty: false})
|
|
274
|
+
Valid.type(to, "String", {allowEmpty: false})
|
|
275
|
+
|
|
276
|
+
// If they're the same, just return one or t'other, tis no mattah
|
|
277
|
+
if(from === to)
|
|
278
|
+
return from
|
|
279
|
+
|
|
280
|
+
const fromTrail = from.split(sep)
|
|
281
|
+
const toTrail = to.split(sep)
|
|
282
|
+
const overlapIndex = toTrail.findLastIndex(
|
|
283
|
+
curr => curr === fromTrail.at(-1)
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
// If overlap is found, slice and join
|
|
287
|
+
if(overlapIndex !== -1) {
|
|
288
|
+
const relative = fromTrail.slice(0, overlapIndex+1)
|
|
289
|
+
|
|
290
|
+
return relative.join(sep)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// If no overlap, we got nothing, soz.
|
|
294
|
+
return null
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* @typedef {object} PathParts
|
|
299
|
+
* @property {string} base - The file name with extension
|
|
300
|
+
* @property {string} dir - The directory path
|
|
301
|
+
* @property {string} ext - The file extension (including dot)
|
|
302
|
+
*/
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Deconstruct a file or directory name into parts.
|
|
306
|
+
*
|
|
307
|
+
* @static
|
|
308
|
+
* @param {string} pathName - The file/directory name to deconstruct
|
|
309
|
+
* @returns {PathParts} The filename parts
|
|
310
|
+
* @throws {Sass} If not a string of more than 1 character
|
|
311
|
+
*/
|
|
312
|
+
static pathParts(pathName) {
|
|
313
|
+
Valid.type(pathName, "String", {allowEmpty: false})
|
|
314
|
+
|
|
315
|
+
return path.parse(pathName)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Convert a virtual capped path to its real filesystem path.
|
|
320
|
+
* For capped objects, resolves the virtual path relative to the cap's real path.
|
|
321
|
+
* For uncapped objects, returns the path unchanged.
|
|
322
|
+
*
|
|
323
|
+
* @static
|
|
324
|
+
* @param {FileObject|DirectoryObject} fileOrDirectoryObject - The file or directory object to convert
|
|
325
|
+
* @returns {string} The real filesystem path
|
|
326
|
+
* @throws {Sass} If parameter is not a FileObject or DirectoryObject
|
|
327
|
+
* @example
|
|
328
|
+
* const temp = new TempDirectoryObject("myapp")
|
|
329
|
+
* const file = temp.getFile("/config.json")
|
|
330
|
+
* FS.virtualToRealPath(file) // "/tmp/myapp-ABC123/config.json"
|
|
331
|
+
*
|
|
332
|
+
* @example
|
|
333
|
+
* const regular = new FileObject("/home/user/file.txt")
|
|
334
|
+
* FS.virtualToRealPath(regular) // "/home/user/file.txt"
|
|
335
|
+
*/
|
|
336
|
+
static virtualToRealPath(fileOrDirectoryObject) {
|
|
337
|
+
Valid.type(fileOrDirectoryObject, "FileObject|DirectoryObject")
|
|
338
|
+
|
|
339
|
+
let target, cap
|
|
340
|
+
|
|
341
|
+
if(fileOrDirectoryObject.isFile) {
|
|
342
|
+
if(!fileOrDirectoryObject.parent.isCapped) {
|
|
343
|
+
return fileOrDirectoryObject.path
|
|
344
|
+
} else {
|
|
345
|
+
target = fileOrDirectoryObject.path
|
|
346
|
+
cap = fileOrDirectoryObject.parent.cap.real.path
|
|
347
|
+
}
|
|
348
|
+
} else {
|
|
349
|
+
if(!fileOrDirectoryObject.isCapped) {
|
|
350
|
+
return fileOrDirectoryObject.path
|
|
351
|
+
} else {
|
|
352
|
+
target = fileOrDirectoryObject.path
|
|
353
|
+
cap = fileOrDirectoryObject.cap.real.path
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return this.resolvePath(cap, target)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Convert an absolute path to a relative format by removing the root component.
|
|
362
|
+
* By default, keeps a leading separator (making it "absolute-like relative").
|
|
363
|
+
* Use forceActuallyRelative to get a truly relative path without leading separator.
|
|
364
|
+
*
|
|
365
|
+
* @static
|
|
366
|
+
* @param {string} pathToCheck - The path to convert (returned unchanged if already relative)
|
|
367
|
+
* @param {boolean} [forceActuallyRelative=false] - If true, removes leading separator for truly relative path
|
|
368
|
+
* @returns {string} The relative path (with or without leading separator based on forceActuallyRelative)
|
|
369
|
+
* @example
|
|
370
|
+
* FS.absoluteToRelative("/home/user/docs") // "/home/user/docs" (with leading /)
|
|
371
|
+
* FS.absoluteToRelative("/home/user/docs", true) // "home/user/docs" (truly relative)
|
|
372
|
+
* FS.absoluteToRelative("relative/path") // "relative/path" (unchanged)
|
|
373
|
+
*/
|
|
374
|
+
static absoluteToRelative(pathToCheck, forceActuallyRelative=false) {
|
|
375
|
+
if(!path.isAbsolute(pathToCheck))
|
|
376
|
+
return pathToCheck
|
|
377
|
+
|
|
378
|
+
const {root} = this.pathParts(pathToCheck)
|
|
379
|
+
const sep = path.sep
|
|
380
|
+
const chopped = Data.chopLeft(pathToCheck, root)
|
|
381
|
+
const absolute = forceActuallyRelative
|
|
382
|
+
? chopped
|
|
383
|
+
: Data.prepend(chopped, sep)
|
|
384
|
+
|
|
385
|
+
return absolute
|
|
235
386
|
}
|
|
236
387
|
}
|