@gesslar/toolkit 3.8.0 → 3.12.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +87 -467
- package/src/lib/DirectoryObject.js +109 -147
- package/src/lib/FS.js +221 -70
- package/src/lib/FileObject.js +78 -81
- package/src/lib/TempDirectoryObject.js +93 -129
- 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 +48 -56
- package/src/types/lib/CappedDirectoryObject.d.ts.map +1 -1
- package/src/types/lib/DirectoryObject.d.ts +24 -54
- 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 -3
- package/src/types/lib/FileObject.d.ts.map +1 -1
- package/src/types/lib/TempDirectoryObject.d.ts +19 -59
- package/src/types/lib/TempDirectoryObject.d.ts.map +1 -1
|
@@ -10,11 +10,10 @@
|
|
|
10
10
|
|
|
11
11
|
import path from "node:path"
|
|
12
12
|
|
|
13
|
-
import {Data, Valid} from "../browser/index.js"
|
|
14
13
|
import DirectoryObject from "./DirectoryObject.js"
|
|
15
14
|
import FileObject from "./FileObject.js"
|
|
16
15
|
import FS from "./FS.js"
|
|
17
|
-
import
|
|
16
|
+
import Valid from "./Valid.js"
|
|
18
17
|
|
|
19
18
|
/**
|
|
20
19
|
* CappedDirectoryObject extends DirectoryObject with constraints that ensure
|
|
@@ -26,7 +25,10 @@ import Sass from "./Sass.js"
|
|
|
26
25
|
* @augments DirectoryObject
|
|
27
26
|
*/
|
|
28
27
|
export default class CappedDirectoryObject extends DirectoryObject {
|
|
28
|
+
#real
|
|
29
29
|
#cap
|
|
30
|
+
#cappedParentPath
|
|
31
|
+
#cappedParent
|
|
30
32
|
|
|
31
33
|
/**
|
|
32
34
|
* Constructs a CappedDirectoryObject instance.
|
|
@@ -35,13 +37,16 @@ export default class CappedDirectoryObject extends DirectoryObject {
|
|
|
35
37
|
* (virtual root). With a parent, the path is resolved relative to the parent's
|
|
36
38
|
* cap using virtual path semantics (absolute paths treated as cap-relative).
|
|
37
39
|
*
|
|
38
|
-
* @param {string}
|
|
40
|
+
* @param {string} [directory="."] - Directory path (becomes cap if no parent, else relative to parent's cap, defaults to current directory)
|
|
39
41
|
* @param {CappedDirectoryObject?} [parent] - Optional parent capped directory
|
|
40
|
-
* @param {boolean} [temporary=false] - Whether this is a temporary directory
|
|
41
|
-
* @throws {Sass} If path is empty
|
|
42
42
|
* @throws {Sass} If parent is provided but not a CappedDirectoryObject
|
|
43
43
|
* @throws {Sass} If the resulting path would escape the cap
|
|
44
44
|
* @example
|
|
45
|
+
* // Create new capped directory at current directory
|
|
46
|
+
* const cwd = new CappedDirectoryObject()
|
|
47
|
+
* // path: process.cwd(), cap: process.cwd()
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
45
50
|
* // Create new capped directory
|
|
46
51
|
* const cache = new CappedDirectoryObject("/home/user/.cache")
|
|
47
52
|
* // path: /home/user/.cache, cap: /home/user/.cache
|
|
@@ -56,153 +61,84 @@ export default class CappedDirectoryObject extends DirectoryObject {
|
|
|
56
61
|
* const config = new CappedDirectoryObject("/etc/config", cache)
|
|
57
62
|
* // path: /home/user/.cache/etc/config, cap: /home/user/.cache
|
|
58
63
|
*/
|
|
59
|
-
constructor(
|
|
60
|
-
Valid.type(
|
|
61
|
-
Valid.assert(dirPath.length > 0, "Path must not be empty.")
|
|
62
|
-
|
|
63
|
-
// Validate parent using inheritance-aware type checking
|
|
64
|
-
if(parent !== null && !Data.isType(parent, "CappedDirectoryObject")) {
|
|
65
|
-
throw Sass.new(`Parent must be null or a CappedDirectoryObject instance, got ${Data.typeOf(parent)}`)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
let cap
|
|
69
|
-
let resolvedPath
|
|
70
|
-
|
|
71
|
-
if(!parent) {
|
|
72
|
-
// No parent: dirPath becomes both the directory and the cap
|
|
73
|
-
cap = path.resolve(dirPath)
|
|
74
|
-
resolvedPath = cap
|
|
75
|
-
} else {
|
|
76
|
-
// With parent: inherit cap and resolve dirPath relative to it
|
|
77
|
-
cap = parent.#cap
|
|
78
|
-
|
|
79
|
-
// Use real path for filesystem operations
|
|
80
|
-
const parentPath = parent.realPath || parent.path
|
|
81
|
-
const capResolved = path.resolve(cap)
|
|
82
|
-
|
|
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
|
-
}
|
|
64
|
+
constructor(directory, source=null) {
|
|
65
|
+
Valid.type(source, "Null|CappedDirectoryObject")
|
|
93
66
|
|
|
94
|
-
|
|
95
|
-
const resolved = path.resolve(targetPath)
|
|
67
|
+
directory ||= "."
|
|
96
68
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
// Path tried to escape - clamp to cap root
|
|
100
|
-
resolvedPath = capResolved
|
|
101
|
-
} else {
|
|
102
|
-
resolvedPath = resolved
|
|
103
|
-
}
|
|
104
|
-
}
|
|
69
|
+
const baseLocalPath = source?.path ?? "/"
|
|
70
|
+
const baseRealPath = source?.real.path ?? directory
|
|
105
71
|
|
|
106
|
-
|
|
107
|
-
|
|
72
|
+
if(source && directory.startsWith("/"))
|
|
73
|
+
directory = directory.slice(1)
|
|
108
74
|
|
|
109
|
-
//
|
|
110
|
-
|
|
75
|
+
// Find out what directory means to the basePath
|
|
76
|
+
const realResolved = FS.resolvePath(baseRealPath, directory)
|
|
77
|
+
const localResolved = source
|
|
78
|
+
? FS.resolvePath(baseLocalPath, directory)
|
|
79
|
+
: path.parse(path.resolve("")).root
|
|
111
80
|
|
|
112
|
-
|
|
113
|
-
this.#validateCapPath()
|
|
114
|
-
}
|
|
81
|
+
super(localResolved)
|
|
115
82
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
*
|
|
119
|
-
* @private
|
|
120
|
-
* @throws {Sass} If the path is not within the cap directory
|
|
121
|
-
*/
|
|
122
|
-
#validateCapPath() {
|
|
123
|
-
const cap = this.#cap
|
|
124
|
-
const resolved = path.resolve(this.#realPath)
|
|
125
|
-
const capResolved = path.resolve(cap)
|
|
83
|
+
this.#real = new DirectoryObject(realResolved)
|
|
84
|
+
this.#cap = source?.cap ?? this
|
|
126
85
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
86
|
+
if(source) {
|
|
87
|
+
this.#cappedParent = source
|
|
88
|
+
this.#cappedParentPath = source.path
|
|
89
|
+
} else {
|
|
90
|
+
this.#cappedParent = null
|
|
91
|
+
this.#cappedParentPath = null
|
|
132
92
|
}
|
|
133
93
|
}
|
|
134
94
|
|
|
135
95
|
/**
|
|
136
|
-
*
|
|
137
|
-
* This is
|
|
96
|
+
* Creates a CappedDirectoryObject from the current working directory.
|
|
97
|
+
* This is useful when working with pnpx or other tools where you need to
|
|
98
|
+
* cap at the project's root directory determined at runtime.
|
|
138
99
|
*
|
|
139
|
-
* @
|
|
100
|
+
* @returns {CappedDirectoryObject} A CappedDirectoryObject capped at the current working directory
|
|
101
|
+
* @example
|
|
102
|
+
* // When using pnpx or similar tools
|
|
103
|
+
* const projectRoot = CappedDirectoryObject.fromCwd()
|
|
104
|
+
* const srcDir = projectRoot.getDirectory("src")
|
|
105
|
+
* // srcDir is capped at the project root
|
|
140
106
|
*/
|
|
141
|
-
|
|
142
|
-
|
|
107
|
+
static fromCwd() {
|
|
108
|
+
return new this(process.cwd())
|
|
143
109
|
}
|
|
144
110
|
|
|
145
111
|
/**
|
|
146
|
-
*
|
|
112
|
+
* Indicates whether this directory is capped (constrained to a specific tree).
|
|
113
|
+
* Always returns true for CappedDirectoryObject instances.
|
|
147
114
|
*
|
|
148
|
-
* @returns {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Returns whether this directory is capped.
|
|
115
|
+
* @returns {boolean} True for all CappedDirectoryObject instances
|
|
116
|
+
* @example
|
|
117
|
+
* const capped = new TempDirectoryObject("myapp")
|
|
118
|
+
* console.log(capped.isCapped) // true
|
|
156
119
|
*
|
|
157
|
-
*
|
|
120
|
+
* const regular = new DirectoryObject("/tmp")
|
|
121
|
+
* console.log(regular.isCapped) // false
|
|
158
122
|
*/
|
|
159
|
-
get
|
|
123
|
+
get isCapped() {
|
|
160
124
|
return true
|
|
161
125
|
}
|
|
162
126
|
|
|
163
127
|
/**
|
|
164
|
-
* Returns the
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
* @returns {string} The actual filesystem path
|
|
168
|
-
*/
|
|
169
|
-
get realPath() {
|
|
170
|
-
return super.path
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Private alias for realPath (for use in private methods).
|
|
128
|
+
* Returns the cap (root) of the capped directory tree.
|
|
129
|
+
* For root CappedDirectoryObject instances, returns itself.
|
|
130
|
+
* For children, returns the inherited cap from the parent chain.
|
|
175
131
|
*
|
|
176
|
-
* @
|
|
177
|
-
* @returns {string} The actual filesystem path
|
|
178
|
-
*/
|
|
179
|
-
get #realPath() {
|
|
180
|
-
return this.realPath
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Returns the virtual path relative to the cap.
|
|
185
|
-
* This is the default path representation in the capped environment.
|
|
186
|
-
* Use `.real.path` to access the actual filesystem path.
|
|
187
|
-
*
|
|
188
|
-
* @returns {string} Path relative to cap, or "/" if at cap root
|
|
132
|
+
* @returns {CappedDirectoryObject} The cap directory object (root of the capped tree)
|
|
189
133
|
* @example
|
|
190
134
|
* const temp = new TempDirectoryObject("myapp")
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
135
|
+
* console.log(temp.cap === temp) // true (root is its own cap)
|
|
136
|
+
*
|
|
137
|
+
* const subdir = temp.getDirectory("data")
|
|
138
|
+
* console.log(subdir.cap === temp) // true (child inherits parent's cap)
|
|
194
139
|
*/
|
|
195
|
-
get
|
|
196
|
-
|
|
197
|
-
const relative = path.relative(capResolved, this.#realPath)
|
|
198
|
-
|
|
199
|
-
// If at cap root or empty, return "/"
|
|
200
|
-
if(!relative || relative === ".") {
|
|
201
|
-
return "/"
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Return with leading slash to indicate it's cap-relative
|
|
205
|
-
return "/" + relative.split(path.sep).join("/")
|
|
140
|
+
get cap() {
|
|
141
|
+
return this.#cap
|
|
206
142
|
}
|
|
207
143
|
|
|
208
144
|
/**
|
|
@@ -224,7 +160,7 @@ export default class CappedDirectoryObject extends DirectoryObject {
|
|
|
224
160
|
* subdir.real.parent // Can traverse outside the cap
|
|
225
161
|
*/
|
|
226
162
|
get real() {
|
|
227
|
-
return
|
|
163
|
+
return this.#real
|
|
228
164
|
}
|
|
229
165
|
|
|
230
166
|
/**
|
|
@@ -242,61 +178,31 @@ export default class CappedDirectoryObject extends DirectoryObject {
|
|
|
242
178
|
* console.log(capped.parent) // null (at cap root)
|
|
243
179
|
*/
|
|
244
180
|
get parent() {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
// If we're at the cap, return null (cap is the "root")
|
|
248
|
-
if(this.#realPath === capResolved) {
|
|
249
|
-
return null
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Compute parent's real path
|
|
253
|
-
const parentPath = path.dirname(this.#realPath)
|
|
254
|
-
const isRoot = parentPath === this.#realPath
|
|
255
|
-
|
|
256
|
-
if(isRoot) {
|
|
257
|
-
return null
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Compute relative path from current to parent (just "..")
|
|
261
|
-
// Then use getDirectory to create the parent, which preserves the class type
|
|
262
|
-
return this.getDirectory("..")
|
|
181
|
+
return this.#cappedParent
|
|
263
182
|
}
|
|
264
183
|
|
|
265
184
|
/**
|
|
266
|
-
* Returns the
|
|
185
|
+
* Returns the path of the parent directory.
|
|
186
|
+
* Returns null if this directory is at the cap root (no parent).
|
|
187
|
+
*
|
|
188
|
+
* @returns {string|null} The parent directory path, or null if at cap root
|
|
189
|
+
* @example
|
|
190
|
+
* const temp = new TempDirectoryObject("myapp")
|
|
191
|
+
* console.log(temp.parentPath) // null (at cap root)
|
|
267
192
|
*
|
|
268
|
-
*
|
|
193
|
+
* const subdir = temp.getDirectory("data")
|
|
194
|
+
* console.log(subdir.parentPath) // "/data" or similar (parent's virtual path)
|
|
269
195
|
*/
|
|
270
|
-
get
|
|
271
|
-
return
|
|
196
|
+
get parentPath() {
|
|
197
|
+
return this.#cappedParentPath
|
|
272
198
|
}
|
|
273
199
|
|
|
274
200
|
/**
|
|
275
|
-
* Returns JSON representation
|
|
201
|
+
* Returns a JSON representation of the DirectoryObject.
|
|
276
202
|
*
|
|
277
|
-
* @returns {object} JSON representation
|
|
203
|
+
* @returns {object} JSON representation of the DirectoryObject
|
|
278
204
|
*/
|
|
279
205
|
toJSON() {
|
|
280
|
-
const capResolved = path.resolve(this.#cap)
|
|
281
|
-
let parentPath
|
|
282
|
-
|
|
283
|
-
if(this.#realPath === capResolved) {
|
|
284
|
-
// At cap root, no parent
|
|
285
|
-
parentPath = null
|
|
286
|
-
} else {
|
|
287
|
-
// Compute parent's virtual path
|
|
288
|
-
const parentReal = path.dirname(this.#realPath)
|
|
289
|
-
const relative = path.relative(capResolved, parentReal)
|
|
290
|
-
|
|
291
|
-
// If parent is cap root or empty, return "/"
|
|
292
|
-
if(!relative || relative === ".") {
|
|
293
|
-
parentPath = "/"
|
|
294
|
-
} else {
|
|
295
|
-
// Return parent's virtual path with leading slash
|
|
296
|
-
parentPath = "/" + relative.split(path.sep).join("/")
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
206
|
return {
|
|
301
207
|
supplied: this.supplied,
|
|
302
208
|
path: this.path,
|
|
@@ -306,260 +212,12 @@ export default class CappedDirectoryObject extends DirectoryObject {
|
|
|
306
212
|
extension: this.extension,
|
|
307
213
|
isFile: this.isFile,
|
|
308
214
|
isDirectory: this.isDirectory,
|
|
309
|
-
parent:
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Generator that walks up the directory tree, stopping at the cap.
|
|
317
|
-
* Yields parent directories from current up to (and including) the cap root.
|
|
318
|
-
*
|
|
319
|
-
* @returns {Generator<DirectoryObject>} Generator yielding parent DirectoryObject instances
|
|
320
|
-
* @example
|
|
321
|
-
* const capped = new TempDirectoryObject("myapp")
|
|
322
|
-
* const deep = capped.getDirectory("data").getDirectory("files")
|
|
323
|
-
* for(const parent of deep.walkUp) {
|
|
324
|
-
* console.log(parent.path)
|
|
325
|
-
* // .../myapp-ABC123/data/files
|
|
326
|
-
* // .../myapp-ABC123/data
|
|
327
|
-
* // .../myapp-ABC123 (stops at cap)
|
|
328
|
-
* }
|
|
329
|
-
*/
|
|
330
|
-
*#walkUpCapped() {
|
|
331
|
-
const capResolved = path.resolve(this.#cap)
|
|
332
|
-
|
|
333
|
-
// Build trail from real path
|
|
334
|
-
const trail = this.#realPath.split(path.sep).filter(Boolean)
|
|
335
|
-
const curr = [...trail]
|
|
336
|
-
|
|
337
|
-
while(curr.length > 0) {
|
|
338
|
-
const joined = path.sep + curr.join(path.sep)
|
|
339
|
-
|
|
340
|
-
// Don't yield anything beyond the cap
|
|
341
|
-
if(!joined.startsWith(capResolved)) {
|
|
342
|
-
break
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Yield plain DirectoryObject with real path
|
|
346
|
-
yield new DirectoryObject(joined, this.temporary)
|
|
347
|
-
|
|
348
|
-
// Stop after yielding the cap
|
|
349
|
-
if(joined === capResolved) {
|
|
350
|
-
break
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
curr.pop()
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* Returns a generator that walks up to the cap.
|
|
359
|
-
*
|
|
360
|
-
* @returns {Generator<DirectoryObject>} Generator yielding parent directories
|
|
361
|
-
*/
|
|
362
|
-
get walkUp() {
|
|
363
|
-
return this.#walkUpCapped()
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* Creates a new CappedDirectoryObject by extending this directory's path.
|
|
368
|
-
*
|
|
369
|
-
* All paths are coerced to remain within the cap directory tree:
|
|
370
|
-
* - Absolute paths (e.g., "/foo") are treated as relative to the cap
|
|
371
|
-
* - Parent traversal ("..") is allowed but clamped at the cap boundary
|
|
372
|
-
* - The cap acts as the virtual root directory
|
|
373
|
-
*
|
|
374
|
-
* @param {string} newPath - The path to resolve (can be absolute or contain ..)
|
|
375
|
-
* @returns {CappedDirectoryObject} A new CappedDirectoryObject with the coerced path
|
|
376
|
-
* @example
|
|
377
|
-
* const capped = new TempDirectoryObject("myapp")
|
|
378
|
-
* const subDir = capped.getDirectory("data")
|
|
379
|
-
* console.log(subDir.path) // "/tmp/myapp-ABC123/data"
|
|
380
|
-
*
|
|
381
|
-
* @example
|
|
382
|
-
* // Absolute paths are relative to cap
|
|
383
|
-
* const abs = capped.getDirectory("/foo/bar")
|
|
384
|
-
* console.log(abs.path) // "/tmp/myapp-ABC123/foo/bar"
|
|
385
|
-
*
|
|
386
|
-
* @example
|
|
387
|
-
* // Excessive .. traversal clamps to cap
|
|
388
|
-
* const up = capped.getDirectory("../../../etc/passwd")
|
|
389
|
-
* console.log(up.path) // "/tmp/myapp-ABC123" (clamped to cap)
|
|
390
|
-
*/
|
|
391
|
-
getDirectory(newPath) {
|
|
392
|
-
Valid.type(newPath, "String")
|
|
393
|
-
|
|
394
|
-
// Fast path: if it's a simple name (no separators, not absolute, no ..)
|
|
395
|
-
// use the subclass constructor directly to preserve type
|
|
396
|
-
const isSimpleName = !path.isAbsolute(newPath) &&
|
|
397
|
-
!newPath.includes("/") &&
|
|
398
|
-
!newPath.includes("\\") &&
|
|
399
|
-
!newPath.includes("..")
|
|
400
|
-
|
|
401
|
-
if(isSimpleName) {
|
|
402
|
-
// Both CappedDirectoryObject and subclasses use same signature now
|
|
403
|
-
return new this.constructor(newPath, this, this.temporary)
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// Complex path - handle coercion
|
|
407
|
-
const capResolved = path.resolve(this.#cap)
|
|
408
|
-
let targetPath
|
|
409
|
-
|
|
410
|
-
// If absolute, treat as relative to cap (virtual root)
|
|
411
|
-
if(path.isAbsolute(newPath)) {
|
|
412
|
-
// Strip leading slashes to make relative
|
|
413
|
-
const relative = newPath.replace(/^[/\\]+/, "")
|
|
414
|
-
|
|
415
|
-
// Join with cap (unless empty, which means cap root)
|
|
416
|
-
targetPath = relative ? path.join(capResolved, relative) : capResolved
|
|
417
|
-
} else {
|
|
418
|
-
// Relative path - resolve from current directory
|
|
419
|
-
targetPath = FS.resolvePath(this.#realPath, newPath)
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// Resolve to absolute path (handles .. and .)
|
|
423
|
-
const resolved = path.resolve(targetPath)
|
|
424
|
-
|
|
425
|
-
// Coerce: if path escaped cap, clamp to cap boundary
|
|
426
|
-
const coerced = resolved.startsWith(capResolved)
|
|
427
|
-
? resolved
|
|
428
|
-
: capResolved
|
|
429
|
-
|
|
430
|
-
// Compute path relative to cap for reconstruction
|
|
431
|
-
const relativeToCap = path.relative(capResolved, coerced)
|
|
432
|
-
|
|
433
|
-
// If we're at the cap root, return cap root directory
|
|
434
|
-
if(!relativeToCap || relativeToCap === ".") {
|
|
435
|
-
return this.#createCappedAtRoot()
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// Build directory by traversing segments from cap
|
|
439
|
-
return this.#buildDirectoryFromRelativePath(relativeToCap)
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
/**
|
|
443
|
-
* Creates a CappedDirectoryObject at the cap root.
|
|
444
|
-
* Can be overridden by subclasses that have different root semantics.
|
|
445
|
-
*
|
|
446
|
-
* @private
|
|
447
|
-
* @returns {CappedDirectoryObject} Directory object at cap root
|
|
448
|
-
*/
|
|
449
|
-
#createCappedAtRoot() {
|
|
450
|
-
// Create a base CappedDirectoryObject at the cap path
|
|
451
|
-
// This works for direct usage of CappedDirectoryObject
|
|
452
|
-
// Subclasses may need to override if they have special semantics
|
|
453
|
-
return new CappedDirectoryObject(this.#cap, null, this.temporary)
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
/**
|
|
457
|
-
* Builds a directory by traversing path segments from cap.
|
|
458
|
-
*
|
|
459
|
-
* @private
|
|
460
|
-
* @param {string} relativePath - Path relative to cap
|
|
461
|
-
* @returns {CappedDirectoryObject} The directory at the final path
|
|
462
|
-
*/
|
|
463
|
-
#buildDirectoryFromRelativePath(relativePath) {
|
|
464
|
-
const segments = relativePath.split(path.sep).filter(Boolean)
|
|
465
|
-
|
|
466
|
-
// Start at cap root
|
|
467
|
-
let current = this.#createCappedAtRoot()
|
|
468
|
-
|
|
469
|
-
// Traverse each segment, using constructor to preserve class type
|
|
470
|
-
for(const segment of segments) {
|
|
471
|
-
// Use simple name constructor to preserve subclass type
|
|
472
|
-
// Works for both CappedDirectoryObject and TempDirectoryObject
|
|
473
|
-
current = new this.constructor(segment, current, this.temporary)
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
return current
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
/**
|
|
480
|
-
* Creates a new FileObject by extending this directory's path.
|
|
481
|
-
*
|
|
482
|
-
* All paths are coerced to remain within the cap directory tree:
|
|
483
|
-
* - Absolute paths (e.g., "/config.json") are treated as relative to the cap
|
|
484
|
-
* - Parent traversal ("..") is allowed but clamped at the cap boundary
|
|
485
|
-
* - The cap acts as the virtual root directory
|
|
486
|
-
*
|
|
487
|
-
* @param {string} filename - The filename to resolve (can be absolute or contain ..)
|
|
488
|
-
* @returns {FileObject} A new FileObject with the coerced path
|
|
489
|
-
* @example
|
|
490
|
-
* const capped = new TempDirectoryObject("myapp")
|
|
491
|
-
* const file = capped.getFile("config.json")
|
|
492
|
-
* console.log(file.path) // "/tmp/myapp-ABC123/config.json"
|
|
493
|
-
*
|
|
494
|
-
* @example
|
|
495
|
-
* // Absolute paths are relative to cap
|
|
496
|
-
* const abs = capped.getFile("/data/config.json")
|
|
497
|
-
* console.log(abs.path) // "/tmp/myapp-ABC123/data/config.json"
|
|
498
|
-
*
|
|
499
|
-
* @example
|
|
500
|
-
* // Excessive .. traversal clamps to cap
|
|
501
|
-
* const up = capped.getFile("../../../etc/passwd")
|
|
502
|
-
* console.log(up.path) // "/tmp/myapp-ABC123/passwd" (clamped to cap)
|
|
503
|
-
*/
|
|
504
|
-
getFile(filename) {
|
|
505
|
-
Valid.type(filename, "String")
|
|
506
|
-
|
|
507
|
-
// Fast path: if it's a simple filename (no separators, not absolute, no ..)
|
|
508
|
-
// use this as the parent directly
|
|
509
|
-
const isSimpleName = !path.isAbsolute(filename) &&
|
|
510
|
-
!filename.includes("/") &&
|
|
511
|
-
!filename.includes("\\") &&
|
|
512
|
-
!filename.includes("..")
|
|
513
|
-
|
|
514
|
-
if(isSimpleName) {
|
|
515
|
-
// Simple filename - create directly with this as parent
|
|
516
|
-
return new FileObject(filename, this)
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// Complex path - handle coercion
|
|
520
|
-
const capResolved = path.resolve(this.#cap)
|
|
521
|
-
let targetPath
|
|
522
|
-
|
|
523
|
-
// If absolute, treat as relative to cap (virtual root)
|
|
524
|
-
if(path.isAbsolute(filename)) {
|
|
525
|
-
// Strip leading slashes to make relative
|
|
526
|
-
const relative = filename.replace(/^[/\\]+/, "")
|
|
527
|
-
|
|
528
|
-
// Join with cap
|
|
529
|
-
targetPath = path.join(capResolved, relative)
|
|
530
|
-
} else {
|
|
531
|
-
// Relative path - resolve from current directory
|
|
532
|
-
targetPath = FS.resolvePath(this.#realPath, filename)
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
// Resolve to absolute path (handles .. and .)
|
|
536
|
-
const resolved = path.resolve(targetPath)
|
|
537
|
-
|
|
538
|
-
// Coerce: if path escaped cap, clamp to cap boundary
|
|
539
|
-
const coerced = resolved.startsWith(capResolved)
|
|
540
|
-
? resolved
|
|
541
|
-
: capResolved
|
|
542
|
-
|
|
543
|
-
// Extract directory and filename parts
|
|
544
|
-
let fileDir = path.dirname(coerced)
|
|
545
|
-
let fileBasename = path.basename(coerced)
|
|
546
|
-
|
|
547
|
-
// Special case: if coerced is exactly the cap (file tried to escape),
|
|
548
|
-
// the file should be placed at the cap root with just the filename
|
|
549
|
-
if(coerced === capResolved) {
|
|
550
|
-
// Extract just the filename from the original path
|
|
551
|
-
fileBasename = path.basename(resolved)
|
|
552
|
-
fileDir = capResolved
|
|
215
|
+
parent: this.parent,
|
|
216
|
+
parentPath: this.parentPath,
|
|
217
|
+
capped: this.isCapped,
|
|
218
|
+
cap: this.cap,
|
|
219
|
+
real: this.real
|
|
553
220
|
}
|
|
554
|
-
|
|
555
|
-
// Get or create the parent directory
|
|
556
|
-
const relativeToCap = path.relative(capResolved, fileDir)
|
|
557
|
-
const parentDir = !relativeToCap || relativeToCap === "."
|
|
558
|
-
? this.#createCappedAtRoot()
|
|
559
|
-
: this.#buildDirectoryFromRelativePath(relativeToCap)
|
|
560
|
-
|
|
561
|
-
// Create FileObject with parent directory
|
|
562
|
-
return new FileObject(fileBasename, parentDir)
|
|
563
221
|
}
|
|
564
222
|
|
|
565
223
|
/**
|
|
@@ -577,20 +235,14 @@ export default class CappedDirectoryObject extends DirectoryObject {
|
|
|
577
235
|
* @param {string} [pat=""] - Optional glob pattern
|
|
578
236
|
* @returns {Promise<{files: Array<FileObject>, directories: Array}>} Directory contents
|
|
579
237
|
*/
|
|
580
|
-
async read(
|
|
581
|
-
const {files, directories} = await this.real.read(
|
|
582
|
-
|
|
583
|
-
// Convert plain DirectoryObjects to CappedDirectoryObjects with same cap
|
|
584
|
-
const cappedDirectories = directories.map(dir => {
|
|
585
|
-
const name = dir.name
|
|
586
|
-
|
|
587
|
-
return new this.constructor(name, this)
|
|
588
|
-
})
|
|
238
|
+
async read(...arg) {
|
|
239
|
+
const {files, directories} = await this.real.read(...arg)
|
|
589
240
|
|
|
590
|
-
//
|
|
591
|
-
const
|
|
241
|
+
// we need to re-cast
|
|
242
|
+
const recastDirs = directories.map(e => this.getDirectory(e.name))
|
|
243
|
+
const recastFiles = files.map(f => new FileObject(f.name, this))
|
|
592
244
|
|
|
593
|
-
return {files:
|
|
245
|
+
return {files: recastFiles, directories: recastDirs}
|
|
594
246
|
}
|
|
595
247
|
|
|
596
248
|
/**
|
|
@@ -621,36 +273,4 @@ export default class CappedDirectoryObject extends DirectoryObject {
|
|
|
621
273
|
async delete() {
|
|
622
274
|
return await this.real.delete()
|
|
623
275
|
}
|
|
624
|
-
|
|
625
|
-
/**
|
|
626
|
-
* Override remove to preserve temporary flag check.
|
|
627
|
-
*
|
|
628
|
-
* @returns {Promise<void>}
|
|
629
|
-
*/
|
|
630
|
-
async remove() {
|
|
631
|
-
if(!this.temporary)
|
|
632
|
-
throw Sass.new("This is not a temporary directory.")
|
|
633
|
-
|
|
634
|
-
const {files, directories} = await this.read()
|
|
635
|
-
|
|
636
|
-
// Remove subdirectories recursively
|
|
637
|
-
for(const dir of directories)
|
|
638
|
-
await dir.remove()
|
|
639
|
-
|
|
640
|
-
// Remove files
|
|
641
|
-
for(const file of files)
|
|
642
|
-
await file.delete()
|
|
643
|
-
|
|
644
|
-
// Delete the now-empty directory
|
|
645
|
-
await this.delete()
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
/**
|
|
649
|
-
* Returns a string representation of the CappedDirectoryObject.
|
|
650
|
-
*
|
|
651
|
-
* @returns {string} string representation of the CappedDirectoryObject
|
|
652
|
-
*/
|
|
653
|
-
toString() {
|
|
654
|
-
return `[CappedDirectoryObject: ${this.path} (real: ${this.#realPath})]`
|
|
655
|
-
}
|
|
656
276
|
}
|