@gesslar/toolkit 3.22.1 → 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 +98 -200
- package/src/node/lib/FileObject.js +26 -44
- package/src/node/lib/FileSystem.js +9 -39
- 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 -50
- 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 +13 -16
- 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,
|
|
@@ -95,25 +108,27 @@ export default class DirectoryObject extends FS {
|
|
|
95
108
|
* @param {string?} [supplied="."] - The directory path (defaults to current directory)
|
|
96
109
|
*/
|
|
97
110
|
constructor(supplied) {
|
|
111
|
+
super()
|
|
112
|
+
|
|
98
113
|
const fixedDir = supplied || "."
|
|
99
114
|
|
|
100
115
|
Valid.type(fixedDir, "String")
|
|
101
116
|
|
|
102
|
-
super()
|
|
103
|
-
|
|
104
117
|
const normalizedDir = FS.fixSlashes(fixedDir)
|
|
105
|
-
const resolved =
|
|
106
|
-
const {dir, name, root} = FS.pathParts(resolved)
|
|
118
|
+
const resolved = FS.resolvePath(DirectoryObject.cwd, normalizedDir)
|
|
119
|
+
const {dir, ext, name, root} = FS.pathParts(resolved)
|
|
107
120
|
const url = new URL(FS.pathToUrl(resolved))
|
|
108
121
|
const trail = resolved.split(path.sep)
|
|
109
122
|
|
|
110
|
-
this.#meta.
|
|
123
|
+
this.#meta.extension = ext
|
|
124
|
+
this.#meta.module = name
|
|
125
|
+
this.#meta.name = name + ext
|
|
111
126
|
this.#meta.parentPath = dir == root
|
|
112
127
|
? null
|
|
113
128
|
: dir
|
|
114
129
|
this.#meta.path = resolved
|
|
115
130
|
this.#meta.sep = path.sep
|
|
116
|
-
this.#meta.supplied = supplied
|
|
131
|
+
this.#meta.supplied = supplied ?? null
|
|
117
132
|
this.#meta.trail = trail
|
|
118
133
|
this.#meta.url = url
|
|
119
134
|
|
|
@@ -131,7 +146,7 @@ export default class DirectoryObject extends FS {
|
|
|
131
146
|
* console.log(projectRoot.path) // process.cwd()
|
|
132
147
|
*/
|
|
133
148
|
static fromCwd() {
|
|
134
|
-
return new this(
|
|
149
|
+
return new this(FS.cwd)
|
|
135
150
|
}
|
|
136
151
|
|
|
137
152
|
/**
|
|
@@ -140,9 +155,7 @@ export default class DirectoryObject extends FS {
|
|
|
140
155
|
* @returns {string} string representation of the DirectoryObject
|
|
141
156
|
*/
|
|
142
157
|
toString() {
|
|
143
|
-
return this.
|
|
144
|
-
?`[${this.constructor.name}: ${this.path} → ${this.real.path}]`
|
|
145
|
-
:`[${this.constructor.name}: ${this.path}]`
|
|
158
|
+
return `[${this.constructor.name}: ${this.path}]`
|
|
146
159
|
}
|
|
147
160
|
|
|
148
161
|
/**
|
|
@@ -191,7 +204,7 @@ export default class DirectoryObject extends FS {
|
|
|
191
204
|
}
|
|
192
205
|
|
|
193
206
|
/**
|
|
194
|
-
* Returns the directory name without
|
|
207
|
+
* Returns the directory name without extension.
|
|
195
208
|
*
|
|
196
209
|
* @returns {string} The directory name without extension
|
|
197
210
|
*/
|
|
@@ -200,9 +213,9 @@ export default class DirectoryObject extends FS {
|
|
|
200
213
|
}
|
|
201
214
|
|
|
202
215
|
/**
|
|
203
|
-
* Returns the directory extension
|
|
216
|
+
* Returns the directory extension (if any).
|
|
204
217
|
*
|
|
205
|
-
* @returns {string} The directory extension
|
|
218
|
+
* @returns {string} The directory extension including the dot (e.g., '.git')
|
|
206
219
|
*/
|
|
207
220
|
get extension() {
|
|
208
221
|
return this.#meta.extension
|
|
@@ -273,7 +286,7 @@ export default class DirectoryObject extends FS {
|
|
|
273
286
|
* @returns {Promise<boolean>} Whether the directory exists
|
|
274
287
|
*/
|
|
275
288
|
async #directoryExists() {
|
|
276
|
-
const path = this.
|
|
289
|
+
const path = this.path
|
|
277
290
|
|
|
278
291
|
try {
|
|
279
292
|
(await opendir(path)).close()
|
|
@@ -288,11 +301,10 @@ export default class DirectoryObject extends FS {
|
|
|
288
301
|
* Lists the contents of a directory, optionally filtered by a glob pattern.
|
|
289
302
|
*
|
|
290
303
|
* Returns FileObject and DirectoryObject instances for regular directories.
|
|
291
|
-
* Returns VFileObject and VDirectoryObject instances when called on virtual directories.
|
|
292
304
|
*
|
|
293
305
|
* @async
|
|
294
306
|
* @param {string} [pat=""] - Optional glob pattern to filter results (e.g., "*.txt", "test-*")
|
|
295
|
-
* @returns {Promise<{files: Array<FileObject
|
|
307
|
+
* @returns {Promise<{files: Array<FileObject>, directories: Array<DirectoryObject>}>} Object containing arrays of files and directories
|
|
296
308
|
* @example
|
|
297
309
|
* const dir = new DirectoryObject("./src")
|
|
298
310
|
* const {files, directories} = await dir.read()
|
|
@@ -305,9 +317,7 @@ export default class DirectoryObject extends FS {
|
|
|
305
317
|
*/
|
|
306
318
|
async read(pat="") {
|
|
307
319
|
const withFileTypes = true
|
|
308
|
-
const url = this.
|
|
309
|
-
? this.real?.url
|
|
310
|
-
: this.url
|
|
320
|
+
const url = this.url
|
|
311
321
|
|
|
312
322
|
Valid.type(url, "URL")
|
|
313
323
|
// const href = url.href
|
|
@@ -316,7 +326,7 @@ export default class DirectoryObject extends FS {
|
|
|
316
326
|
? await readdir(url, {withFileTypes})
|
|
317
327
|
: await Array.fromAsync(
|
|
318
328
|
glob(pat, {
|
|
319
|
-
cwd: this.
|
|
329
|
+
cwd: this.path,
|
|
320
330
|
withFileTypes,
|
|
321
331
|
})
|
|
322
332
|
)
|
|
@@ -341,11 +351,10 @@ export default class DirectoryObject extends FS {
|
|
|
341
351
|
* Unlike read(), this method searches recursively through subdirectories.
|
|
342
352
|
*
|
|
343
353
|
* Returns FileObject and DirectoryObject instances for regular directories.
|
|
344
|
-
* Returns VFileObject and VDirectoryObject instances when called on virtual directories.
|
|
345
354
|
*
|
|
346
355
|
* @async
|
|
347
356
|
* @param {string} [pat=""] - Glob pattern to filter results
|
|
348
|
-
* @returns {Promise<{files: Array<FileObject|
|
|
357
|
+
* @returns {Promise<{files: Array<FileObject|FileObject>, directories: Array<DirectoryObject>}>} Object containing arrays of matching files and directories
|
|
349
358
|
* @throws {Sass} If an entry is neither a file nor directory
|
|
350
359
|
* @example
|
|
351
360
|
* const dir = new DirectoryObject("./src")
|
|
@@ -360,31 +369,26 @@ export default class DirectoryObject extends FS {
|
|
|
360
369
|
const withFileTypes = true
|
|
361
370
|
const found = await Array.fromAsync(
|
|
362
371
|
glob(pat, {
|
|
363
|
-
cwd: this.
|
|
372
|
+
cwd: this.path,
|
|
364
373
|
withFileTypes,
|
|
365
374
|
})
|
|
366
375
|
)
|
|
367
376
|
|
|
368
377
|
const files = [], directories = []
|
|
369
|
-
const virtual = this.isVirtual
|
|
370
378
|
|
|
371
379
|
for(const e of found) {
|
|
372
380
|
if(e.isFile()) {
|
|
373
381
|
const {name, parentPath} = e
|
|
374
382
|
const resolved = FS.resolvePath(parentPath, name)
|
|
375
383
|
|
|
376
|
-
const file =
|
|
377
|
-
? new VFileObject(path.relative(this.real.path, resolved), this)
|
|
378
|
-
: new FileObject(resolved, this)
|
|
384
|
+
const file = new FileObject(resolved, this)
|
|
379
385
|
|
|
380
386
|
files.push(file)
|
|
381
387
|
} else if(e.isDirectory()) {
|
|
382
388
|
const {name, parentPath} = e
|
|
383
389
|
const resolved = FS.resolvePath(parentPath, name)
|
|
384
|
-
const relativePath =
|
|
385
|
-
|
|
386
|
-
: resolved
|
|
387
|
-
const directory = new this.constructor(relativePath, this)
|
|
390
|
+
const relativePath = resolved
|
|
391
|
+
const directory = new this.constructor(relativePath)
|
|
388
392
|
|
|
389
393
|
directories.push(directory)
|
|
390
394
|
} else {
|
|
@@ -412,26 +416,26 @@ export default class DirectoryObject extends FS {
|
|
|
412
416
|
if(await this.exists)
|
|
413
417
|
return
|
|
414
418
|
|
|
415
|
-
const path = this.
|
|
419
|
+
const path = this.path
|
|
416
420
|
|
|
417
421
|
try {
|
|
418
422
|
await mkdir(path, options)
|
|
419
|
-
} catch(
|
|
420
|
-
if(
|
|
423
|
+
} catch(error) {
|
|
424
|
+
if(error.code === "EEXIST") {
|
|
421
425
|
// Directory already exists, ignore
|
|
422
426
|
return
|
|
423
427
|
}
|
|
424
428
|
|
|
425
|
-
throw Sass.new(`Unable to create directory '${path}': ${
|
|
429
|
+
throw Sass.new(`Unable to create directory '${path}': ${error.message}`)
|
|
426
430
|
}
|
|
427
431
|
}
|
|
428
432
|
|
|
429
433
|
/**
|
|
430
434
|
* Private generator that walks up the directory tree.
|
|
431
435
|
*
|
|
432
|
-
* @private
|
|
433
436
|
* @generator
|
|
434
437
|
* @yields {DirectoryObject} Parent directory objects from current to root
|
|
438
|
+
* @returns {GeneratorType}
|
|
435
439
|
*/
|
|
436
440
|
*#walkUp() {
|
|
437
441
|
const {root, base, dir} = FS.pathParts(this.path)
|
|
@@ -444,7 +448,7 @@ export default class DirectoryObject extends FS {
|
|
|
444
448
|
return yield this
|
|
445
449
|
|
|
446
450
|
do
|
|
447
|
-
yield new this.constructor(path.join(root, ...trail)
|
|
451
|
+
yield new this.constructor(path.join(root, ...trail))
|
|
448
452
|
|
|
449
453
|
while(trail.pop())
|
|
450
454
|
}
|
|
@@ -485,7 +489,7 @@ export default class DirectoryObject extends FS {
|
|
|
485
489
|
* await dir.delete() // Only works if directory is empty
|
|
486
490
|
*/
|
|
487
491
|
async delete() {
|
|
488
|
-
const dirPath = this.
|
|
492
|
+
const dirPath = this.path
|
|
489
493
|
|
|
490
494
|
if(!dirPath)
|
|
491
495
|
throw Sass.new("This object does not represent a valid resource.")
|
|
@@ -503,9 +507,7 @@ export default class DirectoryObject extends FS {
|
|
|
503
507
|
* @returns {Promise<boolean>} True if the file exists, false otherwise
|
|
504
508
|
*/
|
|
505
509
|
async hasFile(filename) {
|
|
506
|
-
const file = this
|
|
507
|
-
? new VFileObject(filename, this)
|
|
508
|
-
: new FileObject(filename, this)
|
|
510
|
+
const file = new FileObject(filename, this)
|
|
509
511
|
|
|
510
512
|
return await file.exists
|
|
511
513
|
}
|
|
@@ -517,188 +519,84 @@ export default class DirectoryObject extends FS {
|
|
|
517
519
|
* @returns {Promise<boolean>} True if the directory exists, false otherwise
|
|
518
520
|
*/
|
|
519
521
|
async hasDirectory(dirname) {
|
|
520
|
-
const dir = FS.resolvePath(this.
|
|
522
|
+
const dir = FS.resolvePath(this.path, dirname)
|
|
521
523
|
const directory = new DirectoryObject(dir)
|
|
522
524
|
|
|
523
525
|
return await directory.exists
|
|
524
526
|
}
|
|
525
527
|
|
|
526
|
-
#isLocal = candidate => {
|
|
527
|
-
Valid.type(candidate, "String", {allowEmpty: false})
|
|
528
|
-
|
|
529
|
-
const {dir: candidateDir} = FS.pathParts(candidate)
|
|
530
|
-
|
|
531
|
-
return candidateDir === this.path
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
/**
|
|
535
|
-
* Resolves an absolute virtual path from cap root and validates it stays within cap boundary.
|
|
536
|
-
*
|
|
537
|
-
* @private
|
|
538
|
-
* @param {string} absolutePath - Absolute virtual path starting with separator
|
|
539
|
-
* @returns {string} Normalized resolved virtual path
|
|
540
|
-
* @throws {Sass} If path would be out of bounds
|
|
541
|
-
*/
|
|
542
|
-
#resolveAndValidateFromCap(absolutePath) {
|
|
543
|
-
const relativeFromCap = Data.chopLeft(absolutePath, this.sep)
|
|
544
|
-
const resolvedVirtualPath = FS.resolvePath(this.cap.path, relativeFromCap)
|
|
545
|
-
const normalized = FS.fixSlashes(resolvedVirtualPath)
|
|
546
|
-
|
|
547
|
-
// Validate cap boundary using real paths
|
|
548
|
-
const relativeFromCapForReal = normalized.startsWith(this.sep)
|
|
549
|
-
? Data.chopLeft(normalized, this.sep)
|
|
550
|
-
: normalized
|
|
551
|
-
const resolvedRealPath = FS.resolvePath(
|
|
552
|
-
this.cap.real.path,
|
|
553
|
-
relativeFromCapForReal
|
|
554
|
-
)
|
|
555
|
-
|
|
556
|
-
if(!FS.pathContains(this.cap.real.path, resolvedRealPath)) {
|
|
557
|
-
throw Sass.new(`${normalized} would be out of bounds (cap: ${this.cap.path}).`)
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
return normalized
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
/**
|
|
564
|
-
* Validates that a resolved virtual path stays within the cap boundary.
|
|
565
|
-
*
|
|
566
|
-
* @private
|
|
567
|
-
* @param {string} virtualPath - Resolved virtual path
|
|
568
|
-
* @returns {string} Normalized virtual path
|
|
569
|
-
* @throws {Sass} If path would be out of bounds
|
|
570
|
-
*/
|
|
571
|
-
#validateCapBoundary(virtualPath) {
|
|
572
|
-
const normalized = FS.fixSlashes(virtualPath)
|
|
573
|
-
const relativeFromCap = normalized.startsWith(this.sep)
|
|
574
|
-
? Data.chopLeft(normalized, this.sep)
|
|
575
|
-
: normalized
|
|
576
|
-
const resolvedRealPath = FS.resolvePath(
|
|
577
|
-
this.cap.real.path,
|
|
578
|
-
relativeFromCap
|
|
579
|
-
)
|
|
580
|
-
|
|
581
|
-
if(!FS.pathContains(this.cap.real.path, resolvedRealPath)) {
|
|
582
|
-
throw Sass.new(`${normalized} would be out of bounds (cap: ${this.cap.path}).`)
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
return normalized
|
|
586
|
-
}
|
|
587
|
-
|
|
588
528
|
/**
|
|
589
529
|
* Creates a new DirectoryObject by extending this directory's path.
|
|
590
530
|
*
|
|
591
|
-
* Uses
|
|
592
|
-
*
|
|
593
|
-
* The temporary flag is preserved from the parent directory.
|
|
531
|
+
* Uses overlapping path segment detection to intelligently combine paths.
|
|
532
|
+
* Preserves the temporary flag from the current directory.
|
|
594
533
|
*
|
|
595
|
-
* @param {string}
|
|
596
|
-
* @returns {DirectoryObject} A new DirectoryObject
|
|
597
|
-
* @throws {Sass} If newPath is not a string
|
|
534
|
+
* @param {string} newPath - The path to append to this directory's path.
|
|
535
|
+
* @returns {DirectoryObject} A new DirectoryObject with the extended path.
|
|
598
536
|
* @example
|
|
599
537
|
* const dir = new DirectoryObject("/projects/git/toolkit")
|
|
600
|
-
* const subDir = dir.
|
|
538
|
+
* const subDir = dir.addDirectory("src/lib")
|
|
601
539
|
* console.log(subDir.path) // "/projects/git/toolkit/src/lib"
|
|
602
|
-
*
|
|
603
|
-
* @example
|
|
604
|
-
* // Handles overlapping segments intelligently
|
|
605
|
-
* const dir = new DirectoryObject("/projects/toolkit")
|
|
606
|
-
* const subDir = dir.getDirectory("toolkit/src")
|
|
607
|
-
* console.log(subDir.path) // "/projects/toolkit/src" (not /projects/toolkit/toolkit/src)
|
|
608
540
|
*/
|
|
609
|
-
getDirectory(
|
|
610
|
-
Valid.type(
|
|
611
|
-
|
|
612
|
-
// Handle VDirectoryObject with absolute virtual paths (starting with "/")
|
|
613
|
-
if(this.isVirtual && dir.startsWith(this.sep)) {
|
|
614
|
-
const normalized = this.#resolveAndValidateFromCap(dir)
|
|
541
|
+
getDirectory(newPath) {
|
|
542
|
+
Valid.type(newPath, "String", {allowEmpty: false})
|
|
615
543
|
|
|
616
|
-
|
|
617
|
-
|
|
544
|
+
// Reject absolute paths
|
|
545
|
+
if(path.isAbsolute(newPath)) {
|
|
546
|
+
throw Sass.new(`Absolute paths not allowed: ${newPath}`)
|
|
618
547
|
}
|
|
619
548
|
|
|
620
|
-
//
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
// Validate bounds
|
|
624
|
-
if(!this.isVirtual) {
|
|
625
|
-
// Regular DO: enforce local-only constraint
|
|
626
|
-
Valid.assert(this.#isLocal(newPath), `${newPath} would be out of bounds.`)
|
|
627
|
-
|
|
628
|
-
return new this.constructor(newPath, this)
|
|
549
|
+
// Reject parent directory traversal
|
|
550
|
+
if(newPath.includes("..")) {
|
|
551
|
+
throw Sass.new(`Path traversal not allowed: ${newPath} contains '..'`)
|
|
629
552
|
}
|
|
630
553
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
throw Sass.new(`${dir} would be out of bounds. Use "./${dir}" for nested paths or chain getDirectory() calls.`)
|
|
635
|
-
}
|
|
554
|
+
const thisPath = this.path
|
|
555
|
+
const merged = FS.mergeOverlappingPaths(thisPath, newPath)
|
|
556
|
+
const resolved = FS.resolvePath(thisPath, merged)
|
|
636
557
|
|
|
637
|
-
//
|
|
638
|
-
|
|
558
|
+
// Final safety check
|
|
559
|
+
if(!FS.pathContains(thisPath, resolved)) {
|
|
560
|
+
throw Sass.new(`Path resolves outside directory: ${newPath}`)
|
|
561
|
+
}
|
|
639
562
|
|
|
640
|
-
return new this.constructor(
|
|
563
|
+
return new this.constructor(resolved)
|
|
641
564
|
}
|
|
642
565
|
|
|
643
566
|
/**
|
|
644
567
|
* Creates a new FileObject by extending this directory's path.
|
|
645
568
|
*
|
|
646
|
-
* Uses
|
|
647
|
-
* duplication. The resulting FileObject can be used for reading, writing,
|
|
648
|
-
* and other file operations.
|
|
569
|
+
* Uses overlapping path segment detection to intelligently combine paths.
|
|
649
570
|
*
|
|
650
|
-
*
|
|
651
|
-
*
|
|
652
|
-
* which resolve from cap root, and relative paths from current directory.
|
|
653
|
-
*
|
|
654
|
-
* When called on a VDirectoryObject, returns a VFileObject to maintain
|
|
655
|
-
* virtual path semantics.
|
|
656
|
-
*
|
|
657
|
-
* @param {string} file - The filename to append
|
|
658
|
-
* @returns {FileObject|VFileObject} A new FileObject (or VFileObject if virtual)
|
|
659
|
-
* @throws {Sass} If filename is not a string
|
|
660
|
-
* @throws {Sass} If path would be out of bounds
|
|
571
|
+
* @param {string} filename - The filename to append to this directory's path.
|
|
572
|
+
* @returns {FileObject} A new FileObject with the extended path.
|
|
661
573
|
* @example
|
|
662
574
|
* const dir = new DirectoryObject("/projects/git/toolkit")
|
|
663
|
-
* const file = dir.
|
|
575
|
+
* const file = dir.addFile("package.json")
|
|
664
576
|
* console.log(file.path) // "/projects/git/toolkit/package.json"
|
|
665
|
-
*
|
|
666
|
-
* @example
|
|
667
|
-
* // VDirectoryObject with absolute virtual path
|
|
668
|
-
* const vdo = new TempDirectoryObject("myapp")
|
|
669
|
-
* const file = vdo.getFile("/config/settings.json")
|
|
670
|
-
* // Virtual path: /config/settings.json, Real path: {vdo.real.path}/config/settings.json
|
|
671
577
|
*/
|
|
672
|
-
getFile(
|
|
673
|
-
Valid.type(
|
|
674
|
-
|
|
675
|
-
// Handle VDirectoryObject with absolute virtual paths (starting with "/")
|
|
676
|
-
if(this.isVirtual && file.startsWith(this.sep)) {
|
|
677
|
-
const normalized = this.#resolveAndValidateFromCap(file)
|
|
578
|
+
getFile(filename) {
|
|
579
|
+
Valid.type(filename, "String", {allowEmpty: false})
|
|
678
580
|
|
|
679
|
-
|
|
581
|
+
// Reject absolute paths
|
|
582
|
+
if(path.isAbsolute(filename)) {
|
|
583
|
+
throw Sass.new(`Absolute paths not allowed: ${filename}`)
|
|
680
584
|
}
|
|
681
585
|
|
|
682
|
-
//
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
// Validate bounds
|
|
686
|
-
if(!this.isVirtual) {
|
|
687
|
-
// Regular DO: enforce local-only constraint
|
|
688
|
-
Valid.assert(this.#isLocal(resolvedPath), `${resolvedPath} would be out of bounds.`)
|
|
689
|
-
|
|
690
|
-
return new FileObject(file, this)
|
|
586
|
+
// Reject parent directory traversal
|
|
587
|
+
if(filename.includes("..")) {
|
|
588
|
+
throw Sass.new(`Path traversal not allowed: ${filename} contains '..'`)
|
|
691
589
|
}
|
|
692
590
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
throw Sass.new(`${file} would be out of bounds. Use "./${file}" for nested paths or chain getFile() calls.`)
|
|
697
|
-
}
|
|
591
|
+
const thisPath = this.path
|
|
592
|
+
const merged = FS.mergeOverlappingPaths(thisPath, filename)
|
|
593
|
+
const resolved = FS.resolvePath(thisPath, merged)
|
|
698
594
|
|
|
699
|
-
//
|
|
700
|
-
|
|
595
|
+
// Final safety check
|
|
596
|
+
if(!FS.pathContains(thisPath, resolved)) {
|
|
597
|
+
throw Sass.new(`Path resolves outside directory: ${filename}`)
|
|
598
|
+
}
|
|
701
599
|
|
|
702
|
-
return new
|
|
600
|
+
return new FileObject(resolved)
|
|
703
601
|
}
|
|
704
602
|
}
|