@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.
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 +100 -212
  10. package/src/node/lib/FileObject.js +26 -44
  11. package/src/node/lib/FileSystem.js +3 -42
  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 -51
  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 +10 -19
  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,
@@ -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.name = name
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.isVirtual
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 the path or extension.
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. Will be an empty string if unavailable.
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.real?.path ?? this.path
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|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
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.isVirtual
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.isVirtual ? this.real?.path : this.path,
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|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
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.isVirtual ? this.real?.path : this.path,
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 = virtual
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 = virtual
386
- ? path.relative(this.real.path, resolved)
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.real?.path ?? this.path
419
+ const path = this.path
417
420
 
418
421
  try {
419
422
  await mkdir(path, options)
420
- } catch(e) {
421
- if(e.code === "EEXIST") {
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}': ${e.message}`)
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), this.cap)
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.real?.path ?? this.path
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.isVirtual
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.real?.path ?? this.path, dirname)
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
- * Resolves an absolute virtual path from cap root and validates it stays within cap boundary.
529
+ * Creates a new DirectoryObject by extending this directory's path.
537
530
  *
538
- * @private
539
- * @param {string} absolutePath - Absolute virtual path starting with separator
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
- * @private
569
- * @param {string} virtualPath - The virtual path
570
- * @returns {VDirectoryObject} The parent directory object
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
- #getParentDirectoryForPath(virtualPath) {
573
- const {dir} = FS.pathParts(virtualPath)
541
+ getDirectory(newPath) {
542
+ Valid.type(newPath, "String", {allowEmpty: false})
574
543
 
575
- // If at cap root, return cap
576
- if(dir === this.cap.path || dir === this.sep) {
577
- return this.cap
544
+ // Reject absolute paths
545
+ if(path.isAbsolute(newPath)) {
546
+ throw Sass.new(`Absolute paths not allowed: ${newPath}`)
578
547
  }
579
548
 
580
- // Create parent directory object (recursive - it will get its own parent)
581
- return new this.constructor(dir, this.cap)
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
- // Check if resolved path is within cap (handles case where path equals cap root)
603
- const capRealPath = this.cap.real.path
604
- const isWithinCap = resolvedRealPath === capRealPath
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
- if(!isWithinCap) {
608
- throw Sass.new(`${normalized} would be out of bounds (cap: ${this.cap.path}).`)
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 normalized
563
+ return new this.constructor(resolved)
612
564
  }
613
565
 
614
566
  /**
615
- * Creates a new DirectoryObject by extending this directory's path.
567
+ * Creates a new FileObject by extending this directory's path.
616
568
  *
617
- * Uses intelligent path merging that detects overlapping segments to avoid
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} dir - The subdirectory path to append (can be nested like "src/lib")
622
- * @returns {DirectoryObject} A new DirectoryObject instance with the combined path
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 subDir = dir.getDirectory("src/lib")
627
- * console.log(subDir.path) // "/projects/git/toolkit/src/lib"
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
- getDirectory(dir) {
636
- Valid.type(dir, "String", {allowEmpty: false})
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
- // Pass raw input; constructor handles resolution and parent determination
651
- return new this.constructor(dir, this)
581
+ // Reject absolute paths
582
+ if(path.isAbsolute(filename)) {
583
+ throw Sass.new(`Absolute paths not allowed: ${filename}`)
652
584
  }
653
585
 
654
- // Regular DO: validate local-only constraint
655
- const newPath = FS.resolvePath(this.path, dir)
656
- Valid.assert(this.#isLocal(newPath), `${newPath} would be out of bounds.`)
586
+ // Reject parent directory traversal
587
+ if(filename.includes("..")) {
588
+ throw Sass.new(`Path traversal not allowed: ${filename} contains '..'`)
589
+ }
657
590
 
658
- // Pass raw input; constructor will resolve it to cwd
659
- return new this.constructor(newPath)
660
- }
591
+ const thisPath = this.path
592
+ const merged = FS.mergeOverlappingPaths(thisPath, filename)
593
+ const resolved = FS.resolvePath(thisPath, merged)
661
594
 
662
- /**
663
- * Creates a new FileObject by extending this directory's path.
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
- // Pass raw input to constructor - it handles resolution and parent determination
710
- return this.isVirtual
711
- ? new VFileObject(file, this)
712
- : new FileObject(file, this)
600
+ return new FileObject(resolved)
713
601
  }
714
602
  }