@gesslar/toolkit 3.9.0 → 3.12.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,12 +7,12 @@
7
7
  import {glob, mkdir, opendir, readdir, rmdir} from "node:fs/promises"
8
8
  import path from "node:path"
9
9
  import {URL} from "node:url"
10
- import util from "node:util"
11
10
 
12
- import {Data, Valid} from "../browser/index.js"
11
+ import Data from "../browser/lib/Data.js"
13
12
  import FS from "./FS.js"
14
13
  import FileObject from "./FileObject.js"
15
14
  import Sass from "./Sass.js"
15
+ import Valid from "./Valid.js"
16
16
 
17
17
  /**
18
18
  * DirectoryObject encapsulates metadata and operations for a directory,
@@ -24,7 +24,6 @@ import Sass from "./Sass.js"
24
24
  * - Pattern-based content filtering with glob support
25
25
  * - Path traversal via walkUp generator
26
26
  * - Intelligent path merging for subdirectories and files
27
- * - Support for temporary directory management
28
27
  *
29
28
  * @property {string} supplied - The original directory path as supplied to constructor
30
29
  * @property {string} path - The absolute resolved directory path
@@ -34,7 +33,6 @@ import Sass from "./Sass.js"
34
33
  * @property {string} extension - The directory extension (typically empty string)
35
34
  * @property {string} sep - Platform-specific path separator ('/' or '\\')
36
35
  * @property {Array<string>} trail - Path segments split by separator
37
- * @property {boolean} temporary - Whether this is marked as a temporary directory
38
36
  * @property {boolean} isFile - Always false (this is a directory)
39
37
  * @property {boolean} isDirectory - Always true
40
38
  * @property {DirectoryObject|null} parent - The parent directory (null if root)
@@ -87,48 +85,45 @@ export default class DirectoryObject extends FS {
87
85
  isDirectory: true,
88
86
  trail: null,
89
87
  sep: null,
90
- temporary: null,
88
+ parent: undefined,
89
+ parentPath: undefined,
91
90
  })
92
91
 
93
- /**
94
- * Cached parent directory object
95
- *
96
- * @type {DirectoryObject|null|undefined}
97
- * @private
98
- */
92
+ // Not in the meta, because it gets frozen, and these are lazily
93
+ // set.
99
94
  #parent = undefined
100
95
 
101
96
  /**
102
97
  * Constructs a DirectoryObject instance.
103
98
  *
104
- * @param {string? | DirectoryObject?} [directory="."] - The directory path or DirectoryObject (defaults to current directory)
105
- * @param {boolean} [temporary] - Whether this is a temporary directory.
99
+ * @param {string?} [directory="."] - The directory path or DirectoryObject (defaults to current directory)
106
100
  */
