@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.
Files changed (41) hide show
  1. package/README.md +2 -5
  2. package/package.json +10 -4
  3. package/src/browser/lib/Data.js +16 -2
  4. package/src/browser/lib/OObject.js +196 -0
  5. package/src/browser/lib/Promised.js +4 -2
  6. package/src/browser/lib/Time.js +4 -2
  7. package/src/browser/lib/TypeSpec.js +24 -5
  8. package/src/node/index.js +0 -3
  9. package/src/node/lib/DirectoryObject.js +98 -200
  10. package/src/node/lib/FileObject.js +26 -44
  11. package/src/node/lib/FileSystem.js +9 -39
  12. package/src/node/lib/Glog.js +2 -12
  13. package/src/node/lib/Valid.js +8 -1
  14. package/types/browser/lib/Data.d.ts +26 -4
  15. package/types/browser/lib/Data.d.ts.map +1 -1
  16. package/types/browser/lib/OObject.d.ts +127 -0
  17. package/types/browser/lib/OObject.d.ts.map +1 -0
  18. package/types/browser/lib/Promised.d.ts.map +1 -1
  19. package/types/browser/lib/Time.d.ts.map +1 -1
  20. package/types/browser/lib/TypeSpec.d.ts +42 -6
  21. package/types/browser/lib/TypeSpec.d.ts.map +1 -1
  22. package/types/node/index.d.ts +0 -3
  23. package/types/node/lib/DirectoryObject.d.ts +93 -50
  24. package/types/node/lib/DirectoryObject.d.ts.map +1 -1
  25. package/types/node/lib/FileObject.d.ts +2 -10
  26. package/types/node/lib/FileObject.d.ts.map +1 -1
  27. package/types/node/lib/FileSystem.d.ts +13 -16
  28. package/types/node/lib/FileSystem.d.ts.map +1 -1
  29. package/types/node/lib/Glog.d.ts +0 -7
  30. package/types/node/lib/Glog.d.ts.map +1 -1
  31. package/types/node/lib/Valid.d.ts +17 -2
  32. package/types/node/lib/Valid.d.ts.map +1 -1
  33. package/src/node/lib/TempDirectoryObject.js +0 -165
  34. package/src/node/lib/VDirectoryObject.js +0 -198
  35. package/src/node/lib/VFileObject.js +0 -110
  36. package/types/node/lib/TempDirectoryObject.d.ts +0 -42
  37. package/types/node/lib/TempDirectoryObject.d.ts.map +0 -1
  38. package/types/node/lib/VDirectoryObject.d.ts +0 -132
  39. package/types/node/lib/VDirectoryObject.d.ts.map +0 -1
  40. package/types/node/lib/VFileObject.d.ts +0 -33
  41. 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
- * DirectoryObject encapsulates metadata and operations for a directory,
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 {object}
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: undefined,
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 = path.resolve(normalizedDir)
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.name = name
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(process.cwd())
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.isVirtual
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 the path or extension.
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. Will be an empty string if unavailable.
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.real?.path ?? this.path
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|VFileObject>, directories: Array<DirectoryObject|VDirectoryObject>}>} Object containing arrays of files and directories
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.isVirtual
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.isVirtual ? this.real?.path : this.path,
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|VFileObject>, directories: Array<DirectoryObject|VDirectoryObject>}>} Object containing arrays of matching files and directories
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.isVirtual ? this.real?.path : this.path,
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 = virtual
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 = virtual
385
- ? path.relative(this.real.path, resolved)
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.real?.path ?? this.path
419
+ const path = this.path
416
420
 
417
421
  try {
418
422
  await mkdir(path, options)
419
- } catch(e) {
420
- if(e.code === "EEXIST") {
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}': ${e.message}`)
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), this.cap)
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.real?.path ?? this.path
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.isVirtual
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.real?.path ?? this.path, dirname)
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 intelligent path merging that detects overlapping segments to avoid
592
- * duplication (e.g., "/projects/toolkit" + "toolkit/src" = "/projects/toolkit/src").
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} dir - The subdirectory path to append (can be nested like "src/lib")
596
- * @returns {DirectoryObject} A new DirectoryObject instance with the combined path
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.getDirectory("src/lib")
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(dir) {
610
- Valid.type(dir, "String", {allowEmpty: false})
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
- // Pass cap as parent so it resolves from cap root, not current directory
617
- return new this.constructor(normalized, this.cap)
544
+ // Reject absolute paths
545
+ if(path.isAbsolute(newPath)) {
546
+ throw Sass.new(`Absolute paths not allowed: ${newPath}`)
618
547
  }
619
548
 
620
- // Regular resolution
621
- const newPath = FS.resolvePath(this.path, dir)
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
- // VDO relative paths: only allow nested if explicitly prefixed with ./
632
- // This maintains security while allowing explicit relative navigation
633
- if(dir.includes(this.sep) && !dir.startsWith(`.${this.sep}`)) {
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
- // VDO relative paths: validate cap boundary and pass resolved path
638
- const normalized = this.#validateCapBoundary(newPath)
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(normalized, this)
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 intelligent path merging that detects overlapping segments to avoid
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
- * For regular DirectoryObject: only allows direct children (local only).
651
- * For VDirectoryObject: supports absolute virtual paths (starting with "/")
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.getFile("package.json")
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(file) {
673
- Valid.type(file, "String", {allowEmpty: false})
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
- return new VFileObject(normalized, this)
581
+ // Reject absolute paths
582
+ if(path.isAbsolute(filename)) {
583
+ throw Sass.new(`Absolute paths not allowed: ${filename}`)
680
584
  }
681
585
 
682
- // Regular resolution
683
- const resolvedPath = FS.resolvePath(this.path, file)
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
- // VDO relative paths: only allow nested if explicitly prefixed with ./
694
- // This maintains security while allowing explicit relative navigation
695
- if(file.includes(this.sep) && !file.startsWith(`.${this.sep}`)) {
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
- // VDO relative paths: validate cap boundary and pass resolved path
700
- const normalized = this.#validateCapBoundary(resolvedPath)
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 VFileObject(normalized, this)
600
+ return new FileObject(resolved)
703
601
  }
704
602
  }