@gesslar/toolkit 2.7.1 → 2.9.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/README.md CHANGED
@@ -16,6 +16,7 @@ a Tauri app.
16
16
  | Disposer | Lifecycle management for disposable resources |
17
17
  | HTML | HTML loading and sanitization utilities |
18
18
  | Notify | Event system wrapper for DOM events |
19
+ | Promised | Promise utilities for settling, filtering, and extracting values from promise results |
19
20
  | Sass | Custom Error class with enhanced features |
20
21
  | Tantrum | AggregateError implementation |
21
22
  | Type | String-based type management (exported as TypeSpec in browser) |
@@ -93,3 +94,11 @@ import { Data, Collection, Util } from '@gesslar/toolkit/browser'
93
94
  ```
94
95
 
95
96
  The browser version includes: Collection, Data, Disposer, HTML, Notify, Sass, Tantrum, Type (TypeSpec), Util, and Valid. Node-only modules (Cache, CappedDirectoryObject, Contract, DirectoryObject, FileObject, FS, Glog, Schemer, TempDirectoryObject, Term, Terms) are not available in the browser version.
97
+
98
+ ## Post Partum
99
+
100
+ If you made it this far, please understand that I have absolutely zero scruples when it comes to breaking changes. Primarily, the audience for this library is myself. Consequently, anything that relies on the contents of this library will dutifully crash and I'll have to refactor those things then. It's like leaving playing nicky nicky nine doors. But with myself. And there's a lazy bomb waiting for me. That I planted. For me. And the bomb just explodes poop.
101
+
102
+ You're of course welcome to use my library! It's pretty robust. Uhhh, but maybe lock in the version until you see if something is gonna poop all over you. I make robots make my PR notifications and generally they're very good at firing off klaxons about my fetish for breaking changes, so you should be all right if you're paying attention. 🤷🏻
103
+
104
+ Sincerely, Senator Yabba of the Dabba (Doo)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gesslar/toolkit",
3
- "version": "2.7.1",
3
+ "version": "2.9.0",
4
4
  "description": "A collection of utilities for Node.js and browser environments.",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
@@ -8,6 +8,7 @@ export {Disposer as DisposerClass} from "./lib/Disposer.js"
8
8
  export {default as HTML} from "./lib/HTML.js"
9
9
  export {HTML as HTMLClass} from "./lib/HTML.js"
10
10
  export {default as Notify} from "./lib/Notify.js"
11
+ export {default as Promised} from "./lib/Promised.js"
11
12
  export {default as Sass} from "./lib/Sass.js"
12
13
  export {default as Tantrum} from "./lib/Tantrum.js"
13
14
  export {default as Type} from "./lib/TypeSpec.js"
@@ -0,0 +1,120 @@
1
+ import Tantrum from "./Tantrum.js"
2
+
3
+ /**
4
+ * Utility class providing helper functions for working with Promises,
5
+ * including settling, filtering, and extracting values from promise results.
6
+ */
7
+ export default class Promised {
8
+ /**
9
+ * Asynchronously awaits all promises in parallel.
10
+ * Wrapper around Promise.all for consistency with other utility methods.
11
+ *
12
+ * @param {Array<Promise<unknown>>} promises - Array of promises to await
13
+ * @returns {Promise<Array<unknown>>} Results of all promises
14
+ */
15
+ static async await(promises) {
16
+ return await Promise.all(promises)
17
+ }
18
+
19
+ /**
20
+ * Settles all promises (both fulfilled and rejected) in parallel.
21
+ * Wrapper around Promise.allSettled for consistency with other utility methods.
22
+ *
23
+ * @param {Array<Promise<unknown>>} promises - Array of promises to settle
24
+ * @returns {Promise<Array<{status: 'fulfilled'|'rejected', value?: unknown, reason?: unknown}>>} Results of all settled promises with status and value/reason
25
+ */
26
+ static async settle(promises) {
27
+ return await Promise.allSettled(promises)
28
+ }
29
+
30
+ /**
31
+ * Checks if any result in the settled promise array is rejected.
32
+ *
33
+ * @param {Array<{status: 'fulfilled'|'rejected', value?: unknown, reason?: unknown}>} settled - Array of settled promise results
34
+ * @returns {boolean} True if any result is rejected, false otherwise
35
+ */
36
+ static hasRejected(settled) {
37
+ return settled.some(r => r.status === "rejected")
38
+ }
39
+
40
+ /**
41
+ * Checks if any result in the settled promise array is fulfilled.
42
+ *
43
+ * @param {Array<{status: 'fulfilled'|'rejected', value?: unknown, reason?: unknown}>} settled - Array of settled promise results
44
+ * @returns {boolean} True if any result is fulfilled, false otherwise
45
+ */
46
+ static hasFulfilled(settled) {
47
+ return settled.some(r => r.status === "fulfilled")
48
+ }
49
+
50
+ /**
51
+ * Filters and returns all rejected results from a settled promise array.
52
+ *
53
+ * @param {Array<{status: 'fulfilled'|'rejected', value?: unknown, reason?: unknown}>} settled - Array of settled promise results
54
+ * @returns {Array<{status: 'rejected', reason: unknown}>} Array of rejected results
55
+ */
56
+ static rejected(settled) {
57
+ return settled.filter(r => r.status === "rejected")
58
+ }
59
+
60
+ /**
61
+ * Filters and returns all fulfilled results from a settled promise array.
62
+ *
63
+ * @param {Array<{status: 'fulfilled'|'rejected', value?: unknown, reason?: unknown}>} result - Array of settled promise results
64
+ * @returns {Array<{status: 'fulfilled', value: unknown}>} Array of fulfilled results
65
+ */
66
+ static fulfilled(result) {
67
+ return result.filter(r => r.status === "fulfilled")
68
+ }
69
+
70
+ /**
71
+ * Extracts the rejection reasons from a settled promise array.
72
+ *
73
+ * @param {Array<{status: 'fulfilled'|'rejected', value?: unknown, reason?: unknown}>} settled - Array of settled promise results
74
+ * @returns {Array<unknown>} Array of rejection reasons
75
+ */
76
+ static reasons(settled) {
77
+ const rejected = this.rejected(settled)
78
+ const reasons = rejected.map(e => e.reason)
79
+
80
+ return reasons
81
+ }
82
+
83
+ /**
84
+ * Extracts the values from fulfilled results in a settled promise array.
85
+ *
86
+ * @param {Array<{status: 'fulfilled'|'rejected', value?: unknown, reason?: unknown}>} settled - Array of settled promise results
87
+ * @returns {Array<unknown>} Array of fulfilled values
88
+ */
89
+ static values(settled) {
90
+ const fulfilled = this.fulfilled(settled)
91
+ const values = fulfilled.map(e => e.value)
92
+
93
+ return values
94
+ }
95
+
96
+ /**
97
+ * Throws a Tantrum containing all rejection reasons from settled promises.
98
+ *
99
+ * @param {string} message - Error message. Defaults to "GIGO"
100
+ * @param {Array<{status: 'fulfilled'|'rejected', value?: unknown, reason?: unknown}>} settled - Array of settled promise results
101
+ * @throws {Tantrum} Throws a Tantrum error with rejection reasons
102
+ */
103
+ static throw(message="GIGO", settled) {
104
+ const rejected = this.rejected(settled)
105
+ const reasons = this.reasons(rejected)
106
+
107
+ throw Tantrum.new(message, reasons)
108
+ }
109
+
110
+ /**
111
+ * Returns the first promise to resolve or reject from an array of promises.
112
+ * Wrapper around Promise.race for consistency with other utility methods.
113
+ *
114
+ * @param {Array<Promise<unknown>>} promises - Array of promises to race
115
+ * @returns {Promise<unknown>} Result of the first settled promise
116
+ */
117
+ static async race(promises) {
118
+ return await Promise.race(promises)
119
+ }
120
+ }
@@ -1,4 +1,3 @@
1
- import Tantrum from "./Tantrum.js"
2
1
  import Valid from "./Valid.js"
3
2
  import Collection from "./Collection.js"
4
3
 
@@ -80,103 +79,6 @@ export default class Util {
80
79
  return `${" ".repeat(leftPadding)}${work}${" ".repeat(rightPadding)}`
81
80
  }
82
81
 
83
- /**
84
- * Asynchronously awaits all promises in parallel.
85
- * Wrapper around Promise.all for consistency with other utility methods.
86
- *
87
- * @param {Array<Promise<unknown>>} promises - Array of promises to await
88
- * @returns {Promise<Array<unknown>>} Results of all promises
89
- */
90
- static async awaitAll(promises) {
91
- return await Promise.all(promises)
92
- }
93
-
94
- /**
95
- * Settles all promises (both fulfilled and rejected) in parallel.
96
- * Wrapper around Promise.allSettled for consistency with other utility methods.
97
- *
98
- * @param {Array<Promise<unknown>>} promises - Array of promises to settle
99
- * @returns {Promise<Array<object>>} Results of all settled promises with status and value/reason
100
- */
101
- static async settleAll(promises) {
102
- return await Promise.allSettled(promises)
103
- }
104
-
105
- /**
106
- * Checks if any result in the settled promise array is rejected.
107
- *
108
- * @param {Array<object>} result - Array of settled promise results.
109
- * @returns {boolean} True if any result is rejected, false otherwise.
110
- */
111
- static anyRejected(result) {
112
- return result.some(r => r.status === "rejected")
113
- }
114
-
115
- /**
116
- * Filters and returns all rejected results from a settled promise array.
117
- *
118
- * @param {Array<object>} result - Array of settled promise results.
119
- * @returns {Array<object>} Array of rejected results.
120
- */
121
- static settledAndRejected(result) {
122
- return result.filter(r => r.status === "rejected")
123
- }
124
-
125
- /**
126
- * Extracts the rejection reasons from an array of rejected promise results.
127
- *
128
- * @param {Array<object>} rejected - Array of rejected results.
129
- * @returns {Array<unknown>} Array of rejection reasons.
130
- */
131
- static rejectedReasons(rejected) {
132
- return rejected.map(r => r.reason)
133
- }
134
-
135
- /**
136
- * Throws a Sass error containing all rejection reasons from settled promises.
137
- *
138
- * @param {string} [_message] - Optional error message. Defaults to "GIGO"
139
- * @param {Array<object>} rejected - Array of rejected results.
140
- * @throws {Error} Throws a Tantrum error with rejection reasons.
141
- */
142
- static throwRejected(message="GIGO", settled) {
143
- throw Tantrum.new(
144
- message,
145
- this.rejectedReasons(this.settledAndRejected(settled))
146
- )
147
- }
148
-
149
- /**
150
- * Filters and returns all fulfilled results from a settled promise array.
151
- *
152
- * @param {Array<object>} result - Array of settled promise results.
153
- * @returns {Array<object>} Array of fulfilled results.
154
- */
155
- static settledAndFulfilled(result) {
156
- return result.filter(r => r.status === "fulfilled")
157
- }
158
-
159
- /**
160
- * Extracts the values from all fulfilled results in a settled promise array.
161
- *
162
- * @param {Array<object>} result - Array of settled promise results.
163
- * @returns {Array<unknown>} Array of fulfilled values.
164
- */
165
- static fulfilledValues(result) {
166
- return this.settledAndFulfilled(result).map(r => r.value)
167
- }
168
-
169
- /**
170
- * Returns the first promise to resolve or reject from an array of promises.
171
- * Wrapper around Promise.race for consistency with other utility methods.
172
- *
173
- * @param {Array<Promise<unknown>>} promises - Array of promises to race
174
- * @returns {Promise<unknown>} Result of the first settled promise
175
- */
176
- static async race(promises) {
177
- return await Promise.race(promises)
178
- }
179
-
180
82
  /**
181
83
  * Determine the Levenshtein distance between two string values
182
84
  *
package/src/index.js CHANGED
@@ -3,6 +3,7 @@ export {default as Collection} from "./browser/lib/Collection.js"
3
3
  export {default as Data} from "./browser/lib/Data.js"
4
4
  export {default as Disposer} from "./browser/lib/Disposer.js"
5
5
  export {Disposer as DisposerClass} from "./browser/lib/Disposer.js"
6
+ export {default as Promised} from "./browser/lib/Promised.js"
6
7
  export {default as Type} from "./browser/lib/TypeSpec.js"
7
8
  export {default as Valid} from "./lib/Valid.js"
8
9
 
@@ -13,6 +13,7 @@ import path from "node:path"
13
13
  import {Data, Valid} from "../browser/index.js"
14
14
  import DirectoryObject from "./DirectoryObject.js"
15
15
  import FileObject from "./FileObject.js"
16
+ import FS from "./FS.js"
16
17
  import Sass from "./Sass.js"
17
18
 
18
19
  /**
@@ -84,7 +85,8 @@ export default class CappedDirectoryObject extends DirectoryObject {
84
85
  "Parent must have the same cap as this directory.",
85
86
  )
86
87
 
87
- const parentPath = parent.path
88
+ // Use real path for filesystem operations
89
+ const parentPath = parent.realPath || parent.path
88
90
 
89
91
  // Validate parent's lineage traces back to the cap
90
92
  let found = false
@@ -128,13 +130,13 @@ export default class CappedDirectoryObject extends DirectoryObject {
128
130
  */
129
131
  #validateCapPath() {
130
132
  const cap = this.#cap
131
- const resolved = path.resolve(this.path)
133
+ const resolved = path.resolve(this.#realPath)
132
134
  const capResolved = path.resolve(cap)
133
135
 
134
136
  // Check if the resolved path starts with the cap directory
135
137
  if(!resolved.startsWith(capResolved)) {
136
138
  throw Sass.new(
137
- `Path '${this.path}' is not within the cap directory '${cap}'`
139
+ `Path '${this.#realPath}' is not within the cap directory '${cap}'`
138
140
  )
139
141
  }
140
142
  }
@@ -157,6 +159,73 @@ export default class CappedDirectoryObject extends DirectoryObject {
157
159
  return true
158
160
  }
159
161
 
162
+ /**
163
+ * Returns the real filesystem path (for internal and subclass use).
164
+ *
165
+ * @protected
166
+ * @returns {string} The actual filesystem path
167
+ */
168
+ get realPath() {
169
+ return super.path
170
+ }
171
+
172
+ /**
173
+ * Private alias for realPath (for use in private methods).
174
+ *
175
+ * @private
176
+ * @returns {string} The actual filesystem path
177
+ */
178
+ get #realPath() {
179
+ return this.realPath
180
+ }
181
+
182
+ /**
183
+ * Returns the virtual path relative to the cap.
184
+ * This is the default path representation in the capped environment.
185
+ * Use `.real.path` to access the actual filesystem path.
186
+ *
187
+ * @returns {string} Path relative to cap, or "/" if at cap root
188
+ * @example
189
+ * const temp = new TempDirectoryObject("myapp")
190
+ * const subdir = temp.getDirectory("data/cache")
191
+ * console.log(subdir.path) // "/data/cache" (virtual, relative to cap)
192
+ * console.log(subdir.real.path) // "/tmp/myapp-ABC123/data/cache" (actual filesystem)
193
+ */
194
+ get path() {
195
+ const capResolved = path.resolve(this.#cap)
196
+ const relative = path.relative(capResolved, this.#realPath)
197
+
198
+ // If at cap root or empty, return "/"
199
+ if(!relative || relative === ".") {
200
+ return "/"
201
+ }
202
+
203
+ // Return with leading slash to indicate it's cap-relative
204
+ return "/" + relative.split(path.sep).join("/")
205
+ }
206
+
207
+ /**
208
+ * Returns a plain DirectoryObject representing the actual filesystem location.
209
+ * This provides an "escape hatch" from the capped environment to interact
210
+ * with the real filesystem when needed.
211
+ *
212
+ * @returns {DirectoryObject} Uncapped directory object at the real filesystem path
213
+ * @example
214
+ * const temp = new TempDirectoryObject("myapp")
215
+ * const subdir = temp.getDirectory("data")
216
+ *
217
+ * // Work within the capped environment (virtual paths)
218
+ * console.log(subdir.path) // "/data" (virtual)
219
+ * subdir.getFile("config.json") // Stays within cap
220
+ *
221
+ * // Break out to real filesystem when needed
222
+ * console.log(subdir.real.path) // "/tmp/myapp-ABC123/data" (real)
223
+ * subdir.real.parent // Can traverse outside the cap
224
+ */
225
+ get real() {
226
+ return new DirectoryObject(this.#realPath)
227
+ }
228
+
160
229
  /**
161
230
  * Returns the parent directory of this capped directory.
162
231
  * Returns null only if this directory is at the cap (the "root" of the capped tree).
@@ -175,12 +244,17 @@ export default class CappedDirectoryObject extends DirectoryObject {
175
244
  const capResolved = path.resolve(this.#cap)
176
245
 
177
246
  // If we're at the cap, return null (cap is the "root")
178
- if(this.path === capResolved) {
247
+ if(this.#realPath === capResolved) {
179
248
  return null
180
249
  }
181
250
 
182
- // Otherwise return the parent (plain DirectoryObject, not capped)
183
- return super.parent
251
+ // Otherwise return the parent using real path (plain DirectoryObject, not capped)
252
+ const parentPath = path.dirname(this.#realPath)
253
+ const isRoot = parentPath === this.#realPath
254
+
255
+ return isRoot
256
+ ? null
257
+ : new DirectoryObject(parentPath, this.temporary)
184
258
  }
185
259
 
186
260
  /**
@@ -201,19 +275,27 @@ export default class CappedDirectoryObject extends DirectoryObject {
201
275
  *#walkUpCapped() {
202
276
  const capResolved = path.resolve(this.#cap)
203
277
 
204
- // Use super.walkUp but stop when we would go beyond the cap
205
- for(const dir of super.walkUp) {
278
+ // Build trail from real path
279
+ const trail = this.#realPath.split(path.sep).filter(Boolean)
280
+ const curr = [...trail]
281
+
282
+ while(curr.length > 0) {
283
+ const joined = path.sep + curr.join(path.sep)
284
+
206
285
  // Don't yield anything beyond the cap
207
- if(!dir.path.startsWith(capResolved)) {
286
+ if(!joined.startsWith(capResolved)) {
208
287
  break
209
288
  }
210
289
 
211
- yield dir
290
+ // Yield plain DirectoryObject with real path
291
+ yield new DirectoryObject(joined, this.temporary)
212
292
 
213
293
  // Stop after yielding the cap
214
- if(dir.path === capResolved) {
294
+ if(joined === capResolved) {
215
295
  break
216
296
  }
297
+
298
+ curr.pop()
217
299
  }
218
300
  }
219
301
 
@@ -229,69 +311,286 @@ export default class CappedDirectoryObject extends DirectoryObject {
229
311
  /**
230
312
  * Creates a new CappedDirectoryObject by extending this directory's path.
231
313
  *
232
- * Validates that the resulting path remains within the cap directory tree.
314
+ * All paths are coerced to remain within the cap directory tree:
315
+ * - Absolute paths (e.g., "/foo") are treated as relative to the cap
316
+ * - Parent traversal ("..") is allowed but clamped at the cap boundary
317
+ * - The cap acts as the virtual root directory
233
318
  *
234
- * @param {string} newPath - The path segment to append
235
- * @returns {CappedDirectoryObject} A new CappedDirectoryObject with the extended path
236
- * @throws {Sass} If the path would escape the cap directory
237
- * @throws {Sass} If the path is absolute
238
- * @throws {Sass} If the path contains traversal (..)
319
+ * @param {string} newPath - The path to resolve (can be absolute or contain ..)
320
+ * @returns {CappedDirectoryObject} A new CappedDirectoryObject with the coerced path
239
321
  * @example
240
322
  * const capped = new TempDirectoryObject("myapp")
241
323
  * const subDir = capped.getDirectory("data")
242
324
  * console.log(subDir.path) // "/tmp/myapp-ABC123/data"
325
+ *
326
+ * @example
327
+ * // Absolute paths are relative to cap
328
+ * const abs = capped.getDirectory("/foo/bar")
329
+ * console.log(abs.path) // "/tmp/myapp-ABC123/foo/bar"
330
+ *
331
+ * @example
332
+ * // Excessive .. traversal clamps to cap
333
+ * const up = capped.getDirectory("../../../etc/passwd")
334
+ * console.log(up.path) // "/tmp/myapp-ABC123" (clamped to cap)
243
335
  */
244
336
  getDirectory(newPath) {
245
337
  Valid.type(newPath, "String")
246
338
 
247
- // Prevent absolute paths
339
+ // Fast path: if it's a simple name (no separators, not absolute, no ..)
340
+ // use the subclass constructor directly to preserve type
341
+ const isSimpleName = !path.isAbsolute(newPath) &&
342
+ !newPath.includes("/") &&
343
+ !newPath.includes("\\") &&
344
+ !newPath.includes("..")
345
+
346
+ 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)
361
+ }
362
+
363
+ // Complex path - handle coercion
364
+ const capResolved = path.resolve(this.#cap)
365
+ let targetPath
366
+
367
+ // If absolute, treat as relative to cap (virtual root)
248
368
  if(path.isAbsolute(newPath)) {
249
- throw Sass.new("Absolute paths are not allowed in capped directories")
369
+ // Strip leading slashes to make relative
370
+ const relative = newPath.replace(/^[/\\]+/, "")
371
+
372
+ // Join with cap (unless empty, which means cap root)
373
+ targetPath = relative ? path.join(capResolved, relative) : capResolved
374
+ } else {
375
+ // Relative path - resolve from current directory
376
+ targetPath = FS.resolvePath(this.#realPath, newPath)
250
377
  }
251
378
 
252
- // Prevent path traversal attacks
253
- const normalized = path.normalize(newPath)
254
- if(normalized.includes("..")) {
255
- throw Sass.new("Path traversal (..) is not allowed in capped directories")
379
+ // Resolve to absolute path (handles .. and .)
380
+ const resolved = path.resolve(targetPath)
381
+
382
+ // Coerce: if path escaped cap, clamp to cap boundary
383
+ const coerced = resolved.startsWith(capResolved)
384
+ ? resolved
385
+ : capResolved
386
+
387
+ // Compute path relative to cap for reconstruction
388
+ const relativeToCap = path.relative(capResolved, coerced)
389
+
390
+ // If we're at the cap root, return cap root directory
391
+ if(!relativeToCap || relativeToCap === ".") {
392
+ return this.#createCappedAtRoot()
256
393
  }
257
394
 
258
- // Use the constructor of the current class (supports subclassing)
259
- // Pass this as parent so the child inherits the same cap
260
- return new this.constructor(newPath, this)
395
+ // Build directory by traversing segments from cap
396
+ return this.#buildDirectoryFromRelativePath(relativeToCap)
397
+ }
398
+
399
+ /**
400
+ * Creates a CappedDirectoryObject at the cap root.
401
+ * Can be overridden by subclasses that have different root semantics.
402
+ *
403
+ * @private
404
+ * @returns {CappedDirectoryObject} Directory object at cap root
405
+ */
406
+ #createCappedAtRoot() {
407
+ // Create a base CappedDirectoryObject at the cap path
408
+ // This works for direct usage of CappedDirectoryObject
409
+ // Subclasses may need to override if they have special semantics
410
+ return new CappedDirectoryObject(null, this.#cap, null, this.temporary)
411
+ }
412
+
413
+ /**
414
+ * Builds a directory by traversing path segments from cap.
415
+ *
416
+ * @private
417
+ * @param {string} relativePath - Path relative to cap
418
+ * @returns {CappedDirectoryObject} The directory at the final path
419
+ */
420
+ #buildDirectoryFromRelativePath(relativePath) {
421
+ const segments = relativePath.split(path.sep).filter(Boolean)
422
+
423
+ // Start at cap root
424
+ let current = this.#createCappedAtRoot()
425
+
426
+ // Traverse each segment, creating CappedDirectoryObject instances
427
+ // (not subclass instances, to avoid constructor signature issues)
428
+ for(const segment of segments) {
429
+ current = new CappedDirectoryObject(
430
+ segment,
431
+ this.#cap,
432
+ current,
433
+ this.temporary
434
+ )
435
+ }
436
+
437
+ return current
261
438
  }
262
439
 
263
440
  /**
264
441
  * Creates a new FileObject by extending this directory's path.
265
442
  *
266
- * Validates that the resulting path remains within the cap directory tree.
443
+ * All paths are coerced to remain within the cap directory tree:
444
+ * - Absolute paths (e.g., "/config.json") are treated as relative to the cap
445
+ * - Parent traversal ("..") is allowed but clamped at the cap boundary
446
+ * - The cap acts as the virtual root directory
267
447
  *
268
- * @param {string} filename - The filename to append
269
- * @returns {FileObject} A new FileObject with the extended path
270
- * @throws {Sass} If the path would escape the cap directory
271
- * @throws {Sass} If the path is absolute
272
- * @throws {Sass} If the path contains traversal (..)
448
+ * @param {string} filename - The filename to resolve (can be absolute or contain ..)
449
+ * @returns {FileObject} A new FileObject with the coerced path
273
450
  * @example
274
451
  * const capped = new TempDirectoryObject("myapp")
275
452
  * const file = capped.getFile("config.json")
276
453
  * console.log(file.path) // "/tmp/myapp-ABC123/config.json"
454
+ *
455
+ * @example
456
+ * // Absolute paths are relative to cap
457
+ * const abs = capped.getFile("/data/config.json")
458
+ * console.log(abs.path) // "/tmp/myapp-ABC123/data/config.json"
459
+ *
460
+ * @example
461
+ * // Excessive .. traversal clamps to cap
462
+ * const up = capped.getFile("../../../etc/passwd")
463
+ * console.log(up.path) // "/tmp/myapp-ABC123/passwd" (clamped to cap)
277
464
  */
278
465
  getFile(filename) {
279
466
  Valid.type(filename, "String")
280
467
 
281
- // Prevent absolute paths
468
+ // Fast path: if it's a simple filename (no separators, not absolute, no ..)
469
+ // use this as the parent directly
470
+ const isSimpleName = !path.isAbsolute(filename) &&
471
+ !filename.includes("/") &&
472
+ !filename.includes("\\") &&
473
+ !filename.includes("..")
474
+
475
+ if(isSimpleName) {
476
+ // Simple filename - create directly with this as parent
477
+ return new FileObject(filename, this)
478
+ }
479
+
480
+ // Complex path - handle coercion
481
+ const capResolved = path.resolve(this.#cap)
482
+ let targetPath
483
+
484
+ // If absolute, treat as relative to cap (virtual root)
282
485
  if(path.isAbsolute(filename)) {
283
- throw Sass.new("Absolute paths are not allowed in capped directories")
486
+ // Strip leading slashes to make relative
487
+ const relative = filename.replace(/^[/\\]+/, "")
488
+
489
+ // Join with cap
490
+ targetPath = path.join(capResolved, relative)
491
+ } else {
492
+ // Relative path - resolve from current directory
493
+ targetPath = FS.resolvePath(this.#realPath, filename)
284
494
  }
285
495
 
286
- // Prevent path traversal attacks
287
- const normalized = path.normalize(filename)
288
- if(normalized.includes("..")) {
289
- throw Sass.new("Path traversal (..) is not allowed in capped directories")
496
+ // Resolve to absolute path (handles .. and .)
497
+ const resolved = path.resolve(targetPath)
498
+
499
+ // Coerce: if path escaped cap, clamp to cap boundary
500
+ const coerced = resolved.startsWith(capResolved)
501
+ ? resolved
502
+ : capResolved
503
+
504
+ // Extract directory and filename parts
505
+ let fileDir = path.dirname(coerced)
506
+ let fileBasename = path.basename(coerced)
507
+
508
+ // Special case: if coerced is exactly the cap (file tried to escape),
509
+ // the file should be placed at the cap root with just the filename
510
+ if(coerced === capResolved) {
511
+ // Extract just the filename from the original path
512
+ fileBasename = path.basename(resolved)
513
+ fileDir = capResolved
290
514
  }
291
515
 
292
- // Pass the filename and this directory as parent
293
- // This ensures the FileObject maintains the correct parent reference
294
- return new FileObject(filename, this)
516
+ // Get or create the parent directory
517
+ const relativeToCap = path.relative(capResolved, fileDir)
518
+ const parentDir = !relativeToCap || relativeToCap === "."
519
+ ? this.#createCappedAtRoot()
520
+ : this.#buildDirectoryFromRelativePath(relativeToCap)
521
+
522
+ // Create FileObject with parent directory
523
+ return new FileObject(fileBasename, parentDir)
524
+ }
525
+
526
+ /**
527
+ * Override exists to use real filesystem path.
528
+ *
529
+ * @returns {Promise<boolean>} Whether the directory exists
530
+ */
531
+ get exists() {
532
+ return this.real.exists
533
+ }
534
+
535
+ /**
536
+ * Override read to use real filesystem path and return capped objects.
537
+ *
538
+ * @param {string} [pat=""] - Optional glob pattern
539
+ * @returns {Promise<{files: Array<FileObject>, directories: Array}>} Directory contents
540
+ */
541
+ async read(pat="") {
542
+ const {files, directories} = await this.real.read(pat)
543
+
544
+ // Convert plain DirectoryObjects to CappedDirectoryObjects with same cap
545
+ const cappedDirectories = directories.map(dir => {
546
+ const name = dir.name
547
+
548
+ return new this.constructor(name, this)
549
+ })
550
+
551
+ return {files, directories: cappedDirectories}
552
+ }
553
+
554
+ /**
555
+ * Override assureExists to use real filesystem path.
556
+ *
557
+ * @param {object} [options] - Options for mkdir
558
+ * @returns {Promise<void>}
559
+ */
560
+ async assureExists(options = {}) {
561
+ return await this.real.assureExists(options)
562
+ }
563
+
564
+ /**
565
+ * Override delete to use real filesystem path.
566
+ *
567
+ * @returns {Promise<void>}
568
+ */
569
+ async delete() {
570
+ return await this.real.delete()
571
+ }
572
+
573
+ /**
574
+ * Override remove to preserve temporary flag check.
575
+ *
576
+ * @returns {Promise<void>}
577
+ */
578
+ async remove() {
579
+ if(!this.temporary)
580
+ throw Sass.new("This is not a temporary directory.")
581
+
582
+ const {files, directories} = await this.read()
583
+
584
+ // Remove subdirectories recursively
585
+ for(const dir of directories)
586
+ await dir.remove()
587
+
588
+ // Remove files
589
+ for(const file of files)
590
+ await file.delete()
591
+
592
+ // Delete the now-empty directory
593
+ await this.delete()
295
594
  }
296
595
 
297
596
  /**
@@ -300,6 +599,6 @@ export default class CappedDirectoryObject extends DirectoryObject {
300
599
  * @returns {string} string representation of the CappedDirectoryObject
301
600
  */
302
601
  toString() {
303
- return `[CappedDirectoryObject: ${this.path}]`
602
+ return `[CappedDirectoryObject: ${this.path} (real: ${this.#realPath})]`
304
603
  }
305
604
  }
@@ -85,12 +85,10 @@ export default class FileObject extends FS {
85
85
  if(Data.isType(fileName, "FileObject"))
86
86
  fileName = fileName.path
87
87
 
88
- if(!fileName || typeof fileName !== "string" || fileName.length === 0) {
88
+ if(!fileName || typeof fileName !== "string" || fileName.length === 0)
89
89
  throw Sass.new("fileName must be a non-empty string")
90
- }
91
90
 
92
91
  const fixedFile = FS.fixSlashes(fileName)
93
-
94
92
  const {dir,base,ext} = this.#deconstructFilenameToParts(fixedFile)
95
93
 
96
94
  const parentObject = (() => {
@@ -98,6 +96,7 @@ export default class FileObject extends FS {
98
96
  case "String":
99
97
  return new DirectoryObject(parent)
100
98
  case "DirectoryObject":
99
+ case "CappedDirectoryObject":
101
100
  case "TempDirectoryObject":
102
101
  return parent
103
102
  default:
@@ -105,7 +104,9 @@ export default class FileObject extends FS {
105
104
  }
106
105
  })()
107
106
 
108
- const final = FS.resolvePath(parentObject.path ?? ".", fixedFile)
107
+ // Use real path if parent is capped, otherwise use path
108
+ const parentPath = parentObject.realPath || parentObject.path
109
+ const final = FS.resolvePath(parentPath ?? ".", fixedFile)
109
110
 
110
111
  const resolved = final
111
112
  const url = new URL(FS.pathToUri(resolved))
@@ -115,7 +116,9 @@ export default class FileObject extends FS {
115
116
 
116
117
  // If the file is directly in the provided parent directory, reuse that object
117
118
  // Otherwise, create a DirectoryObject for the actual parent directory
118
- const actualParent = parentObject && actualParentPath === parentObject.path
119
+ // Use real path for comparison if parent is capped
120
+ const parentRealPath = parentObject.realPath || parentObject.path
121
+ const actualParent = parentObject && actualParentPath === parentRealPath
119
122
  ? parentObject
120
123
  : new DirectoryObject(actualParentPath)
121
124
 
@@ -186,12 +189,28 @@ export default class FileObject extends FS {
186
189
  }
187
190
 
188
191
  /**
189
- * Return the fully resolved absolute path to the file on disk.
192
+ * Returns the file path. If the parent is a capped directory, returns the
193
+ * virtual path relative to the cap. Otherwise returns the real filesystem path.
194
+ * Use `.real.path` to always get the actual filesystem path.
190
195
  *
191
- * @returns {string} The fully resolved absolute file path
196
+ * @returns {string} The file path (virtual if parent is capped, real otherwise)
192
197
  */
193
198
  get path() {
194
- return this.#meta.path
199
+ const realPath = this.#meta.path
200
+ const parent = this.#meta.parent
201
+
202
+ // If parent is capped, return virtual path
203
+ if(parent?.capped) {
204
+ const cap = parent.cap
205
+ const capResolved = path.resolve(cap)
206
+ const relative = path.relative(capResolved, realPath)
207
+
208
+ // Return with leading slash to indicate it's cap-relative
209
+ return "/" + relative.split(path.sep).join("/")
210
+ }
211
+
212
+ // Otherwise return real path
213
+ return realPath
195
214
  }
196
215
 
197
216
  /**
@@ -266,6 +285,26 @@ export default class FileObject extends FS {
266
285
  return this.#meta.parent
267
286
  }
268
287
 
288
+ /**
289
+ * Returns a plain FileObject representing the actual filesystem location.
290
+ * This provides an "escape hatch" when working with capped directories,
291
+ * allowing direct filesystem access when needed.
292
+ *
293
+ * @returns {FileObject} Uncapped file object at the real filesystem path
294
+ * @example
295
+ * const temp = new TempDirectoryObject("myapp")
296
+ * const file = temp.getFile("/config/app.json")
297
+ *
298
+ * // file.path shows virtual path
299
+ * console.log(file.path) // "/config/app.json"
300
+ * // file.real.path shows actual filesystem path
301
+ * console.log(file.real.path) // "/tmp/myapp-ABC123/config/app.json"
302
+ * file.real.parent.parent // Can traverse outside the cap
303
+ */
304
+ get real() {
305
+ return new FileObject(this.#meta.path)
306
+ }
307
+
269
308
  /**
270
309
  * Check if a file can be read. Returns true if the file can be read, false
271
310
  *
@@ -273,7 +312,7 @@ export default class FileObject extends FS {
273
312
  */
274
313
  async canRead() {
275
314
  try {
276
- await fs.access(this.path, fs.constants.R_OK)
315
+ await fs.access(this.#meta.path, fs.constants.R_OK)
277
316
 
278
317
  return true
279
318
  } catch(_) {
@@ -288,7 +327,7 @@ export default class FileObject extends FS {
288
327
  */
289
328
  async canWrite() {
290
329
  try {
291
- await fs.access(this.path, fs.constants.W_OK)
330
+ await fs.access(this.#meta.path, fs.constants.W_OK)
292
331
 
293
332
  return true
294
333
  } catch(_) {
@@ -303,7 +342,7 @@ export default class FileObject extends FS {
303
342
  */
304
343
  async #fileExists() {
305
344
  try {
306
- await fs.access(this.path, fs.constants.F_OK)
345
+ await fs.access(this.#meta.path, fs.constants.F_OK)
307
346
 
308
347
  return true
309
348
  } catch(_) {
@@ -318,7 +357,7 @@ export default class FileObject extends FS {
318
357
  */
319
358
  async size() {
320
359
  try {
321
- const stat = await fs.stat(this.path)
360
+ const stat = await fs.stat(this.#meta.path)
322
361
 
323
362
  return stat.size
324
363
  } catch(_) {
@@ -334,7 +373,7 @@ export default class FileObject extends FS {
334
373
  */
335
374
  async modified() {
336
375
  try {
337
- const stat = await fs.stat(this.path)
376
+ const stat = await fs.stat(this.#meta.path)
338
377
 
339
378
  return stat.mtime
340
379
  } catch(_) {
@@ -86,12 +86,12 @@ export default class TempDirectoryObject extends CappedDirectoryObject {
86
86
  */
87
87
  #createDirectory() {
88
88
  try {
89
- fs.mkdirSync(this.path)
89
+ fs.mkdirSync(this.realPath)
90
90
  } catch(e) {
91
91
  // EEXIST is fine - directory already exists
92
92
  if(e.code !== "EEXIST") {
93
93
  throw Sass.new(
94
- `Unable to create temporary directory '${this.path}': ${e.message}`
94
+ `Unable to create temporary directory '${this.realPath}': ${e.message}`
95
95
  )
96
96
  }
97
97
  }
@@ -1,6 +1,7 @@
1
1
  export { default as Collection } from "./lib/Collection.js";
2
2
  export { default as Data } from "./lib/Data.js";
3
3
  export { default as Notify } from "./lib/Notify.js";
4
+ export { default as Promised } from "./lib/Promised.js";
4
5
  export { default as Sass } from "./lib/Sass.js";
5
6
  export { default as Tantrum } from "./lib/Tantrum.js";
6
7
  export { default as Type } from "./lib/TypeSpec.js";
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Utility class providing helper functions for working with Promises,
3
+ * including settling, filtering, and extracting values from promise results.
4
+ */
5
+ export default class Promised {
6
+ /**
7
+ * Asynchronously awaits all promises in parallel.
8
+ * Wrapper around Promise.all for consistency with other utility methods.
9
+ *
10
+ * @param {Array<Promise<unknown>>} promises - Array of promises to await
11
+ * @returns {Promise<Array<unknown>>} Results of all promises
12
+ */
13
+ static await(promises: Array<Promise<unknown>>): Promise<Array<unknown>>;
14
+ /**
15
+ * Settles all promises (both fulfilled and rejected) in parallel.
16
+ * Wrapper around Promise.allSettled for consistency with other utility methods.
17
+ *
18
+ * @param {Array<Promise<unknown>>} promises - Array of promises to settle
19
+ * @returns {Promise<Array<{status: 'fulfilled'|'rejected', value?: unknown, reason?: unknown}>>} Results of all settled promises with status and value/reason
20
+ */
21
+ static settle(promises: Array<Promise<unknown>>): Promise<Array<{
22
+ status: "fulfilled" | "rejected";
23
+ value?: unknown;
24
+ reason?: unknown;
25
+ }>>;
26
+ /**
27
+ * Checks if any result in the settled promise array is rejected.
28
+ *
29
+ * @param {Array<{status: 'fulfilled'|'rejected', value?: unknown, reason?: unknown}>} settled - Array of settled promise results
30
+ * @returns {boolean} True if any result is rejected, false otherwise
31
+ */
32
+ static hasRejected(settled: Array<{
33
+ status: "fulfilled" | "rejected";
34
+ value?: unknown;
35
+ reason?: unknown;
36
+ }>): boolean;
37
+ /**
38
+ * Checks if any result in the settled promise array is fulfilled.
39
+ *
40
+ * @param {Array<{status: 'fulfilled'|'rejected', value?: unknown, reason?: unknown}>} settled - Array of settled promise results
41
+ * @returns {boolean} True if any result is fulfilled, false otherwise
42
+ */
43
+ static hasFulfilled(settled: Array<{
44
+ status: "fulfilled" | "rejected";
45
+ value?: unknown;
46
+ reason?: unknown;
47
+ }>): boolean;
48
+ /**
49
+ * Filters and returns all rejected results from a settled promise array.
50
+ *
51
+ * @param {Array<{status: 'fulfilled'|'rejected', value?: unknown, reason?: unknown}>} settled - Array of settled promise results
52
+ * @returns {Array<{status: 'rejected', reason: unknown}>} Array of rejected results
53
+ */
54
+ static rejected(settled: Array<{
55
+ status: "fulfilled" | "rejected";
56
+ value?: unknown;
57
+ reason?: unknown;
58
+ }>): Array<{
59
+ status: "rejected";
60
+ reason: unknown;
61
+ }>;
62
+ /**
63
+ * Filters and returns all fulfilled results from a settled promise array.
64
+ *
65
+ * @param {Array<{status: 'fulfilled'|'rejected', value?: unknown, reason?: unknown}>} result - Array of settled promise results
66
+ * @returns {Array<{status: 'fulfilled', value: unknown}>} Array of fulfilled results
67
+ */
68
+ static fulfilled(result: Array<{
69
+ status: "fulfilled" | "rejected";
70
+ value?: unknown;
71
+ reason?: unknown;
72
+ }>): Array<{
73
+ status: "fulfilled";
74
+ value: unknown;
75
+ }>;
76
+ /**
77
+ * Extracts the rejection reasons from a settled promise array.
78
+ *
79
+ * @param {Array<{status: 'fulfilled'|'rejected', value?: unknown, reason?: unknown}>} settled - Array of settled promise results
80
+ * @returns {Array<unknown>} Array of rejection reasons
81
+ */
82
+ static reasons(settled: Array<{
83
+ status: "fulfilled" | "rejected";
84
+ value?: unknown;
85
+ reason?: unknown;
86
+ }>): Array<unknown>;
87
+ /**
88
+ * Extracts the values from fulfilled results in a settled promise array.
89
+ *
90
+ * @param {Array<{status: 'fulfilled'|'rejected', value?: unknown, reason?: unknown}>} settled - Array of settled promise results
91
+ * @returns {Array<unknown>} Array of fulfilled values
92
+ */
93
+ static values(settled: Array<{
94
+ status: "fulfilled" | "rejected";
95
+ value?: unknown;
96
+ reason?: unknown;
97
+ }>): Array<unknown>;
98
+ /**
99
+ * Throws a Tantrum containing all rejection reasons from settled promises.
100
+ *
101
+ * @param {string} message - Error message. Defaults to "GIGO"
102
+ * @param {Array<{status: 'fulfilled'|'rejected', value?: unknown, reason?: unknown}>} settled - Array of settled promise results
103
+ * @throws {Tantrum} Throws a Tantrum error with rejection reasons
104
+ */
105
+ static throw(message: string, settled: Array<{
106
+ status: "fulfilled" | "rejected";
107
+ value?: unknown;
108
+ reason?: unknown;
109
+ }>): void;
110
+ /**
111
+ * Returns the first promise to resolve or reject from an array of promises.
112
+ * Wrapper around Promise.race for consistency with other utility methods.
113
+ *
114
+ * @param {Array<Promise<unknown>>} promises - Array of promises to race
115
+ * @returns {Promise<unknown>} Result of the first settled promise
116
+ */
117
+ static race(promises: Array<Promise<unknown>>): Promise<unknown>;
118
+ }
119
+ //# sourceMappingURL=Promised.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Promised.d.ts","sourceRoot":"","sources":["../../../browser/lib/Promised.js"],"names":[],"mappings":"AAEA;;;GAGG;AACH;IACE;;;;;;OAMG;IACH,uBAHW,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,GACrB,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAInC;IAED;;;;;;OAMG;IACH,wBAHW,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,GACrB,OAAO,CAAC,KAAK,CAAC;QAAC,MAAM,EAAE,WAAW,GAAC,UAAU,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAC,CAAC,CAAC,CAI/F;IAED;;;;;OAKG;IACH,4BAHW,KAAK,CAAC;QAAC,MAAM,EAAE,WAAW,GAAC,UAAU,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAC,CAAC,GACxE,OAAO,CAInB;IAED;;;;;OAKG;IACH,6BAHW,KAAK,CAAC;QAAC,MAAM,EAAE,WAAW,GAAC,UAAU,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAC,CAAC,GACxE,OAAO,CAInB;IAED;;;;;OAKG;IACH,yBAHW,KAAK,CAAC;QAAC,MAAM,EAAE,WAAW,GAAC,UAAU,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAC,CAAC,GACxE,KAAK,CAAC;QAAC,MAAM,EAAE,UAAU,CAAC;QAAC,MAAM,EAAE,OAAO,CAAA;KAAC,CAAC,CAIxD;IAED;;;;;OAKG;IACH,yBAHW,KAAK,CAAC;QAAC,MAAM,EAAE,WAAW,GAAC,UAAU,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAC,CAAC,GACxE,KAAK,CAAC;QAAC,MAAM,EAAE,WAAW,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAC,CAAC,CAIxD;IAED;;;;;OAKG;IACH,wBAHW,KAAK,CAAC;QAAC,MAAM,EAAE,WAAW,GAAC,UAAU,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAC,CAAC,GACxE,KAAK,CAAC,OAAO,CAAC,CAO1B;IAED;;;;;OAKG;IACH,uBAHW,KAAK,CAAC;QAAC,MAAM,EAAE,WAAW,GAAC,UAAU,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAC,CAAC,GACxE,KAAK,CAAC,OAAO,CAAC,CAO1B;IAED;;;;;;OAMG;IACH,sBAJW,MAAM,WACN,KAAK,CAAC;QAAC,MAAM,EAAE,WAAW,GAAC,UAAU,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAC,CAAC,QAQpF;IAED;;;;;;OAMG;IACH,sBAHW,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,GACrB,OAAO,CAAC,OAAO,CAAC,CAI5B;CACF"}
@@ -39,73 +39,6 @@ export default class Util {
39
39
  * @returns {string} Padded string with text centred.
40
40
  */
41
41
  static centreAlignText(text: string | number, width?: number): string;
42
- /**
43
- * Asynchronously awaits all promises in parallel.
44
- * Wrapper around Promise.all for consistency with other utility methods.
45
- *
46
- * @param {Array<Promise<unknown>>} promises - Array of promises to await
47
- * @returns {Promise<Array<unknown>>} Results of all promises
48
- */
49
- static awaitAll(promises: Array<Promise<unknown>>): Promise<Array<unknown>>;
50
- /**
51
- * Settles all promises (both fulfilled and rejected) in parallel.
52
- * Wrapper around Promise.allSettled for consistency with other utility methods.
53
- *
54
- * @param {Array<Promise<unknown>>} promises - Array of promises to settle
55
- * @returns {Promise<Array<object>>} Results of all settled promises with status and value/reason
56
- */
57
- static settleAll(promises: Array<Promise<unknown>>): Promise<Array<object>>;
58
- /**
59
- * Checks if any result in the settled promise array is rejected.
60
- *
61
- * @param {Array<object>} result - Array of settled promise results.
62
- * @returns {boolean} True if any result is rejected, false otherwise.
63
- */
64
- static anyRejected(result: Array<object>): boolean;
65
- /**
66
- * Filters and returns all rejected results from a settled promise array.
67
- *
68
- * @param {Array<object>} result - Array of settled promise results.
69
- * @returns {Array<object>} Array of rejected results.
70
- */
71
- static settledAndRejected(result: Array<object>): Array<object>;
72
- /**
73
- * Extracts the rejection reasons from an array of rejected promise results.
74
- *
75
- * @param {Array<object>} rejected - Array of rejected results.
76
- * @returns {Array<unknown>} Array of rejection reasons.
77
- */
78
- static rejectedReasons(rejected: Array<object>): Array<unknown>;
79
- /**
80
- * Throws a Sass error containing all rejection reasons from settled promises.
81
- *
82
- * @param {string} [_message] - Optional error message. Defaults to "GIGO"
83
- * @param {Array<object>} rejected - Array of rejected results.
84
- * @throws {Error} Throws a Tantrum error with rejection reasons.
85
- */
86
- static throwRejected(message: string, settled: any): void;
87
- /**
88
- * Filters and returns all fulfilled results from a settled promise array.
89
- *
90
- * @param {Array<object>} result - Array of settled promise results.
91
- * @returns {Array<object>} Array of fulfilled results.
92
- */
93
- static settledAndFulfilled(result: Array<object>): Array<object>;
94
- /**
95
- * Extracts the values from all fulfilled results in a settled promise array.
96
- *
97
- * @param {Array<object>} result - Array of settled promise results.
98
- * @returns {Array<unknown>} Array of fulfilled values.
99
- */
100
- static fulfilledValues(result: Array<object>): Array<unknown>;
101
- /**
102
- * Returns the first promise to resolve or reject from an array of promises.
103
- * Wrapper around Promise.race for consistency with other utility methods.
104
- *
105
- * @param {Array<Promise<unknown>>} promises - Array of promises to race
106
- * @returns {Promise<unknown>} Result of the first settled promise
107
- */
108
- static race(promises: Array<Promise<unknown>>): Promise<unknown>;
109
42
  /**
110
43
  * Determine the Levenshtein distance between two string values
111
44
  *
@@ -1 +1 @@
1
- {"version":3,"file":"Util.d.ts","sourceRoot":"","sources":["../../../browser/lib/Util.js"],"names":[],"mappings":"AAIA;;;GAGG;AACH;IACE;;;;;OAKG;IACH,wBAHW,MAAM,GACJ,MAAM,CAYlB;IAED;;;;;;OAMG;IACH,YAJa,CAAC,MACH,MAAM,OAAO,CAAC,CAAC,CAAC,GACd,OAAO,CAAC;QAAC,MAAM,EAAE,CAAC,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAC,CAAC,CAQ9C;IAED;;;;;;;OAOG;IACH,4BAJW,MAAM,GAAC,MAAM,UACb,MAAM,GACJ,MAAM,CAWlB;IAED;;;;;;;OAOG;IACH,6BAJW,MAAM,GAAC,MAAM,UACb,MAAM,GACJ,MAAM,CAalB;IAED;;;;;;OAMG;IACH,0BAHW,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,GACrB,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAInC;IAED;;;;;;OAMG;IACH,2BAHW,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,GACrB,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAIlC;IAED;;;;;OAKG;IACH,2BAHW,KAAK,CAAC,MAAM,CAAC,GACX,OAAO,CAInB;IAED;;;;;OAKG;IACH,kCAHW,KAAK,CAAC,MAAM,CAAC,GACX,KAAK,CAAC,MAAM,CAAC,CAIzB;IAED;;;;;OAKG;IACH,iCAHW,KAAK,CAAC,MAAM,CAAC,GACX,KAAK,CAAC,OAAO,CAAC,CAI1B;IAED;;;;;;OAMG;IACH,0DAKC;IAED;;;;;OAKG;IACH,mCAHW,KAAK,CAAC,MAAM,CAAC,GACX,KAAK,CAAC,MAAM,CAAC,CAIzB;IAED;;;;;OAKG;IACH,+BAHW,KAAK,CAAC,MAAM,CAAC,GACX,KAAK,CAAC,OAAO,CAAC,CAI1B;IAED;;;;;;OAMG;IACH,sBAHW,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,GACrB,OAAO,CAAC,OAAO,CAAC,CAI5B;IAED;;;;;;OAMG;IACH,8BAJW,MAAM,KACN,MAAM,GACJ,MAAM,CAsBlB;IAED;;;;;;;;OAQG;IACH,+BALW,MAAM,iBACN,KAAK,CAAC,MAAM,CAAC,cACb,MAAM,GACJ,MAAM,CAwBlB;IAED,mEAiBC;CACF"}
1
+ {"version":3,"file":"Util.d.ts","sourceRoot":"","sources":["../../../browser/lib/Util.js"],"names":[],"mappings":"AAGA;;;GAGG;AACH;IACE;;;;;OAKG;IACH,wBAHW,MAAM,GACJ,MAAM,CAYlB;IAED;;;;;;OAMG;IACH,YAJa,CAAC,MACH,MAAM,OAAO,CAAC,CAAC,CAAC,GACd,OAAO,CAAC;QAAC,MAAM,EAAE,CAAC,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAC,CAAC,CAQ9C;IAED;;;;;;;OAOG;IACH,4BAJW,MAAM,GAAC,MAAM,UACb,MAAM,GACJ,MAAM,CAWlB;IAED;;;;;;;OAOG;IACH,6BAJW,MAAM,GAAC,MAAM,UACb,MAAM,GACJ,MAAM,CAalB;IAED;;;;;;OAMG;IACH,8BAJW,MAAM,KACN,MAAM,GACJ,MAAM,CAsBlB;IAED;;;;;;;;OAQG;IACH,+BALW,MAAM,iBACN,KAAK,CAAC,MAAM,CAAC,cACb,MAAM,GACJ,MAAM,CAwBlB;IAED,mEAiBC;CACF"}
@@ -1,5 +1,6 @@
1
1
  export { default as Collection } from "./browser/lib/Collection.js";
2
2
  export { default as Data } from "./browser/lib/Data.js";
3
+ export { default as Promised } from "./browser/lib/Promised.js";
3
4
  export { default as Type } from "./browser/lib/TypeSpec.js";
4
5
  export { default as Valid } from "./lib/Valid.js";
5
6
  export { default as Sass } from "./lib/Sass.js";
@@ -38,6 +38,32 @@ export default class CappedDirectoryObject extends DirectoryObject {
38
38
  * @returns {boolean} Always true for CappedDirectoryObject instances
39
39
  */
40
40
  get capped(): boolean;
41
+ /**
42
+ * Returns the real filesystem path (for internal and subclass use).
43
+ *
44
+ * @protected
45
+ * @returns {string} The actual filesystem path
46
+ */
47
+ protected get realPath(): string;
48
+ /**
49
+ * Returns a plain DirectoryObject representing the actual filesystem location.
50
+ * This provides an "escape hatch" from the capped environment to interact
51
+ * with the real filesystem when needed.
52
+ *
53
+ * @returns {DirectoryObject} Uncapped directory object at the real filesystem path
54
+ * @example
55
+ * const temp = new TempDirectoryObject("myapp")
56
+ * const subdir = temp.getDirectory("data")
57
+ *
58
+ * // Work within the capped environment (virtual paths)
59
+ * console.log(subdir.path) // "/data" (virtual)
60
+ * subdir.getFile("config.json") // Stays within cap
61
+ *
62
+ * // Break out to real filesystem when needed
63
+ * console.log(subdir.real.path) // "/tmp/myapp-ABC123/data" (real)
64
+ * subdir.real.parent // Can traverse outside the cap
65
+ */
66
+ get real(): DirectoryObject;
41
67
  /**
42
68
  * Returns a generator that walks up to the cap.
43
69
  *
@@ -47,20 +73,41 @@ export default class CappedDirectoryObject extends DirectoryObject {
47
73
  /**
48
74
  * Creates a new CappedDirectoryObject by extending this directory's path.
49
75
  *
50
- * Validates that the resulting path remains within the cap directory tree.
76
+ * All paths are coerced to remain within the cap directory tree:
77
+ * - Absolute paths (e.g., "/foo") are treated as relative to the cap
78
+ * - Parent traversal ("..") is allowed but clamped at the cap boundary
79
+ * - The cap acts as the virtual root directory
51
80
  *
52
- * @param {string} newPath - The path segment to append
53
- * @returns {CappedDirectoryObject} A new CappedDirectoryObject with the extended path
54
- * @throws {Sass} If the path would escape the cap directory
55
- * @throws {Sass} If the path is absolute
56
- * @throws {Sass} If the path contains traversal (..)
81
+ * @param {string} newPath - The path to resolve (can be absolute or contain ..)
82
+ * @returns {CappedDirectoryObject} A new CappedDirectoryObject with the coerced path
57
83
  * @example
58
84
  * const capped = new TempDirectoryObject("myapp")
59
85
  * const subDir = capped.getDirectory("data")
60
86
  * console.log(subDir.path) // "/tmp/myapp-ABC123/data"
87
+ *
88
+ * @example
89
+ * // Absolute paths are relative to cap
90
+ * const abs = capped.getDirectory("/foo/bar")
91
+ * console.log(abs.path) // "/tmp/myapp-ABC123/foo/bar"
92
+ *
93
+ * @example
94
+ * // Excessive .. traversal clamps to cap
95
+ * const up = capped.getDirectory("../../../etc/passwd")
96
+ * console.log(up.path) // "/tmp/myapp-ABC123" (clamped to cap)
61
97
  */
62
98
  getDirectory(newPath: string): CappedDirectoryObject;
99
+ /**
100
+ * Override read to use real filesystem path and return capped objects.
101
+ *
102
+ * @param {string} [pat=""] - Optional glob pattern
103
+ * @returns {Promise<{files: Array<FileObject>, directories: Array}>} Directory contents
104
+ */
105
+ read(pat?: string): Promise<{
106
+ files: Array<FileObject>;
107
+ directories: any[];
108
+ }>;
63
109
  #private;
64
110
  }
65
111
  import DirectoryObject from "./DirectoryObject.js";
112
+ import FileObject from "./FileObject.js";
66
113
  //# sourceMappingURL=CappedDirectoryObject.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"CappedDirectoryObject.d.ts","sourceRoot":"","sources":["../../lib/CappedDirectoryObject.js"],"names":[],"mappings":"AAiBA;;;;;;;;GAQG;AACH;IAGE;;;;;;;;;;;;;;;;OAgBG;IACH,kBAXW,MAAM,OAAC,OACP,MAAM,WACN,qBAAqB,OAAC,cACtB,OAAO,EAkFjB;IAqBD;;;;OAIG;IACH,WAFa,MAAM,CAIlB;IAED;;;;OAIG;IACH,cAFa,OAAO,CAInB;IA8DD;;;;OAIG;IACH,cAFa,SAAS,CAAC,eAAe,CAAC,CAItC;IAED;;;;;;;;;;;;;;OAcG;IACH,sBAVW,MAAM,GACJ,qBAAqB,CA0BjC;;CA4CF;4BAnS2B,sBAAsB"}
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"}
@@ -50,9 +50,11 @@ export default class FileObject extends FS {
50
50
  */
51
51
  get supplied(): string;
52
52
  /**
53
- * Return the fully resolved absolute path to the file on disk.
53
+ * Returns the file path. If the parent is a capped directory, returns the
54
+ * virtual path relative to the cap. Otherwise returns the real filesystem path.
55
+ * Use `.real.path` to always get the actual filesystem path.
54
56
  *
55
- * @returns {string} The fully resolved absolute file path
57
+ * @returns {string} The file path (virtual if parent is capped, real otherwise)
56
58
  */
57
59
  get path(): string;
58
60
  /**
@@ -107,6 +109,23 @@ export default class FileObject extends FS {
107
109
  * @returns {DirectoryObject} The parent directory object
108
110
  */
109
111
  get parent(): DirectoryObject;
112
+ /**
113
+ * Returns a plain FileObject representing the actual filesystem location.
114
+ * This provides an "escape hatch" when working with capped directories,
115
+ * allowing direct filesystem access when needed.
116
+ *
117
+ * @returns {FileObject} Uncapped file object at the real filesystem path
118
+ * @example
119
+ * const temp = new TempDirectoryObject("myapp")
120
+ * const file = temp.getFile("/config/app.json")
121
+ *
122
+ * // file.path shows virtual path
123
+ * console.log(file.path) // "/config/app.json"
124
+ * // file.real.path shows actual filesystem path
125
+ * console.log(file.real.path) // "/tmp/myapp-ABC123/config/app.json"
126
+ * file.real.parent.parent // Can traverse outside the cap
127
+ */
128
+ get real(): FileObject;
110
129
  /**
111
130
  * Check if a file can be read. Returns true if the file can be read, false
112
131
  *
@@ -1 +1 @@
1
- {"version":3,"file":"FileObject.d.ts","sourceRoot":"","sources":["../../lib/FileObject.js"],"names":[],"mappings":"AAmBA;;;;;;;;;;;;;;GAcG;AAEH;uBAgIe,MAAM;IA/HnB;;;;;OAKG;IACH,yBAFU;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,CAAC,OAAO,KAAK,GAAG,OAAO,IAAI,CAAC,CAAA;KAAC,CAO1D;IA2BF;;;;;OAKG;IACH,sBAHW,MAAM,GAAG,UAAU,WACnB,eAAe,GAAC,MAAM,GAAC,IAAI,EAoDrC;IAWD;;;;OAIG;IACH,UAFa,MAAM,CAclB;IAWD;;;;OAIG;IACH,cAFa,OAAO,CAAC,OAAO,CAAC,CAI5B;IAED;;;;OAIG;IACH,gBAFa,MAAM,CAIlB;IAED;;;;OAIG;IACH,YAFa,MAAM,CAIlB;IAED;;;;OAIG;IACH,WAFa,GAAG,CAIf;IAED;;;;OAIG;IACH,YAFa,MAAM,CAIlB;IAED;;;;OAIG;IACH,cAFa,MAAM,CAIlB;IAED;;;;OAIG;IACH,iBAFa,MAAM,CAIlB;IACD;;;;OAIG;IACH,cAFa,OAAO,CAInB;IAED;;;;OAIG;IACH,mBAFa,OAAO,CAInB;IAED;;;;;;;;;;;;;;OAcG;IACH,cAFa,eAAe,CAI3B;IAED;;;;OAIG;IACH,WAFa,OAAO,CAAC,OAAO,CAAC,CAU5B;IAED;;;;OAIG;IACH,YAFa,OAAO,CAAC,OAAO,CAAC,CAU5B;IAiBD;;;;OAIG;IACH,QAFa,OAAO,CAAC,MAAM,OAAC,CAAC,CAU5B;IAED;;;;;OAKG;IACH,YAFa,OAAO,CAAC,IAAI,OAAC,CAAC,CAU1B;IAsBD;;;;;OAKG;IACH,gBAHW,MAAM,GACJ,OAAO,CAAC,MAAM,CAAC,CAY3B;IAED;;;;;;;;;;;OAWG;IACH,cARa,OAAO,CAAC,MAAM,CAAC,CAkB3B;IAED;;;;;;;;;;;OAWG;IACH,eARW,MAAM,aACN,MAAM,GACJ,OAAO,CAAC,IAAI,CAAC,CAezB;IAED;;;;;;;;;;;;;;;OAeG;IACH,kBAXW,WAAW,GAAC,IAAI,GAAC,MAAM,GACrB,OAAO,CAAC,IAAI,CAAC,CAyBzB;IAED;;;;;;;;;;;;;;OAcG;IACH,gBAXW,MAAM,aACN,MAAM,GACJ,OAAO,CAAC,OAAO,CAAC,CAkC5B;IAED;;;;OAIG;IACH,UAFa,OAAO,CAAC,MAAM,CAAC,CAY3B;IAED;;;;;;;;;OASG;IACH,UAPa,OAAO,CAAC,IAAI,CAAC,CAiBzB;;CACF;eA/gBc,SAAS;4BADI,sBAAsB;kBARhC,OAAO;iBAIR,MAAM"}
1
+ {"version":3,"file":"FileObject.d.ts","sourceRoot":"","sources":["../../lib/FileObject.js"],"names":[],"mappings":"AAmBA;;;;;;;;;;;;;;GAcG;AAEH;uBAmIe,MAAM;IAlInB;;;;;OAKG;IACH,yBAFU;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,CAAC,OAAO,KAAK,GAAG,OAAO,IAAI,CAAC,CAAA;KAAC,CAO1D;IA2BF;;;;;OAKG;IACH,sBAHW,MAAM,GAAG,UAAU,WACnB,eAAe,GAAC,MAAM,GAAC,IAAI,EAuDrC;IAWD;;;;OAIG;IACH,UAFa,MAAM,CAclB;IAWD;;;;OAIG;IACH,cAFa,OAAO,CAAC,OAAO,CAAC,CAI5B;IAED;;;;OAIG;IACH,gBAFa,MAAM,CAIlB;IAED;;;;;;OAMG;IACH,YAFa,MAAM,CAkBlB;IAED;;;;OAIG;IACH,WAFa,GAAG,CAIf;IAED;;;;OAIG;IACH,YAFa,MAAM,CAIlB;IAED;;;;OAIG;IACH,cAFa,MAAM,CAIlB;IAED;;;;OAIG;IACH,iBAFa,MAAM,CAIlB;IACD;;;;OAIG;IACH,cAFa,OAAO,CAInB;IAED;;;;OAIG;IACH,mBAFa,OAAO,CAInB;IAED;;;;;;;;;;;;;;OAcG;IACH,cAFa,eAAe,CAI3B;IAED;;;;;;;;;;;;;;;OAeG;IACH,YAXa,UAAU,CAatB;IAED;;;;OAIG;IACH,WAFa,OAAO,CAAC,OAAO,CAAC,CAU5B;IAED;;;;OAIG;IACH,YAFa,OAAO,CAAC,OAAO,CAAC,CAU5B;IAiBD;;;;OAIG;IACH,QAFa,OAAO,CAAC,MAAM,OAAC,CAAC,CAU5B;IAED;;;;;OAKG;IACH,YAFa,OAAO,CAAC,IAAI,OAAC,CAAC,CAU1B;IAsBD;;;;;OAKG;IACH,gBAHW,MAAM,GACJ,OAAO,CAAC,MAAM,CAAC,CAY3B;IAED;;;;;;;;;;;OAWG;IACH,cARa,OAAO,CAAC,MAAM,CAAC,CAkB3B;IAED;;;;;;;;;;;OAWG;IACH,eARW,MAAM,aACN,MAAM,GACJ,OAAO,CAAC,IAAI,CAAC,CAezB;IAED;;;;;;;;;;;;;;;OAeG;IACH,kBAXW,WAAW,GAAC,IAAI,GAAC,MAAM,GACrB,OAAO,CAAC,IAAI,CAAC,CAyBzB;IAED;;;;;;;;;;;;;;OAcG;IACH,gBAXW,MAAM,aACN,MAAM,GACJ,OAAO,CAAC,OAAO,CAAC,CAkC5B;IAED;;;;OAIG;IACH,UAFa,OAAO,CAAC,MAAM,CAAC,CAY3B;IAED;;;;;;;;;OASG;IACH,UAPa,OAAO,CAAC,IAAI,CAAC,CAiBzB;;CACF;eAtjBc,SAAS;4BADI,sBAAsB;kBARhC,OAAO;iBAIR,MAAM"}