@gesslar/toolkit 2.9.0 → 2.10.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gesslar/toolkit",
3
- "version": "2.9.0",
3
+ "version": "2.10.0",
4
4
  "description": "A collection of utilities for Node.js and browser environments.",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
@@ -31,89 +31,80 @@ export default class CappedDirectoryObject extends DirectoryObject {
31
31
  /**
32
32
  * Constructs a CappedDirectoryObject instance.
33
33
  *
34
- * This is an abstract base class - use subclasses like TempDirectoryObject
35
- * that define specific caps.
34
+ * Without a parent, the path becomes both the directory location and the cap
35
+ * (virtual root). With a parent, the path is resolved relative to the parent's
36
+ * cap using virtual path semantics (absolute paths treated as cap-relative).
36
37
  *
37
- * @param {string?} name - Base name for the directory (if empty/null, uses cap root)
38
- * @param {string} cap - The root path that constrains this directory tree
38
+ * @param {string} dirPath - Directory path (becomes cap if no parent, else relative to parent's cap)
39
39
  * @param {CappedDirectoryObject?} [parent] - Optional parent capped directory
40
40
  * @param {boolean} [temporary=false] - Whether this is a temporary directory
41
- * @throws {Sass} If name is absolute
42
- * @throws {Sass} If name is empty (when parent is provided)
43
- * @throws {Sass} If name contains path separators
44
- * @throws {Sass} If parent is not a capped directory
45
- * @throws {Sass} If parent's lineage does not trace back to the cap
41
+ * @throws {Sass} If path is empty
42
+ * @throws {Sass} If parent is provided but not a CappedDirectoryObject
46
43
  * @throws {Sass} If the resulting path would escape the cap
44
+ * @example
45
+ * // Create new capped directory
46
+ * const cache = new CappedDirectoryObject("/home/user/.cache")
47
+ * // path: /home/user/.cache, cap: /home/user/.cache
48
+ *
49
+ * @example
50
+ * // Create subdirectory with parent
51
+ * const data = new CappedDirectoryObject("data", cache)
52
+ * // path: /home/user/.cache/data, cap: /home/user/.cache
53
+ *
54
+ * @example
55
+ * // Virtual absolute path with parent
56
+ * const config = new CappedDirectoryObject("/etc/config", cache)
57
+ * // path: /home/user/.cache/etc/config, cap: /home/user/.cache
47
58
  */
48
- constructor(name, cap, parent=null, temporary=false) {
49
- Valid.type(cap, "String")
59
+ constructor(dirPath, parent=null, temporary=false) {
60
+ Valid.type(dirPath, "String")
61
+ Valid.assert(dirPath.length > 0, "Path must not be empty.")
50
62
 
51
63
  // Validate parent using instanceof since TypeSpec doesn't understand inheritance
52
64
  if(parent !== null && !(parent instanceof CappedDirectoryObject)) {
53
65
  throw Sass.new(`Parent must be null or a CappedDirectoryObject instance, got ${Data.typeOf(parent)}`)
54
66
  }
55
67
 
56
- let dirPath
68
+ let cap
69
+ let resolvedPath
57
70
 
58
- // Special case: empty name with no parent means use cap root
59
- if(!name && !parent) {
60
- dirPath = cap
71
+ if(!parent) {
72
+ // No parent: dirPath becomes both the directory and the cap
73
+ cap = path.resolve(dirPath)
74
+ resolvedPath = cap
61
75
  } else {
62
- Valid.type(name, "String")
76
+ // With parent: inherit cap and resolve dirPath relative to it
77
+ cap = parent.#cap
63
78
 
64
- // Security: Validate name before any processing
65
- Valid.assert(
66
- !path.isAbsolute(name),
67
- "Capped directory name must not be an absolute path.",
68
- )
69
- Valid.assert(
70
- name.length > 0,
71
- "Capped directory name must not be empty.",
72
- )
73
- Valid.assert(
74
- !name.includes("/") && !name.includes("\\") && !name.includes(path.sep),
75
- "Capped directory name must not contain path separators.",
76
- )
79
+ // Use real path for filesystem operations
80
+ const parentPath = parent.realPath || parent.path
81
+ const capResolved = path.resolve(cap)
77
82
 
78
- if(parent) {
79
- // Ensure parent is capped
80
- Valid.assert(parent.capped, "Parent must be a capped DirectoryObject.")
81
-
82
- // Ensure parent has same cap
83
- Valid.assert(
84
- parent.cap === cap,
85
- "Parent must have the same cap as this directory.",
86
- )
87
-
88
- // Use real path for filesystem operations
89
- const parentPath = parent.realPath || parent.path
90
-
91
- // Validate parent's lineage traces back to the cap
92
- let found = false
93
- if(parent.trail) {
94
- for(const p of parent.walkUp) {
95
- if(p.path === cap) {
96
- found = true
97
- break
98
- }
99
- }
100
- }
101
-
102
- Valid.assert(
103
- found,
104
- `The lineage of this directory must trace back to the cap '${cap}'.`,
105
- )
106
-
107
- dirPath = path.join(parentPath, name)
83
+ let targetPath
84
+
85
+ // If absolute, treat as virtual path relative to cap (strip leading /)
86
+ if(path.isAbsolute(dirPath)) {
87
+ const relative = dirPath.replace(/^[/\\]+/, "")
88
+ targetPath = relative ? path.join(capResolved, relative) : capResolved
89
+ } else {
90
+ // Relative path - resolve from parent directory
91
+ targetPath = FS.resolvePath(parentPath, dirPath)
92
+ }
93
+
94
+ // Resolve to absolute path (handles .. and .)
95
+ const resolved = path.resolve(targetPath)
96
+
97
+ // Clamp to cap boundary - cannot escape above cap
98
+ if(!resolved.startsWith(capResolved)) {
99
+ // Path tried to escape - clamp to cap root
100
+ resolvedPath = capResolved
108
101
  } else {
109
- // No parent - this is a root-level capped directory
110
- dirPath = path.join(cap, name)
102
+ resolvedPath = resolved
111
103
  }
112
104
  }
113
105
 
114
106
  // Call parent constructor with the path
115
- // Pass through the temporary flag (subclasses control this)
116
- super(dirPath, temporary)
107
+ super(resolvedPath, temporary)
117
108
 
118
109
  // Store the cap AFTER calling super()
119
110
  this.#cap = cap
@@ -344,20 +335,8 @@ export default class CappedDirectoryObject extends DirectoryObject {
344
335
  !newPath.includes("..")
345
336
 
346
337
  if(isSimpleName) {
347
- // For CappedDirectoryObject, pass (name, cap, parent, temporary)
348
- // For TempDirectoryObject subclass, it expects (name, parent) but will
349
- // internally call super with the cap parameter
350
- if(this.constructor === CappedDirectoryObject) {
351
- return new CappedDirectoryObject(
352
- newPath,
353
- this.#cap,
354
- this,
355
- this.temporary
356
- )
357
- }
358
-
359
- // For subclasses like TempDirectoryObject
360
- return new this.constructor(newPath, this)
338
+ // Both CappedDirectoryObject and subclasses use same signature now
339
+ return new this.constructor(newPath, this, this.temporary)
361
340
  }
362
341
 
363
342
  // Complex path - handle coercion
@@ -407,7 +386,7 @@ export default class CappedDirectoryObject extends DirectoryObject {
407
386
  // Create a base CappedDirectoryObject at the cap path
408
387
  // This works for direct usage of CappedDirectoryObject
409
388
  // Subclasses may need to override if they have special semantics
410
- return new CappedDirectoryObject(null, this.#cap, null, this.temporary)
389
+ return new CappedDirectoryObject(this.#cap, null, this.temporary)
411
390
  }
412
391
 
413
392
  /**
@@ -426,12 +405,7 @@ export default class CappedDirectoryObject extends DirectoryObject {
426
405
  // Traverse each segment, creating CappedDirectoryObject instances
427
406
  // (not subclass instances, to avoid constructor signature issues)
428
407
  for(const segment of segments) {
429
- current = new CappedDirectoryObject(
430
- segment,
431
- this.#cap,
432
- current,
433
- this.temporary
434
- )
408
+ current = new CappedDirectoryObject(segment, current, this.temporary)
435
409
  }
436
410
 
437
411
  return current
@@ -6,6 +6,7 @@
6
6
 
7
7
  import fs from "node:fs"
8
8
  import os from "node:os"
9
+ import path from "node:path"
9
10
 
10
11
  import CappedDirectoryObject from "./CappedDirectoryObject.js"
11
12
  import Sass from "./Sass.js"
@@ -57,22 +58,63 @@ export default class TempDirectoryObject extends CappedDirectoryObject {
57
58
  * await parent.remove() // Removes both parent and child
58
59
  */
59
60
  constructor(name, parent=null) {
60
- let finalName = name
61
+ let dirPath
62
+ let cappedParent = parent
61
63
 
62
- // Only generate unique suffix if we have a name and no parent
63
- if(name && !parent) {
64
- const prefix = name.endsWith("-") ? name : `${name}-`
65
- const uniqueSuffix =
66
- Math.random()
67
- .toString(36)
68
- .substring(2, 8)
69
- .toUpperCase()
70
- finalName = `${prefix}${uniqueSuffix}`
64
+ if(!parent) {
65
+ // No parent: need to create a capped parent at tmpdir first
66
+ cappedParent = new CappedDirectoryObject(os.tmpdir(), null, true)
67
+
68
+ if(name) {
69
+ // Check if name is a simple name (no separators, not absolute)
70
+ const isSimpleName = !path.isAbsolute(name) &&
71
+ !name.includes("/") &&
72
+ !name.includes("\\") &&
73
+ !name.includes(path.sep)
74
+
75
+ if(isSimpleName) {
76
+ // Simple name: add unique suffix
77
+ const prefix = name.endsWith("-") ? name : `${name}-`
78
+ const uniqueSuffix =
79
+ Math.random()
80
+ .toString(36)
81
+ .substring(2, 8)
82
+ .toUpperCase()
83
+ dirPath = `${prefix}${uniqueSuffix}`
84
+ } else {
85
+ // Complex path: use as-is, let CappedDirectoryObject handle coercion
86
+ dirPath = name
87
+ }
88
+ } else {
89
+ // No name: use tmpdir itself (no parent)
90
+ dirPath = os.tmpdir()
91
+ cappedParent = null
92
+ }
93
+ } else {
94
+ // With parent: validate it's a proper temp directory parent
95
+ if(!(parent instanceof CappedDirectoryObject)) {
96
+ throw Sass.new(
97
+ "Parent must be a CappedDirectoryObject or TempDirectoryObject."
98
+ )
99
+ }
100
+
101
+ // SECURITY: Ensure parent's cap is tmpdir (prevent escape to other caps)
102
+ const tmpdir = os.tmpdir()
103
+ if(parent.cap !== tmpdir) {
104
+ throw Sass.new(
105
+ `Parent must be capped to OS temp directory (${tmpdir}), ` +
106
+ `got cap: ${parent.cap}`
107
+ )
108
+ }
109
+
110
+ dirPath = name || ""
111
+ if(!dirPath) {
112
+ throw Sass.new("Name must not be empty when parent is provided.")
113
+ }
71
114
  }
72
115
 
73
- // Call parent constructor with the cap set to OS temp directory
74
- // Mark as temporary=true so remove() works
75
- super(finalName, os.tmpdir(), parent, true)
116
+ // Call parent constructor with new signature
117
+ super(dirPath, cappedParent, true)
76
118
 
77
119
  // Temp-specific behavior: create directory immediately
78
120
  this.#createDirectory()
@@ -86,7 +128,8 @@ export default class TempDirectoryObject extends CappedDirectoryObject {
86
128
  */
87
129
  #createDirectory() {
88
130
  try {
89
- fs.mkdirSync(this.realPath)
131
+ // Use recursive: true to create parent directories as needed
132
+ fs.mkdirSync(this.realPath, {recursive: true})
90
133
  } catch(e) {
91
134
  // EEXIST is fine - directory already exists
92
135
  if(e.code !== "EEXIST") {
@@ -11,21 +11,32 @@ export default class CappedDirectoryObject extends DirectoryObject {
11
11
  /**
12
12
  * Constructs a CappedDirectoryObject instance.
13
13
  *
14
- * This is an abstract base class - use subclasses like TempDirectoryObject
15
- * that define specific caps.
14
+ * Without a parent, the path becomes both the directory location and the cap
15
+ * (virtual root). With a parent, the path is resolved relative to the parent's
16
+ * cap using virtual path semantics (absolute paths treated as cap-relative).
16
17
  *
17
- * @param {string?} name - Base name for the directory (if empty/null, uses cap root)
18
- * @param {string} cap - The root path that constrains this directory tree
18
+ * @param {string} dirPath - Directory path (becomes cap if no parent, else relative to parent's cap)
19
19
  * @param {CappedDirectoryObject?} [parent] - Optional parent capped directory
20
20
  * @param {boolean} [temporary=false] - Whether this is a temporary directory
21
- * @throws {Sass} If name is absolute
22
- * @throws {Sass} If name is empty (when parent is provided)
23
- * @throws {Sass} If name contains path separators
24
- * @throws {Sass} If parent is not a capped directory
25
- * @throws {Sass} If parent's lineage does not trace back to the cap
21
+ * @throws {Sass} If path is empty
22
+ * @throws {Sass} If parent is provided but not a CappedDirectoryObject
26
23
  * @throws {Sass} If the resulting path would escape the cap
24
+ * @example
25
+ * // Create new capped directory
26
+ * const cache = new CappedDirectoryObject("/home/user/.cache")
27
+ * // path: /home/user/.cache, cap: /home/user/.cache
28
+ *
29
+ * @example
30
+ * // Create subdirectory with parent
31
+ * const data = new CappedDirectoryObject("data", cache)
32
+ * // path: /home/user/.cache/data, cap: /home/user/.cache
33
+ *
34
+ * @example
35
+ * // Virtual absolute path with parent
36
+ * const config = new CappedDirectoryObject("/etc/config", cache)
37
+ * // path: /home/user/.cache/etc/config, cap: /home/user/.cache
27
38
  */
28
- constructor(name: string | null, cap: string, parent?: CappedDirectoryObject | null, temporary?: boolean);
39
+ constructor(dirPath: string, parent?: CappedDirectoryObject | null, temporary?: boolean);
29
40
  /**
30
41
  * Returns the cap path for this directory.
31
42
  *
@@ -1 +1 @@
1
- {"version":3,"file":"CappedDirectoryObject.d.ts","sourceRoot":"","sources":["../../lib/CappedDirectoryObject.js"],"names":[],"mappings":"AAkBA;;;;;;;;GAQG;AACH;IAGE;;;;;;;;;;;;;;;;OAgBG;IACH,kBAXW,MAAM,OAAC,OACP,MAAM,WACN,qBAAqB,OAAC,cACtB,OAAO,EAmFjB;IAqBD;;;;OAIG;IACH,WAFa,MAAM,CAIlB;IAED;;;;OAIG;IACH,cAFa,OAAO,CAInB;IAED;;;;;OAKG;IACH,0BAFa,MAAM,CAIlB;IAqCD;;;;;;;;;;;;;;;;;OAiBG;IACH,YAba,eAAe,CAe3B;IA2ED;;;;OAIG;IACH,cAFa,SAAS,CAAC,eAAe,CAAC,CAItC;IAED;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,sBAjBW,MAAM,GACJ,qBAAqB,CA6EjC;IA0ID;;;;;OAKG;IACH,WAHW,MAAM,GACJ,OAAO,CAAC;QAAC,KAAK,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;QAAC,WAAW,QAAO;KAAC,CAAC,CAanE;;CAoDF;4BA9kB2B,sBAAsB;uBAC3B,iBAAiB"}
1
+ {"version":3,"file":"CappedDirectoryObject.d.ts","sourceRoot":"","sources":["../../lib/CappedDirectoryObject.js"],"names":[],"mappings":"AAkBA;;;;;;;;GAQG;AACH;IAGE;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACH,qBArBW,MAAM,WACN,qBAAqB,OAAC,cACtB,OAAO,EA0EjB;IAqBD;;;;OAIG;IACH,WAFa,MAAM,CAIlB;IAED;;;;OAIG;IACH,cAFa,OAAO,CAInB;IAED;;;;;OAKG;IACH,0BAFa,MAAM,CAIlB;IAqCD;;;;;;;;;;;;;;;;;OAiBG;IACH,YAba,eAAe,CAe3B;IA2ED;;;;OAIG;IACH,cAFa,SAAS,CAAC,eAAe,CAAC,CAItC;IAED;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,sBAjBW,MAAM,GACJ,qBAAqB,CAiEjC;IAqID;;;;;OAKG;IACH,WAHW,MAAM,GACJ,OAAO,CAAC;QAAC,KAAK,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;QAAC,WAAW,QAAO;KAAC,CAAC,CAanE;;CAoDF;4BApjB2B,sBAAsB;uBAC3B,iBAAiB"}
@@ -1 +1 @@
1
- {"version":3,"file":"TempDirectoryObject.d.ts","sourceRoot":"","sources":["../../lib/TempDirectoryObject.js"],"names":[],"mappings":"AAYA;;;;;;;;;GASG;AACH;IAEE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAiCG;IACH,mBAxBW,MAAM,OAAC,WACP,mBAAmB,OAAC,EA2C9B;IAqBD;;;;;;;;;;;;;;OAcG;IACH,sBAVW,MAAM,GACJ,mBAAmB,CAY/B;IAED;;;;;;;;;;;;;;OAcG;IACH,kBAVW,MAAM,GACJ,UAAU,CAYtB;;CAUF;kCA1IiC,4BAA4B"}
1
+ {"version":3,"file":"TempDirectoryObject.d.ts","sourceRoot":"","sources":["../../lib/TempDirectoryObject.js"],"names":[],"mappings":"AAaA;;;;;;;;;GASG;AACH;IAEE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAiCG;IACH,mBAxBW,MAAM,OAAC,WACP,mBAAmB,OAAC,EAoF9B;IAsBD;;;;;;;;;;;;;;OAcG;IACH,sBAVW,MAAM,GACJ,mBAAmB,CAY/B;IAED;;;;;;;;;;;;;;OAcG;IACH,kBAVW,MAAM,GACJ,UAAU,CAYtB;;CAUF;kCApLiC,4BAA4B"}