@gesslar/toolkit 3.8.0 → 3.12.3
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 +87 -467
- package/src/lib/DirectoryObject.js +109 -147
- package/src/lib/FS.js +221 -70
- package/src/lib/FileObject.js +78 -81
- package/src/lib/TempDirectoryObject.js +93 -129
- 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 +48 -56
- package/src/types/lib/CappedDirectoryObject.d.ts.map +1 -1
- package/src/types/lib/DirectoryObject.d.ts +24 -54
- 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 -3
- package/src/types/lib/FileObject.d.ts.map +1 -1
- package/src/types/lib/TempDirectoryObject.d.ts +19 -59
- 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
|
}
|
package/src/lib/FileObject.js
CHANGED
|
@@ -70,65 +70,61 @@ export default class FileObject extends FS {
|
|
|
70
70
|
isFile: true,
|
|
71
71
|
isDirectory: false,
|
|
72
72
|
parent: null,
|
|
73
|
+
parentPath: null,
|
|
73
74
|
})
|
|
74
75
|
|
|
75
76
|
/**
|
|
76
77
|
* Constructs a FileObject instance.
|
|
77
78
|
*
|
|
78
|
-
* @param {string
|
|
79
|
+
* @param {string} fileName - The file path
|
|
79
80
|
* @param {DirectoryObject|string|null} [parent] - The parent directory (object or string)
|
|
80
81
|
*/
|
|
81
82
|
constructor(fileName, parent=null) {
|
|
82
83
|
super()
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
fileName = fileName.path
|
|
87
|
-
|
|
88
|
-
if(!fileName || typeof fileName !== "string" || fileName.length === 0)
|
|
89
|
-
throw Sass.new("fileName must be a non-empty string")
|
|
85
|
+
Valid.type(fileName, "String", {allowEmpty: false})
|
|
86
|
+
Valid.type(parent, "Null|String|DirectoryObject", {allowEmpty: false})
|
|
90
87
|
|
|
91
88
|
const fixedFile = FS.fixSlashes(fileName)
|
|
92
|
-
const {dir,base,ext} =
|
|
89
|
+
const {dir, base, ext} = FS.pathParts(fixedFile)
|
|
93
90
|
|
|
94
91
|
const parentObject = (() => {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return new DirectoryObject(parent)
|
|
98
|
-
case "DirectoryObject":
|
|
99
|
-
case "CappedDirectoryObject":
|
|
100
|
-
case "TempDirectoryObject":
|
|
101
|
-
return parent
|
|
102
|
-
default:
|
|
103
|
-
return new DirectoryObject(dir)
|
|
104
|
-
}
|
|
105
|
-
})()
|
|
92
|
+
if(Data.isType(parent, "String"))
|
|
93
|
+
return new DirectoryObject(parent)
|
|
106
94
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const final = FS.resolvePath(parentPath ?? ".", fixedFile)
|
|
95
|
+
if(Data.isType(parent, "DirectoryObject"))
|
|
96
|
+
return parent
|
|
110
97
|
|
|
111
|
-
|
|
112
|
-
|
|
98
|
+
return new DirectoryObject(dir)
|
|
99
|
+
})()
|
|
113
100
|
|
|
114
|
-
//
|
|
115
|
-
|
|
101
|
+
// If the parent is passed, we need to treat the fileName as relative,
|
|
102
|
+
// regardless of what you-know-who says.
|
|
103
|
+
const resolvedFilename = parent
|
|
104
|
+
? FS.absoluteToRelative(fixedFile, true)
|
|
105
|
+
: fixedFile
|
|
116
106
|
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
? parentObject
|
|
123
|
-
: new DirectoryObject(actualParentPath)
|
|
107
|
+
// Use real path if parent is capped, otherwise use path
|
|
108
|
+
const parentPath = parentObject.real?.path || parentObject.path
|
|
109
|
+
const resolved = FS.resolvePath(parentPath ?? ".", resolvedFilename)
|
|
110
|
+
const {dir: actualParent} = FS.pathParts(resolved)
|
|
111
|
+
const url = new URL(FS.pathToUrl(resolved))
|
|
124
112
|
|
|
125
|
-
this.#meta.supplied =
|
|
113
|
+
this.#meta.supplied = fileName
|
|
126
114
|
this.#meta.path = resolved
|
|
127
115
|
this.#meta.url = url
|
|
128
116
|
this.#meta.name = base
|
|
129
117
|
this.#meta.extension = ext
|
|
130
118
|
this.#meta.module = path.basename(this.supplied, this.extension)
|
|
131
|
-
this.#meta.
|
|
119
|
+
this.#meta.parentPath = actualParent
|
|
120
|
+
// Preserve capped parent or use actualParent path match
|
|
121
|
+
const useCappedParent =
|
|
122
|
+
parentObject.isCapped ||
|
|
123
|
+
FS.fixSlashes(actualParent) === FS.fixSlashes(parentObject.path)
|
|
124
|
+
|
|
125
|
+
this.#meta.parent = useCappedParent
|
|
126
|
+
? parentObject
|
|
127
|
+
: new DirectoryObject(actualParent)
|
|
132
128
|
|
|
133
129
|
Object.freeze(this.#meta)
|
|
134
130
|
}
|
|
@@ -157,6 +153,7 @@ export default class FileObject extends FS {
|
|
|
157
153
|
extension: this.extension,
|
|
158
154
|
isFile: this.isFile,
|
|
159
155
|
isDirectory: this.isDirectory,
|
|
156
|
+
parentPath: this.parentPath,
|
|
160
157
|
parent: this.parent ? this.parent.path : null
|
|
161
158
|
}
|
|
162
159
|
}
|
|
@@ -190,7 +187,9 @@ export default class FileObject extends FS {
|
|
|
190
187
|
|
|
191
188
|
/**
|
|
192
189
|
* Returns the file path. If the parent is a capped directory, returns the
|
|
193
|
-
* virtual path relative to the cap. Otherwise returns the real filesystem
|
|
190
|
+
* virtual path relative to the cap. Otherwise returns the real filesystem
|
|
191
|
+
* path.
|
|
192
|
+
*
|
|
194
193
|
* Use `.real.path` to always get the actual filesystem path.
|
|
195
194
|
*
|
|
196
195
|
* @returns {string} The file path (virtual if parent is capped, real otherwise)
|
|
@@ -200,13 +199,14 @@ export default class FileObject extends FS {
|
|
|
200
199
|
const parent = this.#meta.parent
|
|
201
200
|
|
|
202
201
|
// If parent is capped, return virtual path
|
|
203
|
-
if(parent?.
|
|
204
|
-
const cap = parent.cap
|
|
202
|
+
if(parent?.isCapped) {
|
|
203
|
+
const cap = parent.cap.real.path
|
|
205
204
|
const capResolved = path.resolve(cap)
|
|
206
|
-
const
|
|
205
|
+
const relativeRealPath = FS.absoluteToRelative(realPath)
|
|
206
|
+
const absolute = FS.resolvePath(capResolved, relativeRealPath)
|
|
207
207
|
|
|
208
208
|
// Return with leading slash to indicate it's cap-relative
|
|
209
|
-
return
|
|
209
|
+
return FS.absoluteToRelative(absolute)
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
// Otherwise return real path
|
|
@@ -220,12 +220,9 @@ export default class FileObject extends FS {
|
|
|
220
220
|
* @returns {URL} The file URL (virtual if parent is capped, real otherwise)
|
|
221
221
|
*/
|
|
222
222
|
get url() {
|
|
223
|
-
const parent = this.#meta.parent
|
|
224
|
-
|
|
225
223
|
// If parent is capped, return virtual URL
|
|
226
|
-
if(parent?.
|
|
227
|
-
return new URL(FS.
|
|
228
|
-
}
|
|
224
|
+
if(this.parent?.isCapped)
|
|
225
|
+
return new URL(FS.pathToUrl(this.path))
|
|
229
226
|
|
|
230
227
|
return this.#meta.url
|
|
231
228
|
}
|
|
@@ -293,6 +290,10 @@ export default class FileObject extends FS {
|
|
|
293
290
|
return this.#meta.parent
|
|
294
291
|
}
|
|
295
292
|
|
|
293
|
+
get parentPath() {
|
|
294
|
+
return this.#meta.parentPath
|
|
295
|
+
}
|
|
296
|
+
|
|
296
297
|
/**
|
|
297
298
|
* Returns a plain FileObject representing the actual filesystem location.
|
|
298
299
|
* This provides an "escape hatch" when working with capped directories,
|
|
@@ -310,7 +311,7 @@ export default class FileObject extends FS {
|
|
|
310
311
|
* file.real.parent.parent // Can traverse outside the cap
|
|
311
312
|
*/
|
|
312
313
|
get real() {
|
|
313
|
-
return new FileObject(this
|
|
314
|
+
return new FileObject(this.path)
|
|
314
315
|
}
|
|
315
316
|
|
|
316
317
|
/**
|
|
@@ -323,7 +324,7 @@ export default class FileObject extends FS {
|
|
|
323
324
|
await fs.access(this.#meta.path, fs.constants.R_OK)
|
|
324
325
|
|
|
325
326
|
return true
|
|
326
|
-
} catch
|
|
327
|
+
} catch {
|
|
327
328
|
return false
|
|
328
329
|
}
|
|
329
330
|
}
|
|
@@ -338,7 +339,7 @@ export default class FileObject extends FS {
|
|
|
338
339
|
await fs.access(this.#meta.path, fs.constants.W_OK)
|
|
339
340
|
|
|
340
341
|
return true
|
|
341
|
-
} catch
|
|
342
|
+
} catch {
|
|
342
343
|
return false
|
|
343
344
|
}
|
|
344
345
|
}
|
|
@@ -353,7 +354,7 @@ export default class FileObject extends FS {
|
|
|
353
354
|
await fs.access(this.#meta.path, fs.constants.F_OK)
|
|
354
355
|
|
|
355
356
|
return true
|
|
356
|
-
} catch
|
|
357
|
+
} catch {
|
|
357
358
|
return false
|
|
358
359
|
}
|
|
359
360
|
}
|
|
@@ -368,7 +369,7 @@ export default class FileObject extends FS {
|
|
|
368
369
|
const stat = await fs.stat(this.#meta.path)
|
|
369
370
|
|
|
370
371
|
return stat.size
|
|
371
|
-
} catch
|
|
372
|
+
} catch {
|
|
372
373
|
return null
|
|
373
374
|
}
|
|
374
375
|
}
|
|
@@ -384,31 +385,11 @@ export default class FileObject extends FS {
|
|
|
384
385
|
const stat = await fs.stat(this.#meta.path)
|
|
385
386
|
|
|
386
387
|
return stat.mtime
|
|
387
|
-
} catch
|
|
388
|
+
} catch {
|
|
388
389
|
return null
|
|
389
390
|
}
|
|
390
391
|
}
|
|
391
392
|
|
|
392
|
-
/**
|
|
393
|
-
* @typedef {object} FileParts
|
|
394
|
-
* @property {string} base - The file name with extension
|
|
395
|
-
* @property {string} dir - The directory path
|
|
396
|
-
* @property {string} ext - The file extension (including dot)
|
|
397
|
-
*/
|
|
398
|
-
|
|
399
|
-
/**
|
|
400
|
-
* Deconstruct a filename into parts
|
|
401
|
-
*
|
|
402
|
-
* @param {string} fileName - The filename to deconstruct
|
|
403
|
-
* @returns {FileParts} The filename parts
|
|
404
|
-
*/
|
|
405
|
-
#deconstructFilenameToParts(fileName) {
|
|
406
|
-
Valid.assert(typeof fileName === "string" && fileName.length > 0,
|
|
407
|
-
"file must be a non-zero length string", 1)
|
|
408
|
-
|
|
409
|
-
return path.parse(fileName)
|
|
410
|
-
}
|
|
411
|
-
|
|
412
393
|
/**
|
|
413
394
|
* Reads the content of a file asynchronously.
|
|
414
395
|
*
|
|
@@ -464,14 +445,30 @@ export default class FileObject extends FS {
|
|
|
464
445
|
* await file.write(JSON.stringify({key: 'value'}))
|
|
465
446
|
*/
|
|
466
447
|
async write(content, encoding="utf8") {
|
|
467
|
-
|
|
468
|
-
|
|
448
|
+
const realPath = FS.virtualToRealPath(this)
|
|
449
|
+
if(!realPath)
|
|
450
|
+
throw Sass.new("No actual disk location detected.")
|
|
469
451
|
|
|
470
|
-
|
|
471
|
-
|
|
452
|
+
// On Windows, normalize the parent directory path to handle 8.3 short names
|
|
453
|
+
let pathToWrite = realPath
|
|
454
|
+
if(process.platform === "win32") {
|
|
455
|
+
try {
|
|
456
|
+
const parentPath = path.dirname(realPath)
|
|
457
|
+
const normalizedParent = await fs.realpath(parentPath)
|
|
458
|
+
pathToWrite = path.join(normalizedParent, path.basename(realPath))
|
|
459
|
+
} catch {
|
|
460
|
+
// If normalization fails, use original path
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
try {
|
|
465
|
+
await fs.writeFile(pathToWrite, content, encoding)
|
|
466
|
+
} catch(error) {
|
|
467
|
+
if(error.code === "ENOENT")
|
|
468
|
+
throw Sass.new(`Invalid directory: ${path.dirname(pathToWrite)}`)
|
|
472
469
|
|
|
473
|
-
|
|
474
|
-
|
|
470
|
+
throw Sass.from(error, "Failed to write file")
|
|
471
|
+
}
|
|
475
472
|
}
|
|
476
473
|
|
|
477
474
|
/**
|
|
@@ -491,7 +488,7 @@ export default class FileObject extends FS {
|
|
|
491
488
|
* await file.writeBinary(buffer)
|
|
492
489
|
*/
|
|
493
490
|
async writeBinary(data) {
|
|
494
|
-
if(!this
|
|
491
|
+
if(!this.url)
|
|
495
492
|
throw Sass.new("No URL in file")
|
|
496
493
|
|
|
497
494
|
const exists = await this.parent.exists
|
|
@@ -504,7 +501,7 @@ export default class FileObject extends FS {
|
|
|
504
501
|
|
|
505
502
|
// According to the internet, if it's already binary, I don't need
|
|
506
503
|
// an encoding. 🤷
|
|
507
|
-
return await fs.writeFile(this
|
|
504
|
+
return await fs.writeFile(this.url, bufferData)
|
|
508
505
|
}
|
|
509
506
|
|
|
510
507
|
/**
|
|
@@ -555,7 +552,7 @@ export default class FileObject extends FS {
|
|
|
555
552
|
* @returns {Promise<object>} The file contents as a module.
|
|
556
553
|
*/
|
|
557
554
|
async import() {
|
|
558
|
-
const url = this
|
|
555
|
+
const url = this.url
|
|
559
556
|
|
|
560
557
|
if(!url)
|
|
561
558
|
throw Sass.new("No URL in file map")
|
|
@@ -577,7 +574,7 @@ export default class FileObject extends FS {
|
|
|
577
574
|
* await file.delete()
|
|
578
575
|
*/
|
|
579
576
|
async delete() {
|
|
580
|
-
const url = this
|
|
577
|
+
const url = this.url
|
|
581
578
|
|
|
582
579
|
if(!url)
|
|
583
580
|
throw Sass.new("This object does not represent a valid resource.")
|