@gesslar/toolkit 3.8.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,59 +85,76 @@ 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
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=null, temporary=false) {
108
- super()
101
+ constructor(directory) {
102
+ directory ||= "."
109
103
 
110
- Valid.type(directory, "String|TempDirectoryObject|DirectoryObject|Null")
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
- const fixedDir = FS.fixSlashes(directory ?? ".")
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
  }
135
130
 
131
+ /**
132
+ * Creates a DirectoryObject from the current working directory.
133
+ * Useful when working with pnpx or other tools where the project root
134
+ * needs to be determined at runtime.
135
+ *
136
+ * @returns {DirectoryObject} A DirectoryObject representing the current working directory
137
+ * @example
138
+ * const projectRoot = DirectoryObject.fromCwd()
139
+ * console.log(projectRoot.path) // process.cwd()
140
+ */
141
+ static fromCwd() {
142
+ return new this(process.cwd())
143
+ }
144
+
136
145
  /**
137
146
  * Returns a string representation of the DirectoryObject.
138
147
  *
139
148
  * @returns {string} string representation of the DirectoryObject
140
149
  */
141
150
  toString() {
142
- 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}]`
143
158
  }
144
159
 
145
160
  /**
@@ -157,8 +172,7 @@ export default class DirectoryObject extends FS {
157
172
  extension: this.extension,
158
173
  isFile: this.isFile,
159
174
  isDirectory: this.isDirectory,
160
- parent: this.parent ? this.parent.path : null,
161
- root: this.root.path
175
+ parent: this.parent?.path ?? null,
162
176
  }
163
177
  }
164
178
 
@@ -167,9 +181,9 @@ export default class DirectoryObject extends FS {
167
181
  *
168
182
  * @returns {object} JSON representation of this object.
169
183
  */
170
- [util.inspect.custom]() {
171
- return this.toJSON()
172
- }
184
+ // [util.inspect.custom]() {
185
+ // return this.toJSON()
186
+ // }
173
187
 
174
188
  /**
175
189
  * Checks if the directory exists (async).
@@ -255,15 +269,6 @@ export default class DirectoryObject extends FS {
255
269
  return this.#meta.trail
256
270
  }
257
271
 
258
- /**
259
- * Returns whether this directory is marked as temporary.
260
- *
261
- * @returns {boolean} True if this is a temporary directory, false otherwise
262
- */
263
- get temporary() {
264
- return this.#meta.temporary
265
- }
266
-
267
272
  /**
268
273
  * Returns the parent directory of this directory.
269
274
  * Returns null if this directory is the root directory.
@@ -279,84 +284,18 @@ export default class DirectoryObject extends FS {
279
284
  */
280
285
  get parent() {
281
286
  // Return cached value if available
282
- if(this.#parent !== undefined) {
287
+ if(this.#parent !== undefined)
283
288
  return this.#parent
284
- }
285
-
286
- // Compute parent directory (null if we're at root)
287
- const parentPath = path.dirname(this.path)
288
- const isRoot = parentPath === this.path
289
-
290
- // Cache and return
291
- this.#parent = isRoot
292
- ? null
293
- : new DirectoryObject(parentPath, this.temporary)
294
289
 
295
- return this.#parent
296
- }
297
-
298
- /**
299
- * Returns the root directory of the filesystem.
300
- *
301
- * For DirectoryObject, this walks up to the filesystem root.
302
- * For CappedDirectoryObject, this returns the cap root.
303
- *
304
- * @returns {DirectoryObject} The root directory
305
- * @example
306
- * const dir = new DirectoryObject("/usr/local/bin")
307
- * console.log(dir.root.path) // "/"
308
- *
309
- * @example
310
- * const capped = new CappedDirectoryObject("/projects/myapp")
311
- * const sub = capped.getDirectory("src/lib")
312
- * console.log(sub.root.path) // "/" (virtual, cap root)
313
- * console.log(sub.root.real.path) // "/projects/myapp"
314
- */
315
- get root() {
316
- // Walk up until we find a directory with no parent
317
- let current = this
290
+ if(this.#meta.parentPath === null) {
291
+ this.#parent = null
318
292
 
319
- while(current.parent !== null) {
320
- current = current.parent
293
+ return this.#parent
321
294
  }
322
295
 
323
- return current
324
- }
325
-
326
- /**
327
- * Recursively removes a temporary directory and all its contents.
328
- *
329
- * This method will delete all files and subdirectories within this directory,
330
- * then delete the directory itself. It only works on directories explicitly
331
- * marked as temporary for safety.
332
- *
333
- * @async
334
- * @returns {Promise<void>}
335
- * @throws {Sass} If the directory is not marked as temporary
336
- * @throws {Sass} If the directory deletion fails
337
- * @example
338
- * const tempDir = new TempDirectoryObject("my-temp")
339
- * await tempDir.assureExists()
340
- * // ... use the directory ...
341
- * await tempDir.remove() // Recursively deletes everything
342
- */
343
- async remove() {
344
- if(!this.temporary)
345
- throw Sass.new("This is not a temporary directory.")
346
-
347
- /** @type {{files: Array<FileObject>, directories: Array<DirectoryObject>}} */
348
- const {files, directories} = await this.read()
349
-
350
- // Remove subdirectories recursively
351
- for(const dir of directories)
352
- await dir.remove()
296
+ this.#parent = new this.constructor(this.#meta.parentPath)
353
297
 
354
- // Remove files
355
- for(const file of files)
356
- await file.delete()
357
-
358
- // Delete the now-empty directory
359
- await this.delete()
298
+ return this.#parent
360
299
  }
361
300
 
362
301
  /**
@@ -383,11 +322,15 @@ export default class DirectoryObject extends FS {
383
322
  * @returns {Promise<boolean>} Whether the directory exists
384
323
  */
385
324
  async #directoryExists() {
325
+ const path = this.isCapped
326
+ ? this.cap?.real.path
327
+ : this.path
328
+
386
329
  try {
387
- (await opendir(this.path)).close()
330
+ (await opendir(path)).close()
388
331
 
389
332
  return true
390
- } catch(_) {
333
+ } catch {
391
334
  return false
392
335
  }
393
336
  }
@@ -409,14 +352,27 @@ export default class DirectoryObject extends FS {
409
352
  * console.log(files) // Only .js files in ./src
410
353
  */
411
354
  async read(pat="") {
412
- 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
+
413
363
  const found = !pat
414
- ? await readdir(this.url, {withFileTypes})
364
+ ? await readdir(url, {withFileTypes})
415
365
  : await Array.fromAsync(
416
366
  glob(pat, {
417
- cwd,
367
+ cwd: this.isCapped ? this.real?.path : this.path,
418
368
  withFileTypes,
419
- 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
+ // }
420
376
  })
421
377
  )
422
378
 
@@ -427,9 +383,9 @@ export default class DirectoryObject extends FS {
427
383
  const directories = found
428
384
  .filter(dirent => dirent.isDirectory())
429
385
  .map(dirent => {
430
- const dirPath = path.join(this.path, dirent.name)
386
+ const dirPath = FS.resolvePath(this.path, dirent.name)
431
387
 
432
- return new DirectoryObject(dirPath, this.temporary)
388
+ return new this.constructor(dirPath, this)
433
389
  })
434
390
 
435
391
  return {files, directories}
@@ -472,31 +428,26 @@ export default class DirectoryObject extends FS {
472
428
  * @yields {DirectoryObject} Parent directory objects from current to root
473
429
  */
474
430
  *#walkUp() {
475
- if(!Array.isArray(this.trail))
476
- return
477
-
478
- 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]
479
436
 
480
- while(curr.length > 0) {
481
- const joined = curr.join(this.sep)
437
+ if(trail.length === 0)
438
+ return yield this
482
439
 
483
- // Stop if we've reached an empty path (which would resolve to CWD)
484
- if(joined === "" || joined === this.sep) {
485
- // Yield the root and stop
486
- yield new DirectoryObject(this.sep)
487
- break
488
- }
440
+ do
441
+ yield new this.constructor(path.join(root, ...trail), this.cap)
489
442
 
490
- yield new DirectoryObject(joined)
491
- curr.pop()
492
- }
443
+ while(trail.pop())
493
444
  }
494
445
 
495
446
  /**
496
447
  * Generator that walks up the directory tree, yielding each parent directory.
497
448
  * Starts from the current directory and yields each parent until reaching the root.
498
449
  *
499
- * @returns {object} Generator yielding parent DirectoryObject instances
450
+ * @returns {DirectoryObject} Generator yielding parent DirectoryObject instances
500
451
  * @example
501
452
  * const dir = new DirectoryObject('/path/to/deep/directory')
502
453
  * for(const parent of dir.walkUp) {
@@ -564,6 +515,14 @@ export default class DirectoryObject extends FS {
564
515
  return await directory.exists
565
516
  }
566
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
+
567
526
  /**
568
527
  * Creates a new DirectoryObject by extending this directory's path.
569
528
  *
@@ -571,7 +530,7 @@ export default class DirectoryObject extends FS {
571
530
  * duplication (e.g., "/projects/toolkit" + "toolkit/src" = "/projects/toolkit/src").
572
531
  * The temporary flag is preserved from the parent directory.
573
532
  *
574
- * @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")
575
534
  * @returns {DirectoryObject} A new DirectoryObject instance with the combined path
576
535
  * @throws {Sass} If newPath is not a string
577
536
  * @example
@@ -585,13 +544,14 @@ export default class DirectoryObject extends FS {
585
544
  * const subDir = dir.getDirectory("toolkit/src")
586
545
  * console.log(subDir.path) // "/projects/toolkit/src" (not /projects/toolkit/toolkit/src)
587
546
  */
588
- getDirectory(newPath) {
589
- Valid.type(newPath, "String")
547
+ getDirectory(dir) {
548
+ Valid.type(dir, "String", {allowEmpty: false})
549
+
550
+ const newPath = FS.resolvePath(this.path, dir)
590
551
 
591
- const thisPath = this.path
592
- const merged = FS.mergeOverlappingPaths(thisPath, newPath)
552
+ Valid.assert(this.#isLocal(newPath), `${newPath} would be out of bounds.`)
593
553
 
594
- return new this.constructor(merged, this.temporary)
554
+ return new this.constructor(newPath, this)
595
555
  }
596
556
 
597
557
  /**
@@ -601,7 +561,7 @@ export default class DirectoryObject extends FS {
601
561
  * duplication. The resulting FileObject can be used for reading, writing,
602
562
  * and other file operations.
603
563
  *
604
- * @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")
605
565
  * @returns {FileObject} A new FileObject instance with the combined path
606
566
  * @throws {Sass} If filename is not a string
607
567
  * @example
@@ -614,11 +574,13 @@ export default class DirectoryObject extends FS {
614
574
  * const file = dir.getFile("src/index.js")
615
575
  * const data = await file.read()
616
576
  */
617
- getFile(filename) {
618
- 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.`)
619
583
 
620
- // Pass the filename and this directory as parent
621
- // This ensures the FileObject maintains the correct parent reference
622
- return new FileObject(filename, this)
584
+ return new FileObject(newPath, this)
623
585
  }
624
586
  }