@gesslar/toolkit 3.9.0 → 3.13.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.
@@ -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,36 +148,9 @@ 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}]`
157
- }
158
-
159
- /**
160
- * Returns a JSON representation of the DirectoryObject.
161
- *
162
- * @returns {object} JSON representation of the DirectoryObject
163
- */
164
- toJSON() {
165
- return {
166
- supplied: this.supplied,
167
- path: this.path,
168
- url: this.url.toString(),
169
- name: this.name,
170
- module: this.module,
171
- extension: this.extension,
172
- isFile: this.isFile,
173
- isDirectory: this.isDirectory,
174
- parent: this.parent ? this.parent.path : null,
175
- root: this.root.path
176
- }
177
- }
178
-
179
- /**
180
- * Custom inspect method for Node.js console.
181
- *
182
- * @returns {object} JSON representation of this object.
183
- */
184
- [util.inspect.custom]() {
185
- return this.toJSON()
151
+ return this.isCapped
152
+ ?`[${this.constructor.name}: ${this.path} → ${this.real.path}]`
153
+ :`[${this.constructor.name}: ${this.path}]`
186
154
  }
187
155
 
188
156
  /**
@@ -269,15 +237,6 @@ export default class DirectoryObject extends FS {
269
237
  return this.#meta.trail
270
238
  }
271
239
 
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
240
  /**
282
241
  * Returns the parent directory of this directory.
283
242
  * Returns null if this directory is the root directory.
@@ -293,84 +252,18 @@ export default class DirectoryObject extends FS {
293
252
  */
294
253
  get parent() {
295
254
  // Return cached value if available
296
- if(this.#parent !== undefined) {
255
+ if(this.#parent !== undefined)
297
256
  return this.#parent
298
- }
299
257
 
300
- // Compute parent directory (null if we're at root)
301
- const parentPath = path.dirname(this.path)
302
- const isRoot = parentPath === this.path
303
-
304
- // Cache and return
305
- this.#parent = isRoot
306
- ? null
307
- : new DirectoryObject(parentPath, this.temporary)
258
+ if(this.#meta.parentPath === null) {
259
+ this.#parent = null
308
260
 
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
261
+ return this.#parent
335
262
  }
336
263
 
337
- return current
338
- }
264
+ this.#parent = new this.constructor(this.#meta.parentPath)
339
265
 
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()
266
+ return this.#parent
374
267
  }
375
268
 
376
269
  /**
@@ -397,11 +290,15 @@ export default class DirectoryObject extends FS {
397
290
  * @returns {Promise<boolean>} Whether the directory exists
398
291
  */
399
292
  async #directoryExists() {
293
+ const path = this.isCapped
294
+ ? this.cap?.real.path
295
+ : this.path
296
+
400
297
  try {
401
- (await opendir(this.path)).close()
298
+ (await opendir(path)).close()
402
299
 
403
300
  return true
404
- } catch(_) {
301
+ } catch {
405
302
  return false
406
303
  }
407
304
  }
@@ -423,14 +320,27 @@ export default class DirectoryObject extends FS {
423
320
  * console.log(files) // Only .js files in ./src
424
321
  */
425
322
  async read(pat="") {
426
- const cwd = this.path, withFileTypes = true
323
+ const withFileTypes = true
324
+ const url = this.isCapped
325
+ ? this.real?.url
326
+ : this.url
327
+
328
+ Valid.type(url, "URL")
329
+ // const href = url.href
330
+
427
331
  const found = !pat
428
- ? await readdir(this.url, {withFileTypes})
332
+ ? await readdir(url, {withFileTypes})
429
333
  : await Array.fromAsync(
430
334
  glob(pat, {
431
- cwd,
335
+ cwd: this.isCapped ? this.real?.path : this.path,
432
336
  withFileTypes,
433
- exclude: candidate => candidate.parentPath !== cwd
337
+ // exclude: candidate => {
338
+ // // Only allow entries within this directory's URL
339
+ // const candidateHref = candidate.url.href
340
+
341
+ // // Must start with our URL + path separator, or be exactly our URL
342
+ // return !candidateHref.startsWith(href + "/") && candidateHref !== href
343
+ // }
434
344
  })
435
345
  )
436
346
 
@@ -441,9 +351,9 @@ export default class DirectoryObject extends FS {
441
351
  const directories = found
442
352
  .filter(dirent => dirent.isDirectory())
443
353
  .map(dirent => {
444
- const dirPath = path.join(this.path, dirent.name)
354
+ const dirPath = FS.resolvePath(this.path, dirent.name)
445
355
 
446
- return new DirectoryObject(dirPath, this.temporary)
356
+ return new this.constructor(dirPath, this)
447
357
  })
448
358
 
449
359
  return {files, directories}
@@ -486,31 +396,26 @@ export default class DirectoryObject extends FS {
486
396
  * @yields {DirectoryObject} Parent directory objects from current to root
487
397
  */
488
398
  *#walkUp() {
489
- if(!Array.isArray(this.trail))
490
- return
491
-
492
- const curr = structuredClone(this.trail)
399
+ const {root, base, dir} = FS.pathParts(this.path)
400
+ const sep = path.sep
401
+ // Remove the root and then re-add it every loop, because that's fun!
402
+ const choppedDir = Data.chopLeft(dir, root)
403
+ const trail = [...choppedDir.split(sep).filter(Boolean), base]
493
404
 
494
- while(curr.length > 0) {
495
- const joined = curr.join(this.sep)
405
+ if(trail.length === 0)
406
+ return yield this
496
407
 
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
- }
408
+ do
409
+ yield new this.constructor(path.join(root, ...trail), this.cap)
503
410
 
504
- yield new DirectoryObject(joined)
505
- curr.pop()
506
- }
411
+ while(trail.pop())
507
412
  }
508
413
 
509
414
  /**
510
415
  * Generator that walks up the directory tree, yielding each parent directory.
511
416
  * Starts from the current directory and yields each parent until reaching the root.
512
417
  *
513
- * @returns {object} Generator yielding parent DirectoryObject instances
418
+ * @returns {DirectoryObject} Generator yielding parent DirectoryObject instances
514
419
  * @example
515
420
  * const dir = new DirectoryObject('/path/to/deep/directory')
516
421
  * for(const parent of dir.walkUp) {
@@ -578,6 +483,14 @@ export default class DirectoryObject extends FS {
578
483
  return await directory.exists
579
484
  }
580
485
 
486
+ #isLocal = candidate => {
487
+ Valid.type(candidate, "String", {allowEmpty: false})
488
+
489
+ const {dir: candidateDir} = FS.pathParts(candidate)
490
+
491
+ return candidateDir === this.path
492
+ }
493
+
581
494
  /**
582
495
  * Creates a new DirectoryObject by extending this directory's path.
583
496
  *
@@ -585,7 +498,7 @@ export default class DirectoryObject extends FS {
585
498
  * duplication (e.g., "/projects/toolkit" + "toolkit/src" = "/projects/toolkit/src").
586
499
  * The temporary flag is preserved from the parent directory.
587
500
  *
588
- * @param {string} newPath - The subdirectory path to append (can be nested like "src/lib")
501
+ * @param {string} dir - The subdirectory path to append (can be nested like "src/lib")
589
502
  * @returns {DirectoryObject} A new DirectoryObject instance with the combined path
590
503
  * @throws {Sass} If newPath is not a string
591
504
  * @example
@@ -599,13 +512,14 @@ export default class DirectoryObject extends FS {
599
512
  * const subDir = dir.getDirectory("toolkit/src")
600
513
  * console.log(subDir.path) // "/projects/toolkit/src" (not /projects/toolkit/toolkit/src)
601
514
  */
602
- getDirectory(newPath) {
603
- Valid.type(newPath, "String")
515
+ getDirectory(dir) {
516
+ Valid.type(dir, "String", {allowEmpty: false})
517
+
518
+ const newPath = FS.resolvePath(this.path, dir)
604
519
 
605
- const thisPath = this.path
606
- const merged = FS.mergeOverlappingPaths(thisPath, newPath)
520
+ Valid.assert(this.#isLocal(newPath), `${newPath} would be out of bounds.`)
607
521
 
608
- return new this.constructor(merged, this.temporary)
522
+ return new this.constructor(newPath, this)
609
523
  }
610
524
 
611
525
  /**
@@ -615,7 +529,7 @@ export default class DirectoryObject extends FS {
615
529
  * duplication. The resulting FileObject can be used for reading, writing,
616
530
  * and other file operations.
617
531
  *
618
- * @param {string} filename - The filename to append (can include subdirectories like "src/index.js")
532
+ * @param {string} file - The filename to append (can include subdirectories like "src/index.js")
619
533
  * @returns {FileObject} A new FileObject instance with the combined path
620
534
  * @throws {Sass} If filename is not a string
621
535
  * @example
@@ -628,11 +542,13 @@ export default class DirectoryObject extends FS {
628
542
  * const file = dir.getFile("src/index.js")
629
543
  * const data = await file.read()
630
544
  */
631
- getFile(filename) {
632
- Valid.type(filename, "String")
545
+ getFile(file) {
546
+ Valid.type(file, "String", {allowEmpty: false})
547
+
548
+ const newPath = FS.resolvePath(this.path, file)
549
+
550
+ Valid.assert(this.#isLocal(newPath), `${newPath} would be out of bounds.`)
633
551
 
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)
552
+ return new FileObject(newPath, this)
637
553
  }
638
554
  }