@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.
- package/package.json +3 -3
- package/src/browser/lib/Data.js +89 -5
- package/src/browser/lib/TypeSpec.js +3 -1
- package/src/lib/CappedDirectoryObject.js +62 -484
- package/src/lib/DirectoryObject.js +86 -170
- package/src/lib/FS.js +221 -70
- package/src/lib/FileObject.js +80 -111
- package/src/lib/TempDirectoryObject.js +92 -141
- package/src/lib/Valid.js +1 -1
- package/src/types/browser/lib/Data.d.ts +46 -2
- package/src/types/browser/lib/Data.d.ts.map +1 -1
- package/src/types/browser/lib/TypeSpec.d.ts.map +1 -1
- package/src/types/lib/CappedDirectoryObject.d.ts +30 -55
- package/src/types/lib/CappedDirectoryObject.d.ts.map +1 -1
- package/src/types/lib/DirectoryObject.d.ts +8 -60
- package/src/types/lib/DirectoryObject.d.ts.map +1 -1
- package/src/types/lib/FS.d.ts +115 -15
- package/src/types/lib/FS.d.ts.map +1 -1
- package/src/types/lib/FileObject.d.ts +6 -10
- package/src/types/lib/FileObject.d.ts.map +1 -1
- package/src/types/lib/TempDirectoryObject.d.ts +15 -62
- package/src/types/lib/TempDirectoryObject.d.ts.map +1 -1
package/src/lib/FileObject.js
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
import JSON5 from "json5"
|
|
8
8
|
import fs from "node:fs/promises"
|
|
9
9
|
import path from "node:path"
|
|
10
|
-
import util from "node:util"
|
|
11
10
|
import YAML from "yaml"
|
|
12
11
|
import {URL} from "node:url"
|
|
13
12
|
|
|
@@ -70,65 +69,61 @@ export default class FileObject extends FS {
|
|
|
70
69
|
isFile: true,
|
|
71
70
|
isDirectory: false,
|
|
72
71
|
parent: null,
|
|
72
|
+
parentPath: null,
|
|
73
73
|
})
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
76
|
* Constructs a FileObject instance.
|
|
77
77
|
*
|
|
78
|
-
* @param {string
|
|
78
|
+
* @param {string} fileName - The file path
|
|
79
79
|
* @param {DirectoryObject|string|null} [parent] - The parent directory (object or string)
|
|
80
80
|
*/
|
|
81
81
|
constructor(fileName, parent=null) {
|
|
82
82
|
super()
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
fileName = fileName.path
|
|
87
|
-
|
|
88
|
-
if(!fileName || typeof fileName !== "string" || fileName.length === 0)
|
|
89
|
-
throw Sass.new("fileName must be a non-empty string")
|
|
84
|
+
Valid.type(fileName, "String", {allowEmpty: false})
|
|
85
|
+
Valid.type(parent, "Null|String|DirectoryObject", {allowEmpty: false})
|
|
90
86
|
|
|
91
87
|
const fixedFile = FS.fixSlashes(fileName)
|
|
92
|
-
const {dir,base,ext} =
|
|
88
|
+
const {dir, base, ext} = FS.pathParts(fixedFile)
|
|
93
89
|
|
|
94
90
|
const parentObject = (() => {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return new DirectoryObject(parent)
|
|
98
|
-
case "DirectoryObject":
|
|
99
|
-
case "CappedDirectoryObject":
|
|
100
|
-
case "TempDirectoryObject":
|
|
101
|
-
return parent
|
|
102
|
-
default:
|
|
103
|
-
return new DirectoryObject(dir)
|
|
104
|
-
}
|
|
105
|
-
})()
|
|
91
|
+
if(Data.isType(parent, "String"))
|
|
92
|
+
return new DirectoryObject(parent)
|
|
106
93
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const final = FS.resolvePath(parentPath ?? ".", fixedFile)
|
|
94
|
+
if(Data.isType(parent, "DirectoryObject"))
|
|
95
|
+
return parent
|
|
110
96
|
|
|
111
|
-
|
|
112
|
-
|
|
97
|
+
return new DirectoryObject(dir)
|
|
98
|
+
})()
|
|
113
99
|
|
|
114
|
-
//
|
|
115
|
-
|
|
100
|
+
// If the parent is passed, we need to treat the fileName as relative,
|
|
101
|
+
// regardless of what you-know-who says.
|
|
102
|
+
const resolvedFilename = parent
|
|
103
|
+
? FS.absoluteToRelative(fixedFile, true)
|
|
104
|
+
: fixedFile
|
|
116
105
|
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
? parentObject
|
|
123
|
-
: new DirectoryObject(actualParentPath)
|
|
106
|
+
// Use real path if parent is capped, otherwise use path
|
|
107
|
+
const parentPath = parentObject.real?.path || parentObject.path
|
|
108
|
+
const resolved = FS.resolvePath(parentPath ?? ".", resolvedFilename)
|
|
109
|
+
const {dir: actualParent} = FS.pathParts(resolved)
|
|
110
|
+
const url = new URL(FS.pathToUrl(resolved))
|
|
124
111
|
|
|
125
|
-
this.#meta.supplied =
|
|
112
|
+
this.#meta.supplied = fileName
|
|
126
113
|
this.#meta.path = resolved
|
|
127
114
|
this.#meta.url = url
|
|
128
115
|
this.#meta.name = base
|
|
129
116
|
this.#meta.extension = ext
|
|
130
117
|
this.#meta.module = path.basename(this.supplied, this.extension)
|
|
131
|
-
this.#meta.
|
|
118
|
+
this.#meta.parentPath = actualParent
|
|
119
|
+
// Preserve capped parent or use actualParent path match
|
|
120
|
+
const useCappedParent =
|
|
121
|
+
parentObject.isCapped ||
|
|
122
|
+
FS.fixSlashes(actualParent) === FS.fixSlashes(parentObject.path)
|
|
123
|
+
|
|
124
|
+
this.#meta.parent = useCappedParent
|
|
125
|
+
? parentObject
|
|
126
|
+
: new DirectoryObject(actualParent)
|
|
132
127
|
|
|
133
128
|
Object.freeze(this.#meta)
|
|
134
129
|
}
|
|
@@ -139,35 +134,9 @@ export default class FileObject extends FS {
|
|
|
139
134
|
* @returns {string} string representation of the FileObject
|
|
140
135
|
*/
|
|
141
136
|
toString() {
|
|
142
|
-
return
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Returns a JSON representation of the FileObject.
|
|
147
|
-
*
|
|
148
|
-
* @returns {object} JSON representation of the FileObject
|
|
149
|
-
*/
|
|
150
|
-
toJSON() {
|
|
151
|
-
return {
|
|
152
|
-
supplied: this.supplied,
|
|
153
|
-
path: this.path,
|
|
154
|
-
url: this.url.toString(),
|
|
155
|
-
name: this.name,
|
|
156
|
-
module: this.module,
|
|
157
|
-
extension: this.extension,
|
|
158
|
-
isFile: this.isFile,
|
|
159
|
-
isDirectory: this.isDirectory,
|
|
160
|
-
parent: this.parent ? this.parent.path : null
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Custom inspect method for Node.js console.
|
|
166
|
-
*
|
|
167
|
-
* @returns {object} JSON representation of this object.
|
|
168
|
-
*/
|
|
169
|
-
[util.inspect.custom]() {
|
|
170
|
-
return this.toJSON()
|
|
137
|
+
return this.parent.isCapped
|
|
138
|
+
?`[${this.constructor.name}: ${this.path} β ${this.real.path}]`
|
|
139
|
+
:`[${this.constructor.name}: ${this.path}]`
|
|
171
140
|
}
|
|
172
141
|
|
|
173
142
|
/**
|
|
@@ -190,7 +159,9 @@ export default class FileObject extends FS {
|
|
|
190
159
|
|
|
191
160
|
/**
|
|
192
161
|
* 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
|
|
162
|
+
* virtual path relative to the cap. Otherwise returns the real filesystem
|
|
163
|
+
* path.
|
|
164
|
+
*
|
|
194
165
|
* Use `.real.path` to always get the actual filesystem path.
|
|
195
166
|
*
|
|
196
167
|
* @returns {string} The file path (virtual if parent is capped, real otherwise)
|
|
@@ -200,13 +171,14 @@ export default class FileObject extends FS {
|
|
|
200
171
|
const parent = this.#meta.parent
|
|
201
172
|
|
|
202
173
|
// If parent is capped, return virtual path
|
|
203
|
-
if(parent?.
|
|
204
|
-
const cap = parent.cap
|
|
174
|
+
if(parent?.isCapped) {
|
|
175
|
+
const cap = parent.cap.real.path
|
|
205
176
|
const capResolved = path.resolve(cap)
|
|
206
|
-
const
|
|
177
|
+
const relativeRealPath = FS.absoluteToRelative(realPath)
|
|
178
|
+
const absolute = FS.resolvePath(capResolved, relativeRealPath)
|
|
207
179
|
|
|
208
180
|
// Return with leading slash to indicate it's cap-relative
|
|
209
|
-
return
|
|
181
|
+
return FS.absoluteToRelative(absolute)
|
|
210
182
|
}
|
|
211
183
|
|
|
212
184
|
// Otherwise return real path
|
|
@@ -220,12 +192,9 @@ export default class FileObject extends FS {
|
|
|
220
192
|
* @returns {URL} The file URL (virtual if parent is capped, real otherwise)
|
|
221
193
|
*/
|
|
222
194
|
get url() {
|
|
223
|
-
const parent = this.#meta.parent
|
|
224
|
-
|
|
225
195
|
// If parent is capped, return virtual URL
|
|
226
|
-
if(parent?.
|
|
227
|
-
return new URL(FS.
|
|
228
|
-
}
|
|
196
|
+
if(this.parent?.isCapped)
|
|
197
|
+
return new URL(FS.pathToUrl(this.path))
|
|
229
198
|
|
|
230
199
|
return this.#meta.url
|
|
231
200
|
}
|
|
@@ -293,6 +262,10 @@ export default class FileObject extends FS {
|
|
|
293
262
|
return this.#meta.parent
|
|
294
263
|
}
|
|
295
264
|
|
|
265
|
+
get parentPath() {
|
|
266
|
+
return this.#meta.parentPath
|
|
267
|
+
}
|
|
268
|
+
|
|
296
269
|
/**
|
|
297
270
|
* Returns a plain FileObject representing the actual filesystem location.
|
|
298
271
|
* This provides an "escape hatch" when working with capped directories,
|
|
@@ -310,7 +283,7 @@ export default class FileObject extends FS {
|
|
|
310
283
|
* file.real.parent.parent // Can traverse outside the cap
|
|
311
284
|
*/
|
|
312
285
|
get real() {
|
|
313
|
-
return new FileObject(this
|
|
286
|
+
return new FileObject(this.path)
|
|
314
287
|
}
|
|
315
288
|
|
|
316
289
|
/**
|
|
@@ -323,7 +296,7 @@ export default class FileObject extends FS {
|
|
|
323
296
|
await fs.access(this.#meta.path, fs.constants.R_OK)
|
|
324
297
|
|
|
325
298
|
return true
|
|
326
|
-
} catch
|
|
299
|
+
} catch {
|
|
327
300
|
return false
|
|
328
301
|
}
|
|
329
302
|
}
|
|
@@ -338,7 +311,7 @@ export default class FileObject extends FS {
|
|
|
338
311
|
await fs.access(this.#meta.path, fs.constants.W_OK)
|
|
339
312
|
|
|
340
313
|
return true
|
|
341
|
-
} catch
|
|
314
|
+
} catch {
|
|
342
315
|
return false
|
|
343
316
|
}
|
|
344
317
|
}
|
|
@@ -353,7 +326,7 @@ export default class FileObject extends FS {
|
|
|
353
326
|
await fs.access(this.#meta.path, fs.constants.F_OK)
|
|
354
327
|
|
|
355
328
|
return true
|
|
356
|
-
} catch
|
|
329
|
+
} catch {
|
|
357
330
|
return false
|
|
358
331
|
}
|
|
359
332
|
}
|
|
@@ -368,7 +341,7 @@ export default class FileObject extends FS {
|
|
|
368
341
|
const stat = await fs.stat(this.#meta.path)
|
|
369
342
|
|
|
370
343
|
return stat.size
|
|
371
|
-
} catch
|
|
344
|
+
} catch {
|
|
372
345
|
return null
|
|
373
346
|
}
|
|
374
347
|
}
|
|
@@ -384,31 +357,11 @@ export default class FileObject extends FS {
|
|
|
384
357
|
const stat = await fs.stat(this.#meta.path)
|
|
385
358
|
|
|
386
359
|
return stat.mtime
|
|
387
|
-
} catch
|
|
360
|
+
} catch {
|
|
388
361
|
return null
|
|
389
362
|
}
|
|
390
363
|
}
|
|
391
364
|
|
|
392
|
-
/**
|
|
393
|
-
* @typedef {object} FileParts
|
|
394
|
-
* @property {string} base - The file name with extension
|
|
395
|
-
* @property {string} dir - The directory path
|
|
396
|
-
* @property {string} ext - The file extension (including dot)
|
|
397
|
-
*/
|
|
398
|
-
|
|
399
|
-
/**
|
|
400
|
-
* Deconstruct a filename into parts
|
|
401
|
-
*
|
|
402
|
-
* @param {string} fileName - The filename to deconstruct
|
|
403
|
-
* @returns {FileParts} The filename parts
|
|
404
|
-
*/
|
|
405
|
-
#deconstructFilenameToParts(fileName) {
|
|
406
|
-
Valid.assert(typeof fileName === "string" && fileName.length > 0,
|
|
407
|
-
"file must be a non-zero length string", 1)
|
|
408
|
-
|
|
409
|
-
return path.parse(fileName)
|
|
410
|
-
}
|
|
411
|
-
|
|
412
365
|
/**
|
|
413
366
|
* Reads the content of a file asynchronously.
|
|
414
367
|
*
|
|
@@ -464,14 +417,30 @@ export default class FileObject extends FS {
|
|
|
464
417
|
* await file.write(JSON.stringify({key: 'value'}))
|
|
465
418
|
*/
|
|
466
419
|
async write(content, encoding="utf8") {
|
|
467
|
-
|
|
468
|
-
|
|
420
|
+
const realPath = FS.virtualToRealPath(this)
|
|
421
|
+
if(!realPath)
|
|
422
|
+
throw Sass.new("No actual disk location detected.")
|
|
469
423
|
|
|
470
|
-
|
|
471
|
-
|
|
424
|
+
// On Windows, normalize the parent directory path to handle 8.3 short names
|
|
425
|
+
let pathToWrite = realPath
|
|
426
|
+
if(process.platform === "win32") {
|
|
427
|
+
try {
|
|
428
|
+
const parentPath = path.dirname(realPath)
|
|
429
|
+
const normalizedParent = await fs.realpath(parentPath)
|
|
430
|
+
pathToWrite = path.join(normalizedParent, path.basename(realPath))
|
|
431
|
+
} catch {
|
|
432
|
+
// If normalization fails, use original path
|
|
433
|
+
}
|
|
434
|
+
}
|
|
472
435
|
|
|
473
|
-
|
|
474
|
-
|
|
436
|
+
try {
|
|
437
|
+
await fs.writeFile(pathToWrite, content, encoding)
|
|
438
|
+
} catch(error) {
|
|
439
|
+
if(error.code === "ENOENT")
|
|
440
|
+
throw Sass.new(`Invalid directory: ${path.dirname(pathToWrite)}`)
|
|
441
|
+
|
|
442
|
+
throw Sass.from(error, "Failed to write file")
|
|
443
|
+
}
|
|
475
444
|
}
|
|
476
445
|
|
|
477
446
|
/**
|
|
@@ -491,7 +460,7 @@ export default class FileObject extends FS {
|
|
|
491
460
|
* await file.writeBinary(buffer)
|
|
492
461
|
*/
|
|
493
462
|
async writeBinary(data) {
|
|
494
|
-
if(!this
|
|
463
|
+
if(!this.url)
|
|
495
464
|
throw Sass.new("No URL in file")
|
|
496
465
|
|
|
497
466
|
const exists = await this.parent.exists
|
|
@@ -504,7 +473,7 @@ export default class FileObject extends FS {
|
|
|
504
473
|
|
|
505
474
|
// According to the internet, if it's already binary, I don't need
|
|
506
475
|
// an encoding. π€·
|
|
507
|
-
return await fs.writeFile(this
|
|
476
|
+
return await fs.writeFile(this.url, bufferData)
|
|
508
477
|
}
|
|
509
478
|
|
|
510
479
|
/**
|
|
@@ -555,7 +524,7 @@ export default class FileObject extends FS {
|
|
|
555
524
|
* @returns {Promise<object>} The file contents as a module.
|
|
556
525
|
*/
|
|
557
526
|
async import() {
|
|
558
|
-
const url = this
|
|
527
|
+
const url = this.url
|
|
559
528
|
|
|
560
529
|
if(!url)
|
|
561
530
|
throw Sass.new("No URL in file map")
|
|
@@ -577,7 +546,7 @@ export default class FileObject extends FS {
|
|
|
577
546
|
* await file.delete()
|
|
578
547
|
*/
|
|
579
548
|
async delete() {
|
|
580
|
-
const url = this
|
|
549
|
+
const url = this.url
|
|
581
550
|
|
|
582
551
|
if(!url)
|
|
583
552
|
throw Sass.new("This object does not represent a valid resource.")
|
|
@@ -4,13 +4,16 @@
|
|
|
4
4
|
* to the OS's temporary directory tree.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import fs from "node:fs"
|
|
7
|
+
import fs, {mkdirSync, mkdtempSync} from "node:fs"
|
|
8
8
|
import os from "node:os"
|
|
9
9
|
import path from "node:path"
|
|
10
10
|
|
|
11
|
-
import CappedDirectoryObject from "./CappedDirectoryObject.js"
|
|
12
11
|
import Data from "../browser/lib/Data.js"
|
|
12
|
+
import CappedDirectoryObject from "./CappedDirectoryObject.js"
|
|
13
|
+
import DirectoryObject from "./DirectoryObject.js"
|
|
14
|
+
import FS from "./FS.js"
|
|
13
15
|
import Sass from "./Sass.js"
|
|
16
|
+
import Valid from "./Valid.js"
|
|
14
17
|
|
|
15
18
|
/**
|
|
16
19
|
* TempDirectoryObject extends CappedDirectoryObject with the cap set to
|
|
@@ -23,6 +26,8 @@ import Sass from "./Sass.js"
|
|
|
23
26
|
* @augments CappedDirectoryObject
|
|
24
27
|
*/
|
|
25
28
|
export default class TempDirectoryObject extends CappedDirectoryObject {
|
|
29
|
+
#tmpReal
|
|
30
|
+
#tmpCap
|
|
26
31
|
|
|
27
32
|
/**
|
|
28
33
|
* Constructs a TempDirectoryObject instance and creates the directory.
|
|
@@ -34,7 +39,7 @@ export default class TempDirectoryObject extends CappedDirectoryObject {
|
|
|
34
39
|
* is provided without a parent, creates a new directory with a unique suffix.
|
|
35
40
|
* If a parent is provided, creates a subdirectory within that parent.
|
|
36
41
|
*
|
|
37
|
-
* @param {string?} [
|
|
42
|
+
* @param {string?} [directory] - Base name for the temp directory (if empty/null, uses OS temp dir)
|
|
38
43
|
* @param {TempDirectoryObject?} [parent] - Optional parent temporary directory
|
|
39
44
|
* @throws {Sass} If name is absolute
|
|
40
45
|
* @throws {Sass} If name is empty (when parent is provided)
|
|
@@ -43,172 +48,118 @@ export default class TempDirectoryObject extends CappedDirectoryObject {
|
|
|
43
48
|
* @throws {Sass} If parent's lineage does not trace back to the OS temp directory
|
|
44
49
|
* @throws {Sass} If directory creation fails
|
|
45
50
|
* @example
|
|
46
|
-
* // Use OS temp directory directly
|
|
47
|
-
* const temp = new TempDirectoryObject()
|
|
48
|
-
* console.log(temp.path) // "/tmp"
|
|
49
|
-
*
|
|
50
|
-
* @example
|
|
51
51
|
* // Create with unique name
|
|
52
52
|
* const temp = new TempDirectoryObject("myapp")
|
|
53
|
-
* console.log(temp.path) // "/
|
|
54
|
-
*
|
|
55
|
-
* @example
|
|
56
|
-
* // Nested temp directories
|
|
57
|
-
* const parent = new TempDirectoryObject("parent")
|
|
58
|
-
* const child = new TempDirectoryObject("child", parent)
|
|
59
|
-
* await parent.remove() // Removes both parent and child
|
|
53
|
+
* console.log(temp.path) // "/"
|
|
54
|
+
* console.log(temp.real.path) // "/tmp/myapp-ABC123"
|
|
60
55
|
*/
|
|
61
|
-
constructor(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// No parent: need to create a capped parent at tmpdir first
|
|
67
|
-
cappedParent = new CappedDirectoryObject(os.tmpdir(), null, true)
|
|
68
|
-
|
|
69
|
-
if(name) {
|
|
70
|
-
// Check if name is a simple name (no separators, not absolute)
|
|
71
|
-
const isSimpleName = !path.isAbsolute(name) &&
|
|
72
|
-
!name.includes("/") &&
|
|
73
|
-
!name.includes("\\") &&
|
|
74
|
-
!name.includes(path.sep)
|
|
75
|
-
|
|
76
|
-
if(isSimpleName) {
|
|
77
|
-
// Simple name: add unique suffix
|
|
78
|
-
const prefix = name.endsWith("-") ? name : `${name}-`
|
|
79
|
-
const uniqueSuffix =
|
|
80
|
-
Math.random()
|
|
81
|
-
.toString(36)
|
|
82
|
-
.substring(2, 8)
|
|
83
|
-
.toUpperCase()
|
|
84
|
-
dirPath = `${prefix}${uniqueSuffix}`
|
|
85
|
-
} else {
|
|
86
|
-
// Complex path: use as-is, let CappedDirectoryObject handle coercion
|
|
87
|
-
dirPath = name
|
|
88
|
-
}
|
|
89
|
-
} else {
|
|
90
|
-
// No name: use tmpdir itself (no parent)
|
|
91
|
-
dirPath = os.tmpdir()
|
|
92
|
-
cappedParent = null
|
|
93
|
-
}
|
|
94
|
-
} else {
|
|
95
|
-
// With parent: validate it's a proper temp directory parent
|
|
96
|
-
if(!Data.isType(parent, "CappedDirectoryObject")) {
|
|
97
|
-
throw Sass.new(
|
|
98
|
-
"Parent must be a CappedDirectoryObject or TempDirectoryObject."
|
|
99
|
-
)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// SECURITY: Ensure parent's cap is within tmpdir (prevent escape to other caps)
|
|
103
|
-
const tmpdir = path.resolve(os.tmpdir())
|
|
104
|
-
const parentCap = path.resolve(parent.cap)
|
|
105
|
-
|
|
106
|
-
if(!parentCap.startsWith(tmpdir)) {
|
|
107
|
-
throw Sass.new(
|
|
108
|
-
`Parent must be capped to OS temp directory (${tmpdir}) or a subdirectory thereof, ` +
|
|
109
|
-
`got cap: ${parent.cap}`
|
|
110
|
-
)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
dirPath = name || ""
|
|
114
|
-
if(!dirPath) {
|
|
115
|
-
throw Sass.new("Name must not be empty when parent is provided.")
|
|
116
|
-
}
|
|
117
|
-
}
|
|
56
|
+
constructor(directory, source=null) {
|
|
57
|
+
Valid.type(source, "Null|TempDirectoryObject")
|
|
58
|
+
|
|
59
|
+
directory ||= "temp"
|
|
60
|
+
directory = Data.append(directory, source ? "" : "-")
|
|
118
61
|
|
|
119
|
-
|
|
120
|
-
super(dirPath, cappedParent, true)
|
|
62
|
+
const parentRealPath = source?.real.path ?? os.tmpdir()
|
|
121
63
|
|
|
122
|
-
|
|
123
|
-
|
|
64
|
+
if(source && path.isAbsolute(directory)) {
|
|
65
|
+
const {root} = FS.pathParts(directory)
|
|
124
66
|
|
|
125
|
-
|
|
126
|
-
// This makes temp.cap === temp.real.path and temp.parent === null
|
|
127
|
-
if(!parent) {
|
|
128
|
-
// Only re-cap if this is a root temp directory (no TempDirectoryObject parent)
|
|
129
|
-
this._recapToSelf()
|
|
67
|
+
directory = Data.chopLeft(directory, root)
|
|
130
68
|
}
|
|
131
|
-
}
|
|
132
69
|
|
|
133
|
-
|
|
134
|
-
* TempDirectoryObject does not support fromCwd() since it is specifically
|
|
135
|
-
* designed to work within the OS temporary directory tree.
|
|
136
|
-
*
|
|
137
|
-
* @throws {Sass} Always throws an error
|
|
138
|
-
*/
|
|
139
|
-
static fromCwd() {
|
|
140
|
-
throw Sass.new(
|
|
141
|
-
"TempDirectoryObject.fromCwd() is not supported. " +
|
|
142
|
-
"Use CappedDirectoryObject.fromCwd() instead if you need to cap at the current working directory."
|
|
143
|
-
)
|
|
144
|
-
}
|
|
70
|
+
let realTempDirectoryPath, toSuper
|
|
145
71
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
} catch(e) {
|
|
157
|
-
// EEXIST is fine - directory already exists
|
|
158
|
-
if(e.code !== "EEXIST") {
|
|
159
|
-
throw Sass.new(
|
|
160
|
-
`Unable to create temporary directory '${this.realPath}': ${e.message}`
|
|
161
|
-
)
|
|
162
|
-
}
|
|
72
|
+
if(source) {
|
|
73
|
+
toSuper = `/${directory}`
|
|
74
|
+
realTempDirectoryPath =
|
|
75
|
+
FS.mergeOverlappingPaths(parentRealPath, directory)
|
|
76
|
+
if(!fs.existsSync(realTempDirectoryPath))
|
|
77
|
+
mkdirSync(realTempDirectoryPath)
|
|
78
|
+
} else {
|
|
79
|
+
realTempDirectoryPath =
|
|
80
|
+
mkdtempSync(FS.mergeOverlappingPaths(os.tmpdir(), directory))
|
|
81
|
+
toSuper = path.resolve(path.sep)
|
|
163
82
|
}
|
|
83
|
+
|
|
84
|
+
super(toSuper, source)
|
|
85
|
+
|
|
86
|
+
this.#tmpReal = new DirectoryObject(realTempDirectoryPath)
|
|
87
|
+
this.#tmpCap = source?.cap ?? this
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
get isTemporary() {
|
|
91
|
+
return true
|
|
164
92
|
}
|
|
165
93
|
|
|
166
94
|
/**
|
|
167
|
-
*
|
|
95
|
+
* Returns a plain DirectoryObject representing the actual filesystem location.
|
|
96
|
+
* This provides an "escape hatch" from the capped environment to interact
|
|
97
|
+
* with the real filesystem when needed.
|
|
168
98
|
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
* @param {string} newPath - The path segment to append
|
|
172
|
-
* @returns {TempDirectoryObject} A new TempDirectoryObject with the extended path
|
|
173
|
-
* @throws {Sass} If the path would escape the temp directory
|
|
174
|
-
* @throws {Sass} If the path is absolute
|
|
175
|
-
* @throws {Sass} If the path contains traversal (..)
|
|
99
|
+
* @returns {DirectoryObject} Uncapped directory object at the real filesystem path
|
|
176
100
|
* @example
|
|
177
101
|
* const temp = new TempDirectoryObject("myapp")
|
|
178
|
-
* const
|
|
179
|
-
*
|
|
102
|
+
* const subdir = temp.getDirectory("data")
|
|
103
|
+
*
|
|
104
|
+
* // Work within the capped environment (virtual paths)
|
|
105
|
+
* console.log(subdir.path) // "/data" (virtual)
|
|
106
|
+
* subdir.getFile("config.json") // Stays within cap
|
|
107
|
+
*
|
|
108
|
+
* // Break out to real filesystem when needed
|
|
109
|
+
* console.log(subdir.real.path) // "/tmp/myapp-ABC123/data" (real)
|
|
110
|
+
* subdir.real.parent // Can traverse outside the cap
|
|
180
111
|
*/
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
112
|
+
get real() {
|
|
113
|
+
return this.#tmpReal
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
get cap() {
|
|
117
|
+
return this.#tmpCap
|
|
184
118
|
}
|
|
185
119
|
|
|
186
120
|
/**
|
|
187
|
-
*
|
|
121
|
+
* Recursively removes a temporary directory and all its contents.
|
|
188
122
|
*
|
|
189
|
-
*
|
|
123
|
+
* This method will delete all files and subdirectories within this directory,
|
|
124
|
+
* then delete the directory itself. It only works on directories explicitly
|
|
125
|
+
* marked as temporary for safety.
|
|
190
126
|
*
|
|
191
|
-
* @
|
|
192
|
-
* @returns {
|
|
193
|
-
* @throws {Sass} If the
|
|
194
|
-
* @throws {Sass} If the
|
|
195
|
-
* @throws {Sass} If the path contains traversal (..)
|
|
127
|
+
* @async
|
|
128
|
+
* @returns {Promise<void>}
|
|
129
|
+
* @throws {Sass} If the directory is not marked as temporary
|
|
130
|
+
* @throws {Sass} If the directory deletion fails
|
|
196
131
|
* @example
|
|
197
|
-
* const
|
|
198
|
-
*
|
|
199
|
-
*
|
|
132
|
+
* const tempDir = new TempDirectoryObject("my-temp")
|
|
133
|
+
* await tempDir.assureExists()
|
|
134
|
+
* // ... use the directory ...
|
|
135
|
+
* await tempDir.remove() // Recursively deletes everything
|
|
200
136
|
*/
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
137
|
+
async remove() {
|
|
138
|
+
await this.#recurseDelete(this.real)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async #recurseDelete(directory) {
|
|
142
|
+
const {files, directories} = await directory.read()
|
|
143
|
+
|
|
144
|
+
// files first
|
|
145
|
+
for(const file of files)
|
|
146
|
+
await file.delete()
|
|
147
|
+
|
|
148
|
+
// now dir-ty dirs
|
|
149
|
+
for(const dir of directories)
|
|
150
|
+
await this.#recurseDelete(dir)
|
|
151
|
+
|
|
152
|
+
// byebyebyeeee πΊπΎ
|
|
153
|
+
await directory.delete()
|
|
204
154
|
}
|
|
205
155
|
|
|
206
156
|
/**
|
|
207
|
-
*
|
|
157
|
+
* TempDirectoryObject does not support fromCwd() since it is specifically
|
|
158
|
+
* designed to work within the OS temporary directory tree.
|
|
208
159
|
*
|
|
209
|
-
* @
|
|
160
|
+
* @throws {Sass} Always throws an error
|
|
210
161
|
*/
|
|
211
|
-
|
|
212
|
-
|
|
162
|
+
static fromCwd() {
|
|
163
|
+
throw Sass.new("TempDirectoryObject.fromCwd() is not supported.")
|
|
213
164
|
}
|
|
214
165
|
}
|
package/src/lib/Valid.js
CHANGED
|
@@ -25,7 +25,7 @@ export default class Valid {
|
|
|
25
25
|
static type(value, type, options) {
|
|
26
26
|
Valid.assert(
|
|
27
27
|
Data.isType(value, type, options),
|
|
28
|
-
`Invalid type. Expected ${type}, got ${
|
|
28
|
+
`Invalid type. Expected ${type}, got ${Data.typeOf(value)}`,
|
|
29
29
|
1,
|
|
30
30
|
)
|
|
31
31
|
}
|