@gesslar/toolkit 3.22.2 → 3.23.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/README.md +2 -5
- package/package.json +10 -4
- package/src/browser/lib/Data.js +16 -2
- package/src/browser/lib/OObject.js +196 -0
- package/src/browser/lib/Promised.js +4 -2
- package/src/browser/lib/Time.js +4 -2
- package/src/browser/lib/TypeSpec.js +24 -5
- package/src/node/index.js +0 -3
- package/src/node/lib/DirectoryObject.js +100 -212
- package/src/node/lib/FileObject.js +26 -44
- package/src/node/lib/FileSystem.js +3 -42
- package/src/node/lib/Glog.js +2 -12
- package/src/node/lib/Valid.js +8 -1
- package/types/browser/lib/Data.d.ts +26 -4
- package/types/browser/lib/Data.d.ts.map +1 -1
- package/types/browser/lib/OObject.d.ts +127 -0
- package/types/browser/lib/OObject.d.ts.map +1 -0
- package/types/browser/lib/Promised.d.ts.map +1 -1
- package/types/browser/lib/Time.d.ts.map +1 -1
- package/types/browser/lib/TypeSpec.d.ts +42 -6
- package/types/browser/lib/TypeSpec.d.ts.map +1 -1
- package/types/node/index.d.ts +0 -3
- package/types/node/lib/DirectoryObject.d.ts +93 -51
- package/types/node/lib/DirectoryObject.d.ts.map +1 -1
- package/types/node/lib/FileObject.d.ts +2 -10
- package/types/node/lib/FileObject.d.ts.map +1 -1
- package/types/node/lib/FileSystem.d.ts +10 -19
- package/types/node/lib/FileSystem.d.ts.map +1 -1
- package/types/node/lib/Glog.d.ts +0 -7
- package/types/node/lib/Glog.d.ts.map +1 -1
- package/types/node/lib/Valid.d.ts +17 -2
- package/types/node/lib/Valid.d.ts.map +1 -1
- package/src/node/lib/TempDirectoryObject.js +0 -165
- package/src/node/lib/VDirectoryObject.js +0 -198
- package/src/node/lib/VFileObject.js +0 -110
- package/types/node/lib/TempDirectoryObject.d.ts +0 -42
- package/types/node/lib/TempDirectoryObject.d.ts.map +0 -1
- package/types/node/lib/VDirectoryObject.d.ts +0 -132
- package/types/node/lib/VDirectoryObject.d.ts.map +0 -1
- package/types/node/lib/VFileObject.d.ts +0 -33
- package/types/node/lib/VFileObject.d.ts.map +0 -1
|
@@ -13,10 +13,30 @@ import FileObject from "./FileObject.js"
|
|
|
13
13
|
import FS from "./FileSystem.js"
|
|
14
14
|
import Sass from "./Sass.js"
|
|
15
15
|
import Valid from "./Valid.js"
|
|
16
|
-
import VFileObject from "./VFileObject.js"
|
|
17
16
|
|
|
18
17
|
/**
|
|
19
|
-
*
|
|
18
|
+
* @typedef {object} GeneratorType
|
|
19
|
+
* @property {function(): {value: DirectoryObject, done: boolean}} next
|
|
20
|
+
* @property {function(): GeneratorType} [Symbol.iterator]
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {object} DirectoryMeta
|
|
25
|
+
*
|
|
26
|
+
* @property {boolean} isDirectory - Always true for directories
|
|
27
|
+
* @property {string|null} extension - The directory extension (if any)
|
|
28
|
+
* @property {string|null} module - The directory name without extension
|
|
29
|
+
* @property {string|null} name - The directory name
|
|
30
|
+
* @property {DirectoryObject|undefined} parent - The parent DirectoryObject
|
|
31
|
+
* @property {string|null} parentPath - The parent directory path
|
|
32
|
+
* @property {string|null} path - The absolute directory path
|
|
33
|
+
* @property {string|null} sep - Path separator
|
|
34
|
+
* @property {string|null} supplied - User-supplied path
|
|
35
|
+
* @property {Array<string>|null} trail - Path segments
|
|
36
|
+
* @property {URL|null} url - The directory URL
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/** * DirectoryObject encapsulates metadata and operations for a directory,
|
|
20
40
|
* providing immutable path resolution, existence checks, and content enumeration.
|
|
21
41
|
*
|
|
22
42
|
* Features:
|
|
@@ -37,7 +57,6 @@ import VFileObject from "./VFileObject.js"
|
|
|
37
57
|
* @property {boolean} isDirectory - Always true
|
|
38
58
|
* @property {DirectoryObject|null} parent - The parent directory (null if root)
|
|
39
59
|
* @property {Promise<boolean>} exists - Whether the directory exists (async getter)
|
|
40
|
-
* @property {Generator<DirectoryObject>} walkUp - Generator yielding parent directories up to root
|
|
41
60
|
*
|
|
42
61
|
* @example
|
|
43
62
|
* // Basic usage
|
|
@@ -63,21 +82,15 @@ import VFileObject from "./VFileObject.js"
|
|
|
63
82
|
*/
|
|
64
83
|
export default class DirectoryObject extends FS {
|
|
65
84
|
/**
|
|
66
|
-
* @type {
|
|
67
|
-
* @private
|
|
68
|
-
* @property {string|null} supplied - User-supplied path
|
|
69
|
-
* @property {string|null} path - The absolute file path
|
|
70
|
-
* @property {URL|null} url - The file URL
|
|
71
|
-
* @property {string|null} name - The file name
|
|
72
|
-
* @property {string|null} module - The file name without extension
|
|
73
|
-
* @property {string|null} extension - The file extension
|
|
74
|
-
* @property {boolean} isDirectory - Always true
|
|
85
|
+
* @type {DirectoryMeta}
|
|
75
86
|
*/
|
|
76
87
|
#meta = Object.seal({
|
|
77
88
|
isDirectory: true,
|
|
89
|
+
extension: null,
|
|
90
|
+
module: null,
|
|
78
91
|
name: null,
|
|
79
92
|
parent: undefined,
|
|
80
|
-
parentPath:
|
|
93
|
+
parentPath: null,
|
|
81
94
|
path: null,
|
|
82
95
|
sep: null,
|
|
83
96
|
supplied: null,
|
|
@@ -93,7 +106,6 @@ export default class DirectoryObject extends FS {
|
|
|
93
106
|
* Constructs a DirectoryObject instance.
|
|
94
107
|
*
|
|
95
108
|
* @param {string?} [supplied="."] - The directory path (defaults to current directory)
|
|
96
|
-
* @param {DirectoryObject?} [parent] - Optional parent directory (ignored by DirectoryObject, used by subclasses)
|
|
97
109
|
*/
|
|
98
110
|
constructor(supplied) {
|
|
99
111
|
super()
|
|
@@ -104,17 +116,19 @@ export default class DirectoryObject extends FS {
|
|
|
104
116
|
|
|
105
117
|
const normalizedDir = FS.fixSlashes(fixedDir)
|
|
106
118
|
const resolved = FS.resolvePath(DirectoryObject.cwd, normalizedDir)
|
|
107
|
-
const {dir, name, root} = FS.pathParts(resolved)
|
|
119
|
+
const {dir, ext, name, root} = FS.pathParts(resolved)
|
|
108
120
|
const url = new URL(FS.pathToUrl(resolved))
|
|
109
121
|
const trail = resolved.split(path.sep)
|
|
110
122
|
|
|
111
|
-
this.#meta.
|
|
123
|
+
this.#meta.extension = ext
|
|
124
|
+
this.#meta.module = name
|
|
125
|
+
this.#meta.name = name + ext
|
|
112
126
|
this.#meta.parentPath = dir == root
|
|
113
127
|
? null
|
|
114
128
|
: dir
|
|
115
129
|
this.#meta.path = resolved
|
|
116
130
|
this.#meta.sep = path.sep
|
|
117
|
-
this.#meta.supplied = supplied
|
|
131
|
+
this.#meta.supplied = supplied ?? null
|
|
118
132
|
this.#meta.trail = trail
|
|
119
133
|
this.#meta.url = url
|
|
120
134
|
|
|
@@ -141,9 +155,7 @@ export default class DirectoryObject extends FS {
|
|
|
141
155
|
* @returns {string} string representation of the DirectoryObject
|
|
142
156
|
*/
|
|
143
157
|
toString() {
|
|
144
|
-
return this.
|
|
145
|
-
?`[${this.constructor.name}: ${this.path} → ${this.real.path}]`
|
|
146
|
-
:`[${this.constructor.name}: ${this.path}]`
|
|
158
|
+
return `[${this.constructor.name}: ${this.path}]`
|
|
147
159
|
}
|
|
148
160
|
|
|
149
161
|
/**
|
|
@@ -192,7 +204,7 @@ export default class DirectoryObject extends FS {
|
|
|
192
204
|
}
|
|
193
205
|
|
|
194
206
|
/**
|
|
195
|
-
* Returns the directory name without
|
|
207
|
+
* Returns the directory name without extension.
|
|
196
208
|
*
|
|
197
209
|
* @returns {string} The directory name without extension
|
|
198
210
|
*/
|
|
@@ -201,9 +213,9 @@ export default class DirectoryObject extends FS {
|
|
|
201
213
|
}
|
|
202
214
|
|
|
203
215
|
/**
|
|
204
|
-
* Returns the directory extension
|
|
216
|
+
* Returns the directory extension (if any).
|
|
205
217
|
*
|
|
206
|
-
* @returns {string} The directory extension
|
|
218
|
+
* @returns {string} The directory extension including the dot (e.g., '.git')
|
|
207
219
|
*/
|
|
208
220
|
get extension() {
|
|
209
221
|
return this.#meta.extension
|
|
@@ -274,7 +286,7 @@ export default class DirectoryObject extends FS {
|
|
|
274
286
|
* @returns {Promise<boolean>} Whether the directory exists
|
|
275
287
|
*/
|
|
276
288
|
async #directoryExists() {
|
|
277
|
-
const path = this.
|
|
289
|
+
const path = this.path
|
|
278
290
|
|
|
279
291
|
try {
|
|
280
292
|
(await opendir(path)).close()
|
|
@@ -289,11 +301,10 @@ export default class DirectoryObject extends FS {
|
|
|
289
301
|
* Lists the contents of a directory, optionally filtered by a glob pattern.
|
|
290
302
|
*
|
|
291
303
|
* Returns FileObject and DirectoryObject instances for regular directories.
|
|
292
|
-
* Returns VFileObject and VDirectoryObject instances when called on virtual directories.
|
|
293
304
|
*
|
|
294
305
|
* @async
|
|
295
306
|
* @param {string} [pat=""] - Optional glob pattern to filter results (e.g., "*.txt", "test-*")
|
|
296
|
-
* @returns {Promise<{files: Array<FileObject
|
|
307
|
+
* @returns {Promise<{files: Array<FileObject>, directories: Array<DirectoryObject>}>} Object containing arrays of files and directories
|
|
297
308
|
* @example
|
|
298
309
|
* const dir = new DirectoryObject("./src")
|
|
299
310
|
* const {files, directories} = await dir.read()
|
|
@@ -306,9 +317,7 @@ export default class DirectoryObject extends FS {
|
|
|
306
317
|
*/
|
|
307
318
|
async read(pat="") {
|
|
308
319
|
const withFileTypes = true
|
|
309
|
-
const url = this.
|
|
310
|
-
? this.real?.url
|
|
311
|
-
: this.url
|
|
320
|
+
const url = this.url
|
|
312
321
|
|
|
313
322
|
Valid.type(url, "URL")
|
|
314
323
|
// const href = url.href
|
|
@@ -317,7 +326,7 @@ export default class DirectoryObject extends FS {
|
|
|
317
326
|
? await readdir(url, {withFileTypes})
|
|
318
327
|
: await Array.fromAsync(
|
|
319
328
|
glob(pat, {
|
|
320
|
-
cwd: this.
|
|
329
|
+
cwd: this.path,
|
|
321
330
|
withFileTypes,
|
|
322
331
|
})
|
|
323
332
|
)
|
|
@@ -342,11 +351,10 @@ export default class DirectoryObject extends FS {
|
|
|
342
351
|
* Unlike read(), this method searches recursively through subdirectories.
|
|
343
352
|
*
|
|
344
353
|
* Returns FileObject and DirectoryObject instances for regular directories.
|
|
345
|
-
* Returns VFileObject and VDirectoryObject instances when called on virtual directories.
|
|
346
354
|
*
|
|
347
355
|
* @async
|
|
348
356
|
* @param {string} [pat=""] - Glob pattern to filter results
|
|
349
|
-
* @returns {Promise<{files: Array<FileObject|
|
|
357
|
+
* @returns {Promise<{files: Array<FileObject|FileObject>, directories: Array<DirectoryObject>}>} Object containing arrays of matching files and directories
|
|
350
358
|
* @throws {Sass} If an entry is neither a file nor directory
|
|
351
359
|
* @example
|
|
352
360
|
* const dir = new DirectoryObject("./src")
|
|
@@ -361,31 +369,26 @@ export default class DirectoryObject extends FS {
|
|
|
361
369
|
const withFileTypes = true
|
|
362
370
|
const found = await Array.fromAsync(
|
|
363
371
|
glob(pat, {
|
|
364
|
-
cwd: this.
|
|
372
|
+
cwd: this.path,
|
|
365
373
|
withFileTypes,
|
|
366
374
|
})
|
|
367
375
|
)
|
|
368
376
|
|
|
369
377
|
const files = [], directories = []
|
|
370
|
-
const virtual = this.isVirtual
|
|
371
378
|
|
|
372
379
|
for(const e of found) {
|
|
373
380
|
if(e.isFile()) {
|
|
374
381
|
const {name, parentPath} = e
|
|
375
382
|
const resolved = FS.resolvePath(parentPath, name)
|
|
376
383
|
|
|
377
|
-
const file =
|
|
378
|
-
? new VFileObject(path.relative(this.real.path, resolved), this)
|
|
379
|
-
: new FileObject(resolved, this)
|
|
384
|
+
const file = new FileObject(resolved, this)
|
|
380
385
|
|
|
381
386
|
files.push(file)
|
|
382
387
|
} else if(e.isDirectory()) {
|
|
383
388
|
const {name, parentPath} = e
|
|
384
389
|
const resolved = FS.resolvePath(parentPath, name)
|
|
385
|
-
const relativePath =
|
|
386
|
-
|
|
387
|
-
: resolved
|
|
388
|
-
const directory = new this.constructor(relativePath, this)
|
|
390
|
+
const relativePath = resolved
|
|
391
|
+
const directory = new this.constructor(relativePath)
|
|
389
392
|
|
|
390
393
|
directories.push(directory)
|
|
391
394
|
} else {
|
|
@@ -413,26 +416,26 @@ export default class DirectoryObject extends FS {
|
|
|
413
416
|
if(await this.exists)
|
|
414
417
|
return
|
|
415
418
|
|
|
416
|
-
const path = this.
|
|
419
|
+
const path = this.path
|
|
417
420
|
|
|
418
421
|
try {
|
|
419
422
|
await mkdir(path, options)
|
|
420
|
-
} catch(
|
|
421
|
-
if(
|
|
423
|
+
} catch(error) {
|
|
424
|
+
if(error.code === "EEXIST") {
|
|
422
425
|
// Directory already exists, ignore
|
|
423
426
|
return
|
|
424
427
|
}
|
|
425
428
|
|
|
426
|
-
throw Sass.new(`Unable to create directory '${path}': ${
|
|
429
|
+
throw Sass.new(`Unable to create directory '${path}': ${error.message}`)
|
|
427
430
|
}
|
|
428
431
|
}
|
|
429
432
|
|
|
430
433
|
/**
|
|
431
434
|
* Private generator that walks up the directory tree.
|
|
432
435
|
*
|
|
433
|
-
* @private
|
|
434
436
|
* @generator
|
|
435
437
|
* @yields {DirectoryObject} Parent directory objects from current to root
|
|
438
|
+
* @returns {GeneratorType}
|
|
436
439
|
*/
|
|
437
440
|
*#walkUp() {
|
|
438
441
|
const {root, base, dir} = FS.pathParts(this.path)
|
|
@@ -445,7 +448,7 @@ export default class DirectoryObject extends FS {
|
|
|
445
448
|
return yield this
|
|
446
449
|
|
|
447
450
|
do
|
|
448
|
-
yield new this.constructor(path.join(root, ...trail)
|
|
451
|
+
yield new this.constructor(path.join(root, ...trail))
|
|
449
452
|
|
|
450
453
|
while(trail.pop())
|
|
451
454
|
}
|
|
@@ -486,7 +489,7 @@ export default class DirectoryObject extends FS {
|
|
|
486
489
|
* await dir.delete() // Only works if directory is empty
|
|
487
490
|
*/
|
|
488
491
|
async delete() {
|
|
489
|
-
const dirPath = this.
|
|
492
|
+
const dirPath = this.path
|
|
490
493
|
|
|
491
494
|
if(!dirPath)
|
|
492
495
|
throw Sass.new("This object does not represent a valid resource.")
|
|
@@ -504,9 +507,7 @@ export default class DirectoryObject extends FS {
|
|
|
504
507
|
* @returns {Promise<boolean>} True if the file exists, false otherwise
|
|
505
508
|
*/
|
|
506
509
|
async hasFile(filename) {
|
|
507
|
-
const file = this
|
|
508
|
-
? new VFileObject(filename, this)
|
|
509
|
-
: new FileObject(filename, this)
|
|
510
|
+
const file = new FileObject(filename, this)
|
|
510
511
|
|
|
511
512
|
return await file.exists
|
|
512
513
|
}
|
|
@@ -518,197 +519,84 @@ export default class DirectoryObject extends FS {
|
|
|
518
519
|
* @returns {Promise<boolean>} True if the directory exists, false otherwise
|
|
519
520
|
*/
|
|
520
521
|
async hasDirectory(dirname) {
|
|
521
|
-
const dir = FS.resolvePath(this.
|
|
522
|
+
const dir = FS.resolvePath(this.path, dirname)
|
|
522
523
|
const directory = new DirectoryObject(dir)
|
|
523
524
|
|
|
524
525
|
return await directory.exists
|
|
525
526
|
}
|
|
526
527
|
|
|
527
|
-
#isLocal = candidate => {
|
|
528
|
-
Valid.type(candidate, "String", {allowEmpty: false})
|
|
529
|
-
|
|
530
|
-
const {dir: candidateDir} = FS.pathParts(candidate)
|
|
531
|
-
|
|
532
|
-
return candidateDir === this.path
|
|
533
|
-
}
|
|
534
|
-
|
|
535
528
|
/**
|
|
536
|
-
*
|
|
529
|
+
* Creates a new DirectoryObject by extending this directory's path.
|
|
537
530
|
*
|
|
538
|
-
*
|
|
539
|
-
*
|
|
540
|
-
* @returns {string} Normalized resolved virtual path
|
|
541
|
-
* @throws {Sass} If path would be out of bounds
|
|
542
|
-
*/
|
|
543
|
-
#resolveAndValidateFromCap(absolutePath) {
|
|
544
|
-
const relativeFromCap = Data.chopLeft(absolutePath, this.sep)
|
|
545
|
-
const resolvedVirtualPath = FS.resolvePath(this.cap.path, relativeFromCap)
|
|
546
|
-
const normalized = FS.fixSlashes(resolvedVirtualPath)
|
|
547
|
-
|
|
548
|
-
// Validate cap boundary using real paths
|
|
549
|
-
const relativeFromCapForReal = normalized.startsWith(this.sep)
|
|
550
|
-
? Data.chopLeft(normalized, this.sep)
|
|
551
|
-
: normalized
|
|
552
|
-
const resolvedRealPath = FS.resolvePath(
|
|
553
|
-
this.cap.real.path,
|
|
554
|
-
relativeFromCapForReal
|
|
555
|
-
)
|
|
556
|
-
|
|
557
|
-
if(!FS.pathContains(this.cap.real.path, resolvedRealPath)) {
|
|
558
|
-
throw Sass.new(`${normalized} would be out of bounds (cap: ${this.cap.path}).`)
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
return normalized
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
/**
|
|
565
|
-
* Gets the parent directory object for a given virtual path.
|
|
566
|
-
* Returns the cap if the path is at the cap root.
|
|
531
|
+
* Uses overlapping path segment detection to intelligently combine paths.
|
|
532
|
+
* Preserves the temporary flag from the current directory.
|
|
567
533
|
*
|
|
568
|
-
* @
|
|
569
|
-
* @
|
|
570
|
-
* @
|
|
534
|
+
* @param {string} newPath - The path to append to this directory's path.
|
|
535
|
+
* @returns {DirectoryObject} A new DirectoryObject with the extended path.
|
|
536
|
+
* @example
|
|
537
|
+
* const dir = new DirectoryObject("/projects/git/toolkit")
|
|
538
|
+
* const subDir = dir.addDirectory("src/lib")
|
|
539
|
+
* console.log(subDir.path) // "/projects/git/toolkit/src/lib"
|
|
571
540
|
*/
|
|
572
|
-
|
|
573
|
-
|
|
541
|
+
getDirectory(newPath) {
|
|
542
|
+
Valid.type(newPath, "String", {allowEmpty: false})
|
|
574
543
|
|
|
575
|
-
//
|
|
576
|
-
if(
|
|
577
|
-
|
|
544
|
+
// Reject absolute paths
|
|
545
|
+
if(path.isAbsolute(newPath)) {
|
|
546
|
+
throw Sass.new(`Absolute paths not allowed: ${newPath}`)
|
|
578
547
|
}
|
|
579
548
|
|
|
580
|
-
//
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
/**
|
|
585
|
-
* Validates that a resolved virtual path stays within the cap boundary.
|
|
586
|
-
*
|
|
587
|
-
* @private
|
|
588
|
-
* @param {string} virtualPath - Resolved virtual path
|
|
589
|
-
* @returns {string} Normalized virtual path
|
|
590
|
-
* @throws {Sass} If path would be out of bounds
|
|
591
|
-
*/
|
|
592
|
-
#validateCapBoundary(virtualPath) {
|
|
593
|
-
const normalized = FS.fixSlashes(virtualPath)
|
|
594
|
-
const relativeFromCap = normalized.startsWith(this.sep)
|
|
595
|
-
? Data.chopLeft(normalized, this.sep)
|
|
596
|
-
: normalized
|
|
597
|
-
const resolvedRealPath = FS.resolvePath(
|
|
598
|
-
this.cap.real.path,
|
|
599
|
-
relativeFromCap
|
|
600
|
-
)
|
|
549
|
+
// Reject parent directory traversal
|
|
550
|
+
if(newPath.includes("..")) {
|
|
551
|
+
throw Sass.new(`Path traversal not allowed: ${newPath} contains '..'`)
|
|
552
|
+
}
|
|
601
553
|
|
|
602
|
-
|
|
603
|
-
const
|
|
604
|
-
const
|
|
605
|
-
|| FS.pathContains(capRealPath, resolvedRealPath)
|
|
554
|
+
const thisPath = this.path
|
|
555
|
+
const merged = FS.mergeOverlappingPaths(thisPath, newPath)
|
|
556
|
+
const resolved = FS.resolvePath(thisPath, merged)
|
|
606
557
|
|
|
607
|
-
|
|
608
|
-
|
|
558
|
+
// Final safety check
|
|
559
|
+
if(!FS.pathContains(thisPath, resolved)) {
|
|
560
|
+
throw Sass.new(`Path resolves outside directory: ${newPath}`)
|
|
609
561
|
}
|
|
610
562
|
|
|
611
|
-
return
|
|
563
|
+
return new this.constructor(resolved)
|
|
612
564
|
}
|
|
613
565
|
|
|
614
566
|
/**
|
|
615
|
-
* Creates a new
|
|
567
|
+
* Creates a new FileObject by extending this directory's path.
|
|
616
568
|
*
|
|
617
|
-
* Uses
|
|
618
|
-
* duplication (e.g., "/projects/toolkit" + "toolkit/src" = "/projects/toolkit/src").
|
|
619
|
-
* The temporary flag is preserved from the parent directory.
|
|
569
|
+
* Uses overlapping path segment detection to intelligently combine paths.
|
|
620
570
|
*
|
|
621
|
-
* @param {string}
|
|
622
|
-
* @returns {
|
|
623
|
-
* @throws {Sass} If newPath is not a string
|
|
571
|
+
* @param {string} filename - The filename to append to this directory's path.
|
|
572
|
+
* @returns {FileObject} A new FileObject with the extended path.
|
|
624
573
|
* @example
|
|
625
574
|
* const dir = new DirectoryObject("/projects/git/toolkit")
|
|
626
|
-
* const
|
|
627
|
-
* console.log(
|
|
628
|
-
*
|
|
629
|
-
* @example
|
|
630
|
-
* // Handles overlapping segments intelligently
|
|
631
|
-
* const dir = new DirectoryObject("/projects/toolkit")
|
|
632
|
-
* const subDir = dir.getDirectory("toolkit/src")
|
|
633
|
-
* console.log(subDir.path) // "/projects/toolkit/src" (not /projects/toolkit/toolkit/src)
|
|
575
|
+
* const file = dir.addFile("package.json")
|
|
576
|
+
* console.log(file.path) // "/projects/git/toolkit/package.json"
|
|
634
577
|
*/
|
|
635
|
-
|
|
636
|
-
Valid.type(
|
|
637
|
-
|
|
638
|
-
// Validate boundaries before passing raw input to constructor
|
|
639
|
-
if(this.isVirtual) {
|
|
640
|
-
// VDO: validate cap boundary, then pass raw input to constructor
|
|
641
|
-
if(dir.startsWith(this.sep)) {
|
|
642
|
-
// Absolute path: validate from cap root
|
|
643
|
-
this.#resolveAndValidateFromCap(dir)
|
|
644
|
-
} else {
|
|
645
|
-
// Relative path: resolve and validate stays within cap
|
|
646
|
-
const newPath = FS.resolvePath(this.path, dir)
|
|
647
|
-
this.#validateCapBoundary(newPath)
|
|
648
|
-
}
|
|
578
|
+
getFile(filename) {
|
|
579
|
+
Valid.type(filename, "String", {allowEmpty: false})
|
|
649
580
|
|
|
650
|
-
|
|
651
|
-
|
|
581
|
+
// Reject absolute paths
|
|
582
|
+
if(path.isAbsolute(filename)) {
|
|
583
|
+
throw Sass.new(`Absolute paths not allowed: ${filename}`)
|
|
652
584
|
}
|
|
653
585
|
|
|
654
|
-
//
|
|
655
|
-
|
|
656
|
-
|
|
586
|
+
// Reject parent directory traversal
|
|
587
|
+
if(filename.includes("..")) {
|
|
588
|
+
throw Sass.new(`Path traversal not allowed: ${filename} contains '..'`)
|
|
589
|
+
}
|
|
657
590
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
591
|
+
const thisPath = this.path
|
|
592
|
+
const merged = FS.mergeOverlappingPaths(thisPath, filename)
|
|
593
|
+
const resolved = FS.resolvePath(thisPath, merged)
|
|
661
594
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
* Uses intelligent path merging that detects overlapping segments to avoid
|
|
666
|
-
* duplication. The resulting FileObject can be used for reading, writing,
|
|
667
|
-
* and other file operations.
|
|
668
|
-
*
|
|
669
|
-
* For regular DirectoryObject: only allows direct children (local only).
|
|
670
|
-
* For VDirectoryObject: supports absolute virtual paths (starting with "/")
|
|
671
|
-
* which resolve from cap root, and relative paths from current directory.
|
|
672
|
-
*
|
|
673
|
-
* When called on a VDirectoryObject, returns a VFileObject to maintain
|
|
674
|
-
* virtual path semantics.
|
|
675
|
-
*
|
|
676
|
-
* @param {string} file - The filename to append
|
|
677
|
-
* @returns {FileObject|VFileObject} A new FileObject (or VFileObject if virtual)
|
|
678
|
-
* @throws {Sass} If filename is not a string
|
|
679
|
-
* @throws {Sass} If path would be out of bounds
|
|
680
|
-
* @example
|
|
681
|
-
* const dir = new DirectoryObject("/projects/git/toolkit")
|
|
682
|
-
* const file = dir.getFile("package.json")
|
|
683
|
-
* console.log(file.path) // "/projects/git/toolkit/package.json"
|
|
684
|
-
*
|
|
685
|
-
* @example
|
|
686
|
-
* // VDirectoryObject with absolute virtual path
|
|
687
|
-
* const vdo = new TempDirectoryObject("myapp")
|
|
688
|
-
* const file = vdo.getFile("/config/settings.json")
|
|
689
|
-
* // Virtual path: /config/settings.json, Real path: {vdo.real.path}/config/settings.json
|
|
690
|
-
*/
|
|
691
|
-
getFile(file) {
|
|
692
|
-
Valid.type(file, "String", {allowEmpty: false})
|
|
693
|
-
|
|
694
|
-
// Validate boundaries - check what the resolved path would be
|
|
695
|
-
if(!this.isVirtual) {
|
|
696
|
-
// Regular DO: validate local-only constraint
|
|
697
|
-
const resolvedPath = FS.resolvePath(this.path, file)
|
|
698
|
-
Valid.assert(this.#isLocal(resolvedPath), `${resolvedPath} would be out of bounds.`)
|
|
699
|
-
} else {
|
|
700
|
-
// VDO: validate cap boundary
|
|
701
|
-
if(file.startsWith(this.sep)) {
|
|
702
|
-
this.#resolveAndValidateFromCap(file)
|
|
703
|
-
} else {
|
|
704
|
-
const resolvedPath = FS.resolvePath(this.path, file)
|
|
705
|
-
this.#validateCapBoundary(resolvedPath)
|
|
706
|
-
}
|
|
595
|
+
// Final safety check
|
|
596
|
+
if(!FS.pathContains(thisPath, resolved)) {
|
|
597
|
+
throw Sass.new(`Path resolves outside directory: ${filename}`)
|
|
707
598
|
}
|
|
708
599
|
|
|
709
|
-
|
|
710
|
-
return this.isVirtual
|
|
711
|
-
? new VFileObject(file, this)
|
|
712
|
-
: new FileObject(file, this)
|
|
600
|
+
return new FileObject(resolved)
|
|
713
601
|
}
|
|
714
602
|
}
|