107
- constructor(directory=".", temporary=false) {
108
- super()
101
+ constructor(directory) {
102
+ directory ||= "."
109
103
 
110
- Valid.type(directory, "String|TempDirectoryObject|DirectoryObject")
104
+ Valid.type(directory, "String")
111
105
 
112
- // If passed a DirectoryObject, extract its path
113
- if(Data.isType(directory, "DirectoryObject") || Data.isType(directory, "TempDirectoryObject"))
114
- directory = directory.path
106
+ super()
115
107
 
116
108
  const fixedDir = FS.fixSlashes(directory)
117
109
  const resolved = path.resolve(fixedDir)
118
- const url = new URL(FS.pathToUri(resolved))
119
- const baseName = path.basename(resolved) || "."
110
+ const url = new URL(FS.pathToUrl(resolved))
111
+ const baseName = path.basename(resolved) || ""
120
112
  const trail = resolved.split(path.sep)
121
113
  const sep = path.sep
114
+ const pathParts = FS.pathParts(resolved)
122
115
 
123
116
  this.#meta.supplied = fixedDir
124
117
  this.#meta.path = resolved
125
118
  this.#meta.url = url
126
119
  this.#meta.name = baseName
127
120
  this.#meta.extension = ""
128
- this.#meta.module = baseName
121
+ this.#meta.module = baseName !== "." ? baseName : ""
129
122
  this.#meta.trail = trail
130
123
  this.#meta.sep = sep
131
- this.#meta.temporary = temporary
124
+ this.#meta.parentPath = pathParts.dir === pathParts.root
125
+ ? null
126
+ : pathParts.dir
132
127
 
133
128
  Object.freeze(this.#meta)
134
129
  }
@@ -153,7 +148,13 @@ export default class DirectoryObject extends FS {
153
148
  * @returns {string} string representation of the DirectoryObject
154
149
  */
155
150
  toString() {
156
- return `[DirectoryObject: ${this.path}]`
151
+ return this.isCapped
152
+ ?`[${this.constructor.name}: ${this.path} → ${this.real.path}]`
153
+ :`[${this.constructor.name}: ${this.path}]`
154
+
155
+ return this.isCapped
156
+ ?`[${this.constructor.name}: ${this.path} → ${this.real.path}]`
157
+ :`[${this.constructor.name}: ${this.path}]`
157
158
  }
158
159
 
159
160
  /**
@@ -171,8 +172,7 @@ export default class DirectoryObject extends FS {
171
172
  extension: this.extension,
172
173
  isFile: this.isFile,
173
174
  isDirectory: this.isDirectory,
174
- parent: this.parent ? this.parent.path : null,
175
- root: this.root.path
175
+ parent: this.parent?.path ?? null,
176
176
  }
177
177
  }
178
178
 
@@ -181,9 +181,9 @@ export default class DirectoryObject extends FS {
181
181
  *
182
182
  * @returns {object} JSON representation of this object.
183
183
  */
184
- [util.inspect.custom]() {
185
- return this.toJSON()
186
- }
184
+ // [util.inspect.custom]() {
185
+ // return this.toJSON()
186
+ // }
187
187
 
188
188
  /**
189
189
  * Checks if the directory exists (async).
@@ -269,15 +269,6 @@ export default class DirectoryObject extends FS {
269
269
  return this.#meta.trail
270
270
  }
271
271
 
272
- /**
273
- * Returns whether this directory is marked as temporary.
274
- *
275
- * @returns {boolean} True if this is a temporary directory, false otherwise
276
- */
277
- get temporary() {
278
- return this.#meta.temporary
279
- }
280
-
281
272
  /**
282
273
  * Returns the parent directory of this directory.
283
274
  * Returns null if this directory is the root directory.
@@ -293,84 +284,18 @@ export default class DirectoryObject extends FS {
293
284
  */
294
285
  get parent() {
295
286
  // Return cached value if available
296
- if(this.#parent !== undefined) {
287
+ if(this.#parent !== undefined)
297
288
  return this.#parent
298
- }
299
289
 
300
- // Compute parent directory (null if we're at root)
301
- const parentPath = path.dirname(this.path)
302
- const isRoot = parentPath === this.path
290
+ if(this.#meta.parentPath === null) {
291
+ this.#parent = null
303
292
 
304
- // Cache and return
305
- this.#parent = isRoot
306
- ? null
307
- : new DirectoryObject(parentPath, this.temporary)
308
-
309
- return this.#parent
310
- }
311
-
312
- /**
313
- * Returns the root directory of the filesystem.
314
- *
315
- * For DirectoryObject, this walks up to the filesystem root.
316
- * For CappedDirectoryObject, this returns the cap root.
317
- *
318
- * @returns {DirectoryObject} The root directory
319
- * @example
320
- * const dir = new DirectoryObject("/usr/local/bin")
321
- * console.log(dir.root.path) // "/"
322
- *
323
- * @example
324
- * const capped = new CappedDirectoryObject("/projects/myapp")
325
- * const sub = capped.getDirectory("src/lib")
326
- * console.log(sub.root.path) // "/" (virtual, cap root)
327
- * console.log(sub.root.real.path) // "/projects/myapp"
328
- */
329
- get root() {
330
- // Walk up until we find a directory with no parent
331
- let current = this
332
-
333
- while(current.parent !== null) {
334
- current = current.parent
293
+ return this.#parent
335
294
  }
336
295
 
337
- return current
338
- }
296
+ this.#parent = new this.constructor(this.#meta.parentPath)
339
297
 
340
- /**
341
- * Recursively removes a temporary directory and all its contents.
342
- *
343
- * This method will delete all files and subdirectories within this directory,
344
- * then delete the directory itself. It only works on directories explicitly
345
- * marked as temporary for safety.
346
- *
347
- * @async
348
- * @returns {Promise<void>}
349
- * @throws {Sass} If the directory is not marked as temporary
350
- * @throws {Sass} If the directory deletion fails
351
- * @example
352
- * const tempDir = new TempDirectoryObject("my-temp")
353
- * await tempDir.assureExists()
354
- * // ... use the directory ...
355
- * await tempDir.remove() // Recursively deletes everything
356
- */
357
- async remove() {
358
- if(!this.temporary)
359
- throw Sass.new("This is not a temporary directory.")
360
-
361
- /** @type {{files: Array<FileObject>, directories: Array<DirectoryObject>}} */
362
- const {files, directories} = await this.read()
363
-
364
- // Remove subdirectories recursively
365
- for(const dir of directories)
366
- await dir.remove()
367
-
368
- // Remove files
369
- for(const file of files)
370
- await file.delete()
371
-
372
- // Delete the now-empty directory
373
- await this.delete()
298
+ return this.#parent
374
299
  }
375
300
 
376
301
  /**
@@ -397,11 +322,15 @@ export default class DirectoryObject extends FS {
397
322
  * @returns {Promise<boolean>} Whether the directory exists
398
323
  */
399
324
  async #directoryExists() {
325
+ const path = this.isCapped
326
+ ? this.cap?.real.path
327
+ : this.path
328
+
400
329
  try {
401
- (await opendir(this.path)).close()
330
+ (await opendir(path)).close()
402
331
 
403
332
  return true
404
- } catch(_) {
333
+ } catch {
405
334
  return false
406
335
  }
407
336
  }
@@ -423,14 +352,27 @@ export default class DirectoryObject extends FS {
423
352
  * console.log(files) // Only .js files in ./src
424
353
  */
425
354
  async read(pat="") {
426
- const cwd = this.path, withFileTypes = true
355
+ const withFileTypes = true
356
+ const url = this.isCapped
357
+ ? this.real?.url
358
+ : this.url
359
+
360
+ Valid.type(url, "URL")
361
+ // const href = url.href
362
+
427
363
  const found = !pat
428
- ? await readdir(this.url, {withFileTypes})
364
+ ? await readdir(url, {withFileTypes})
429
365
  : await Array.fromAsync(
430
366
  glob(pat, {
431
- cwd,
367
+ cwd: this.isCapped ? this.real?.path : this.path,
432
368
  withFileTypes,
433
- exclude: candidate => candidate.parentPath !== cwd
369
+ // exclude: candidate => {
370
+ // // Only allow entries within this directory's URL
371
+ // const candidateHref = candidate.url.href
372
+
373
+ // // Must start with our URL + path separator, or be exactly our URL
374
+ // return !candidateHref.startsWith(href + "/") && candidateHref !== href
375
+ // }
434
376
  })
435
377
  )
436
378
 
@@ -441,9 +383,9 @@ export default class DirectoryObject extends FS {
441
383
  const directories = found
442
384
  .filter(dirent => dirent.isDirectory())
443
385
  .map(dirent => {
444
- const dirPath = path.join(this.path, dirent.name)
386
+ const dirPath = FS.resolvePath(this.path, dirent.name)
445
387
 
446
- return new DirectoryObject(dirPath, this.temporary)
388
+ return new this.constructor(dirPath, this)
447
389
  })
448
390
 
449
391
  return {files, directories}
@@ -486,31 +428,26 @@ export default class DirectoryObject extends FS {
486
428
  * @yields {DirectoryObject} Parent directory objects from current to root
487
429
  */
488
430
  *#walkUp() {
489
- if(!Array.isArray(this.trail))
490
- return
491
-
492
- const curr = structuredClone(this.trail)
431
+ const {root, base, dir} = FS.pathParts(this.path)
432
+ const sep = path.sep
433
+ // Remove the root and then re-add it every loop, because that's fun!
434
+ const choppedDir = Data.chopLeft(dir, root)
435
+ const trail = [...choppedDir.split(sep).filter(Boolean), base]
493
436
 
494
- while(curr.length > 0) {
495
- const joined = curr.join(this.sep)
437
+ if(trail.length === 0)
438
+ return yield this
496
439
 
497
- // Stop if we've reached an empty path (which would resolve to CWD)
498
- if(joined === "" || joined === this.sep) {
499
- // Yield the root and stop
500
- yield new DirectoryObject(this.sep)
501
- break
502
- }
440
+ do
441
+ yield new this.constructor(path.join(root, ...trail), this.cap)
503
442
 
504
- yield new DirectoryObject(joined)
505
- curr.pop()
506
- }
443
+ while(trail.pop())
507
444
  }
508
445
 
509
446
  /**
510
447
  * Generator that walks up the directory tree, yielding each parent directory.
511
448
  * Starts from the current directory and yields each parent until reaching the root.
512
449
  *
513
- * @returns {object} Generator yielding parent DirectoryObject instances
450
+ * @returns {DirectoryObject} Generator yielding parent DirectoryObject instances
514
451
  * @example
515
452
  * const dir = new DirectoryObject('/path/to/deep/directory')
516
453
  * for(const parent of dir.walkUp) {
@@ -578,6 +515,14 @@ export default class DirectoryObject extends FS {
578
515
  return await directory.exists
579
516
  }
580
517
 
518
+ #isLocal = candidate => {
519
+ Valid.type(candidate, "String", {allowEmpty: false})
520
+
521
+ const {dir: candidateDir} = FS.pathParts(candidate)
522
+
523
+ return candidateDir === this.path
524
+ }
525
+
581
526
  /**
582
527
  * Creates a new DirectoryObject by extending this directory's path.
583
528
  *
@@ -585,7 +530,7 @@ export default class DirectoryObject extends FS {
585
530
  * duplication (e.g., "/projects/toolkit" + "toolkit/src" = "/projects/toolkit/src").
586
531
  * The temporary flag is preserved from the parent directory.
587
532
  *
588
- * @param {string} newPath - The subdirectory path to append (can be nested like "src/lib")
533
+ * @param {string} dir - The subdirectory path to append (can be nested like "src/lib")
589
534
  * @returns {DirectoryObject} A new DirectoryObject instance with the combined path
590
535
  * @throws {Sass} If newPath is not a string
591
536
  * @example
@@ -599,13 +544,14 @@ export default class DirectoryObject extends FS {
599
544
  * const subDir = dir.getDirectory("toolkit/src")
600
545
  * console.log(subDir.path) // "/projects/toolkit/src" (not /projects/toolkit/toolkit/src)
601
546
  */
602
- getDirectory(newPath) {
603
- Valid.type(newPath, "String")
547
+ getDirectory(dir) {
548
+ Valid.type(dir, "String", {allowEmpty: false})
604
549
 
605
- const thisPath = this.path
606
- const merged = FS.mergeOverlappingPaths(thisPath, newPath)
550
+ const newPath = FS.resolvePath(this.path, dir)
607
551
 
608
- return new this.constructor(merged, this.temporary)
552
+ Valid.assert(this.#isLocal(newPath), `${newPath} would be out of bounds.`)
553
+
554
+ return new this.constructor(newPath, this)
609
555
  }
610
556
 
611
557
  /**
@@ -615,7 +561,7 @@ export default class DirectoryObject extends FS {
615
561
  * duplication. The resulting FileObject can be used for reading, writing,
616
562
  * and other file operations.
617
563
  *
618
- * @param {string} filename - The filename to append (can include subdirectories like "src/index.js")
564
+ * @param {string} file - The filename to append (can include subdirectories like "src/index.js")
619
565
  * @returns {FileObject} A new FileObject instance with the combined path
620
566
  * @throws {Sass} If filename is not a string
621
567
  * @example
@@ -628,11 +574,13 @@ export default class DirectoryObject extends FS {
628
574
  * const file = dir.getFile("src/index.js")
629
575
  * const data = await file.read()
630
576
  */
631
- getFile(filename) {
632
- Valid.type(filename, "String")
577
+ getFile(file) {
578
+ Valid.type(file, "String", {allowEmpty: false})
579
+
580
+ const newPath = FS.resolvePath(this.path, file)
581
+
582
+ Valid.assert(this.#isLocal(newPath), `${newPath} would be out of bounds.`)
633
583
 
634
- // Pass the filename and this directory as parent
635
- // This ensures the FileObject maintains the correct parent reference
636
- return new FileObject(filename, this)
584
+ return new FileObject(newPath, this)
637
585
  }
638
586
  }