@gesslar/toolkit 4.0.0 → 4.2.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.
Files changed (38) hide show
  1. package/README.md +1 -0
  2. package/package.json +4 -4
  3. package/src/browser/lib/Data.js +4 -4
  4. package/src/browser/lib/TypeSpec.js +115 -39
  5. package/src/node/index.js +2 -1
  6. package/src/node/lib/Cache.js +105 -35
  7. package/src/node/lib/Data.js +49 -0
  8. package/src/node/lib/DirectoryObject.js +4 -7
  9. package/src/node/lib/FileObject.js +89 -53
  10. package/src/node/lib/FileSystem.js +47 -2
  11. package/src/node/lib/Font.js +1 -1
  12. package/src/node/lib/Notify.js +26 -6
  13. package/src/node/lib/Term.js +1 -1
  14. package/src/node/lib/Util.js +47 -5
  15. package/src/node/lib/Watcher.js +118 -0
  16. package/types/browser/lib/Data.d.ts +2 -8
  17. package/types/browser/lib/Data.d.ts.map +1 -1
  18. package/types/browser/lib/TypeSpec.d.ts +21 -36
  19. package/types/browser/lib/TypeSpec.d.ts.map +1 -1
  20. package/types/node/index.d.ts +2 -1
  21. package/types/node/lib/Cache.d.ts +36 -5
  22. package/types/node/lib/Cache.d.ts.map +1 -1
  23. package/types/node/lib/Data.d.ts +19 -0
  24. package/types/node/lib/Data.d.ts.map +1 -0
  25. package/types/node/lib/DirectoryObject.d.ts +6 -5
  26. package/types/node/lib/DirectoryObject.d.ts.map +1 -1
  27. package/types/node/lib/FileObject.d.ts +54 -26
  28. package/types/node/lib/FileObject.d.ts.map +1 -1
  29. package/types/node/lib/FileSystem.d.ts +19 -0
  30. package/types/node/lib/FileSystem.d.ts.map +1 -1
  31. package/types/node/lib/Notify.d.ts +23 -10
  32. package/types/node/lib/Notify.d.ts.map +1 -1
  33. package/types/node/lib/Term.d.ts +2 -2
  34. package/types/node/lib/Term.d.ts.map +1 -1
  35. package/types/node/lib/Util.d.ts +20 -6
  36. package/types/node/lib/Util.d.ts.map +1 -1
  37. package/types/node/lib/Watcher.d.ts +38 -0
  38. package/types/node/lib/Watcher.d.ts.map +1 -0
package/README.md CHANGED
@@ -45,6 +45,7 @@ Includes all browser functionality plus Node.js-specific modules for file I/O, l
45
45
  | Term | Terminal formatting and output utilities |
46
46
  | Util | General utility functions (Node-enhanced version) |
47
47
  | Valid | Validation and assertion methods |
48
+ | Watcher | File and directory change watcher with debounce protection |
48
49
 
49
50
 
50
51
  ## Installation
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "name": "gesslar",
6
6
  "url": "https://gesslar.dev"
7
7
  },
8
- "version": "4.0.0",
8
+ "version": "4.2.0",
9
9
  "license": "Unlicense",
10
10
  "homepage": "https://github.com/gesslar/toolkit#readme",
11
11
  "repository": {
@@ -77,9 +77,9 @@
77
77
  "yaml": "^2.8.2"
78
78
  },
79
79
  "devDependencies": {
80
- "@gesslar/uglier": "^2.0.0",
81
- "eslint": "^10.0.2",
82
- "happy-dom": "^20.8.3",
80
+ "@gesslar/uglier": "^2.2.0",
81
+ "eslint": "^10.0.3",
82
+ "happy-dom": "^20.8.4",
83
83
  "typescript": "^5.9.3"
84
84
  }
85
85
  }
@@ -199,11 +199,10 @@ export default class Data {
199
199
  * defining the type of a value and whether an array is expected.
200
200
  *
201
201
  * @param {string} string - The string to parse into a type spec.
202
- * @param {TypeSpecOptions} [options] - Additional options for parsing.
203
- * @returns {Array<object>} An array of type specs.
202
+ * @returns {TypeSpec} A new TypeSpec instance.
204
203
  */
205
- static newTypeSpec(string, options) {
206
- return new TypeSpec(string, options)
204
+ static newTypeSpec(string) {
205
+ return new TypeSpec(string)
207
206
  }
208
207
 
209
208
  /**
@@ -530,4 +529,5 @@ export default class Data {
530
529
  Data.isType(value, "ArrayBuffer|Blob|Buffer")
531
530
  )
532
531
  }
532
+
533
533
  }
@@ -9,20 +9,6 @@ import Data from "./Data.js"
9
9
  import Sass from "./Sass.js"
10
10
  import Util from "./Util.js"
11
11
 
12
- /**
13
- * Options for creating a new TypeSpec.
14
- *
15
- * @typedef {object} TypeSpecOptions
16
- * @property {string} [delimiter="|"] - The delimiter for union types
17
- */
18
-
19
- /**
20
- * Options for type validation methods.
21
- *
22
- * @typedef {object} TypeValidationOptions
23
- * @property {boolean} [allowEmpty=true] - Whether empty values are allowed
24
- */
25
-
26
12
  /**
27
13
  * Type specification class for parsing and validating complex type definitions.
28
14
  * Supports union types, array types, and validation options.
@@ -34,11 +20,10 @@ export default class TypeSpec {
34
20
  * Creates a new TypeSpec instance.
35
21
  *
36
22
  * @param {string} string - The type specification string (e.g., "string|number", "object[]")
37
- * @param {TypeSpecOptions} [options] - Additional parsing options
38
23
  */
39
- constructor(string, options) {
24
+ constructor(string) {
40
25
  this.#specs = []
41
- this.#parse(string, options)
26
+ this.#parse(string)
42
27
  Object.freeze(this.#specs)
43
28
  this.specs = this.#specs
44
29
  this.length = this.#specs.length
@@ -52,11 +37,21 @@ export default class TypeSpec {
52
37
  * @returns {string} The type specification as a string (e.g., "string|number[]")
53
38
  */
54
39
  toString() {
55
- return this.#specs
56
- .map(spec => {
57
- return `${spec.typeName}${spec.array ? "[]" : ""}`
58
- })
59
- .join("|")
40
+ // Reconstruct in parse order, grouping consecutive mixed specs
41
+ const parts = []
42
+ const emittedGroups = new Set()
43
+
44
+ for(const spec of this.#specs) {
45
+ if(spec.mixed === false) {
46
+ parts.push(`${spec.typeName}${spec.array ? "[]" : ""}`)
47
+ } else if(!emittedGroups.has(spec.mixed)) {
48
+ emittedGroups.add(spec.mixed)
49
+ const group = this.#specs.filter(s => s.mixed === spec.mixed)
50
+ parts.push(`(${group.map(s => s.typeName).join("|")})[]`)
51
+ }
52
+ }
53
+
54
+ return parts.join("|")
60
55
  }
61
56
 
62
57
  /**
@@ -148,22 +143,30 @@ export default class TypeSpec {
148
143
  * Handles array types, union types, and empty value validation.
149
144
  *
150
145
  * @param {unknown} value - The value to test against the type specifications
151
- * @param {TypeValidationOptions} [options] - Validation options
146
+ * @param {TypeMatchOptions} [options] - Validation options
152
147
  * @returns {boolean} True if the value matches any type specification
153
148
  */
154
149
  matches(value, options) {
155
150
  return this.match(value, options).length > 0
156
151
  }
157
152
 
153
+ /**
154
+ * Options that can be passed to {@link TypeSpec.match}
155
+ *
156
+ * @typedef {object} TypeMatchOptions
157
+ * @property {boolean} [allowEmpty=true] - Permit a spec of {@link Data.emptyableTypes} to be empty
158
+ */
159
+
158
160
  /**
159
161
  * Returns matching type specifications for a value.
160
162
  *
161
163
  * @param {unknown} value - The value to test against the type specifications
162
- * @param {TypeValidationOptions} [options] - Validation options
164
+ * @param {TypeMatchOptions} [options] - Validation options
163
165
  * @returns {Array<object>} Array of matching type specifications
164
166
  */
165
- match(value, options) {
166
- const allowEmpty = options?.allowEmpty ?? true
167
+ match(value, {
168
+ allowEmpty = true,
169
+ } = {}) {
167
170
 
168
171
  // If we have a list of types, because the string was validly parsed, we
169
172
  // need to ensure that all of the types that were parsed are valid types in
@@ -179,10 +182,13 @@ export default class TypeSpec {
179
182
  // We need to ensure that we match the type and the consistency of the
180
183
  // types in an array, if it is an array and an array is allowed.
181
184
  const matchingTypeSpec = this.filter(spec => {
185
+ // Skip mixed specs — they are handled in the grouped-array check below
186
+ if(spec.mixed !== false)
187
+ return false
188
+
182
189
  const {typeName: allowedType, array: allowedArray} = spec
183
- const empty =
184
- Data.emptyableTypes.includes(allowedType) &&
185
- Data.isEmpty(value)
190
+ const empty = Data.emptyableTypes.includes(allowedType)
191
+ && Data.isEmpty(value)
186
192
 
187
193
  // Handle non-array values
188
194
  if(!isArray && !allowedArray) {
@@ -222,6 +228,41 @@ export default class TypeSpec {
222
228
  return false
223
229
  })
224
230
 
231
+ // Check mixed-array groups independently. Each group (e.g.,
232
+ // (String|Number)[] vs (Boolean|Bigint)[]) is validated separately
233
+ // so that multiple groups don't merge into one.
234
+ if(isArray) {
235
+ const mixedSpecs = this.filter(spec => spec.mixed !== false)
236
+
237
+ if(mixedSpecs.length) {
238
+ const empty = Data.isEmpty(value)
239
+
240
+ if(empty)
241
+ return allowEmpty ? [...matchingTypeSpec, ...mixedSpecs] : []
242
+
243
+ // Collect unique group IDs
244
+ const groups = [...new Set(mixedSpecs.map(s => s.mixed))]
245
+
246
+ for(const gid of groups) {
247
+ const groupSpecs = mixedSpecs.filter(s => s.mixed === gid)
248
+
249
+ const allMatch = value.every(element => {
250
+ const elType = Data.typeOf(element)
251
+
252
+ return groupSpecs.some(spec => {
253
+ if(spec.typeName === "Object")
254
+ return Data.isPlainObject(element)
255
+
256
+ return elType === spec.typeName
257
+ })
258
+ })
259
+
260
+ if(allMatch)
261
+ return [...matchingTypeSpec, ...groupSpecs]
262
+ }
263
+ }
264
+ }
265
+
225
266
  return matchingTypeSpec
226
267
  }
227
268
 
@@ -231,29 +272,64 @@ export default class TypeSpec {
231
272
  *
232
273
  * @private
233
274
  * @param {string} string - The type specification string to parse
234
- * @param {TypeSpecOptions} [options] - Parsing options
235
275
  * @throws {Sass} If the type specification is invalid
236
276
  */
237
- #parse(string, options={delimiter: "|"}) {
238
- const delimiter = options?.delimiter ?? "|"
239
- const parts = string.split(delimiter)
277
+ #parse(string) {
278
+ const specs = []
279
+ const groupPattern = /\((\w+(?:\|\w+)*)\)\[\]/g
280
+
281
+ // Replace groups with placeholder X to validate structure and
282
+ // determine parse order
283
+ const groups = []
284
+ const stripped = string.replace(groupPattern, (_, inner) => {
285
+ groups.push(inner)
286
+
287
+ return "X"
288
+ })
289
+
290
+ // Validate for malformed delimiters and missing boundaries
291
+ if(/\|\||^\||\|$/.test(stripped) || /[^|]X|X[^|]/.test(stripped))
292
+ throw Sass.new(`Invalid type: ${string}`)
293
+
294
+ // Parse in order using the stripped template
295
+ const segments = stripped.split("|")
296
+ let groupId = 0
297
+
298
+ for(const segment of segments) {
299
+ if(segment === "X") {
300
+ const currentGroup = groupId++
301
+ const inner = groups[currentGroup]
240
302
 
241
- this.#specs = parts.map(part => {
242
- const typeMatches = /^(\w+)(\[\])?$/.exec(part)
303
+ for(const raw of inner.split("|")) {
304
+ const typeName = Util.capitalize(raw)
305
+
306
+ if(!Data.isValidType(typeName))
307
+ throw Sass.new(`Invalid type: ${raw}`)
308
+
309
+ specs.push({typeName, array: true, mixed: currentGroup})
310
+ }
311
+
312
+ continue
313
+ }
314
+
315
+ const typeMatches = /^(\w+)(\[\])?$/.exec(segment)
243
316
 
244
317
  if(!typeMatches || typeMatches.length !== 3)
245
- throw Sass.new(`Invalid type: ${part}`)
318
+ throw Sass.new(`Invalid type: ${segment}`)
246
319
 
247
320
  const typeName = Util.capitalize(typeMatches[1])
248
321
 
249
322
  if(!Data.isValidType(typeName))
250
323
  throw Sass.new(`Invalid type: ${typeMatches[1]}`)
251
324
 
252
- return {
325
+ specs.push({
253
326
  typeName,
254
327
  array: typeMatches[2] === "[]",
255
- }
256
- })
328
+ mixed: false,
329
+ })
330
+ }
331
+
332
+ this.#specs = specs
257
333
  }
258
334
 
259
335
  #getTypeLineage(value) {
package/src/node/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // Browser-compatible utilities (pure JS)
2
2
  export {default as Collection} from "../browser/lib/Collection.js"
3
- export {default as Data} from "../browser/lib/Data.js"
3
+ export {default as Data} from "./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
6
  export {default as Promised} from "../browser/lib/Promised.js"
@@ -23,3 +23,4 @@ export {default as Glog} from "./lib/Glog.js"
23
23
  export {default as Notify} from "./lib/Notify.js"
24
24
  export {Notify as NotifyClass} from "./lib/Notify.js"
25
25
  export {default as Term} from "./lib/Term.js"
26
+ export {default as Watcher, OverFlowBehaviour} from "./lib/Watcher.js"
@@ -1,32 +1,97 @@
1
+ import Valid from "../../browser/lib/Valid.js"
2
+ import Data from "./Data.js"
1
3
  import Sass from "./Sass.js"
2
4
 
5
+ /**
6
+ * @import FileObject from "./FileObject.js"
7
+ */
8
+
9
+ /**
10
+ * @typedef {"raw" | "structured"} CacheDataType
11
+ */
12
+
13
+ /**
14
+ * @typedef {{modified: Date, raw: string|null, structured: unknown}} CacheData
15
+ */
16
+
3
17
  /**
4
18
  * File system cache with automatic invalidation based on modification time.
5
19
  * Provides intelligent caching of parsed JSON5/YAML files with mtime-based
6
20
  * cache invalidation to optimize performance for repeated file access.
7
21
  *
8
- * The cache eliminates redundant file reads and parsing when multiple processes
9
- * access the same dependency files, while ensuring data freshness through
10
- * modification time checking.
22
+ * The cache eliminates redundant file reads and parsing when multiple
23
+ * processes access the same dependency files, while ensuring data freshness
24
+ * through modification time checking.
11
25
  */
12
26
  export default class Cache {
13
- /** @type {Map<string, Date>} Map of file paths to last modification times */
14
- #modifiedTimes = new Map()
15
- /** @type {Map<string, object>} Map of file paths to parsed file data */
16
- #dataCache = new Map()
27
+ /** @type {Map<string, CacheData>} Map of file paths to cached data */
28
+ #cache = new Map()
17
29
 
18
30
  /**
19
- * Removes cached data for a specific file from both time and data maps.
31
+ * Removes cached data for a specific file from the #cache map.
20
32
  * Used when files are modified or when cache consistency needs to be
21
33
  * maintained.
22
34
  *
23
35
  * @private
24
- * @param {import("./FileObject.js").FileObject} file - The file object to remove from cache
25
- * @returns {void}
36
+ * @param {FileObject} file - The file object to remove from cache
37
+ * @returns {undefined}
26
38
  */
27
39
  #cleanup(file) {
28
- this.#modifiedTimes.delete(file.path)
29
- this.#dataCache.delete(file.path)
40
+ this.#cache.delete(file.path)
41
+ }
42
+
43
+ /**
44
+ * Internal cache loader that reads raw content via FileObject and
45
+ * optionally parses it, using mtime-based invalidation to serve cached
46
+ * results when possible.
47
+ *
48
+ * @private
49
+ * @param {FileObject} fileObject - The file object to load
50
+ * @param {CacheDataType} kind - Whether to return "raw" text or
51
+ * "structured" parsed data
52
+ * @param {object} [options] - Options forwarded to read/parse
53
+ * @param {string} [options.encoding="utf8"] - File encoding
54
+ * @param {string} [options.type="any"] - Data format for parsing
55
+ * @returns {Promise<unknown>} The cached or freshly loaded data
56
+ * @throws {Sass} If the file does not exist
57
+ */
58
+ async #loadFromCache(fileObject, kind, options={}) {
59
+ Valid.assert(kind === "raw" || kind === "structured",
60
+ "Cache data type must be 'raw' or 'structured'.")
61
+
62
+ const lastModified = await fileObject.modified()
63
+
64
+ if(lastModified === null)
65
+ throw Sass.new(`No such file '${fileObject}'`)
66
+
67
+ const rec = this.#cache.get(fileObject.path) ?? Object.seal({
68
+ modified: new Date(0),
69
+ raw: null,
70
+ structured: null,
71
+ })
72
+
73
+ if(lastModified.getTime() === rec.modified.getTime()) {
74
+ if(kind === "raw" && rec.raw !== null)
75
+ return rec.raw
76
+
77
+ if(kind === "structured" && rec.structured !== null)
78
+ return rec.structured
79
+ }
80
+
81
+ this.#cache.set(fileObject.path, rec)
82
+ rec.modified = lastModified
83
+ rec.raw = await fileObject.read({
84
+ encoding: options.encoding,
85
+ skipCache: true,
86
+ })
87
+ rec.structured = null
88
+
89
+ if(kind === "raw")
90
+ return rec.raw
91
+
92
+ rec.structured = Data.textAsData(rec.raw, options.type)
93
+
94
+ return rec.structured
30
95
  }
31
96
 
32
97
  /**
@@ -38,35 +103,40 @@ export default class Cache {
38
103
  * freshness while optimizing performance for repeated file access during
39
104
  * parallel processing.
40
105
  *
41
- * @param {import("./FileObject.js").FileObject} fileObject - The file object to load and cache
106
+ * @param {FileObject} fileObject - The file object to load and cache
42
107
  * @returns {Promise<unknown>} The parsed file data (JSON5 or YAML)
43
108
  * @throws {Sass} If the file cannot be found or accessed
44
109
  */
45
- async loadCachedData(fileObject) {
46
- const lastModified = await fileObject.modified()
110
+ async loadDataFromCache(fileObject, options={}) {
111
+ Valid.type(fileObject, "FileObject")
47
112
 
48
- if(lastModified === null)
49
- throw Sass.new(`Unable to find file '${fileObject.path}'`)
50
-
51
- if(this.#modifiedTimes.has(fileObject.path)) {
52
- const lastCached = this.#modifiedTimes.get(fileObject.path)
53
-
54
- if(lastModified > lastCached) {
55
- this.#cleanup(fileObject)
56
- } else {
57
- if(!(this.#dataCache.has(fileObject.path)))
58
- this.#cleanup(fileObject)
59
- else {
60
- return this.#dataCache.get(fileObject.path)
61
- }
62
- }
63
- }
113
+ return await this.#loadFromCache(
114
+ fileObject, "structured", options)
115
+ }
116
+
117
+ /**
118
+ * Loads and caches raw file content with automatic mtime-based
119
+ * invalidation.
120
+ *
121
+ * @param {FileObject} fileObject - The file object to read and cache
122
+ * @returns {Promise<string>} The raw file content
123
+ * @throws {Sass} If the file cannot be found or accessed
124
+ */
125
+ async loadFromCache(fileObject, options={}) {
126
+ Valid.type(fileObject, "FileObject")
64
127
 
65
- const data = await fileObject.loadData()
128
+ return await this.#loadFromCache(
129
+ fileObject, "raw", options)
130
+ }
66
131
 
67
- this.#modifiedTimes.set(fileObject.path, lastModified)
68
- this.#dataCache.set(fileObject.path, data)
132
+ /**
133
+ * Clears cached data for a specific file from both time and data maps.
134
+ *
135
+ * @param {import("./FileObject.js").default} file - The file object to clear from cache
136
+ */
137
+ resetCache(file) {
138
+ Valid.type(file, "FileObject")
69
139
 
70
- return data
140
+ this.#cleanup(file)
71
141
  }
72
142
  }
@@ -0,0 +1,49 @@
1
+ import JSON5 from "json5"
2
+ import YAML from "yaml"
3
+ import BrowserData from "../../browser/lib/Data.js"
4
+ import Sass from "./Sass.js"
5
+
6
+ /**
7
+ * Node-side extension of Data with parsing utilities that require
8
+ * node-specific dependencies.
9
+ */
10
+ export default class Data extends BrowserData {
11
+ /**
12
+ * Parses text content as structured data (JSON5 or YAML).
13
+ *
14
+ * @param {string} source - The text content to parse
15
+ * @param {string} [type="any"] - The expected format ("json",
16
+ * "json5", "yaml", or "any")
17
+ * @returns {unknown} The parsed data
18
+ * @throws {Sass} If content cannot be parsed or type is
19
+ * unsupported
20
+ */
21
+ static textAsData(source, type="any") {
22
+ const normalizedType = type.toLowerCase()
23
+ const toTry = {
24
+ json5: [JSON5],
25
+ json: [JSON5],
26
+ yaml: [YAML],
27
+ any: [JSON5, YAML],
28
+ }[normalizedType]
29
+
30
+ if(!toTry) {
31
+ throw Sass.new(
32
+ `Unsupported data type '${type}'.`
33
+ + ` Supported types: json, json5, yaml.`)
34
+ }
35
+
36
+ for(const format of toTry) {
37
+ try {
38
+ const result = format.parse(source)
39
+
40
+ return result
41
+ } catch {
42
+ // nothing to see here
43
+ }
44
+ }
45
+
46
+ throw Sass.new(
47
+ `Content is neither valid JSON5 nor valid YAML.`)
48
+ }
49
+ }
@@ -37,7 +37,8 @@ import Valid from "./Valid.js"
37
37
  * @property {URL|null} url - The directory URL
38
38
  */
39
39
 
40
- /** * DirectoryObject encapsulates metadata and operations for a directory,
40
+ /**
41
+ * DirectoryObject encapsulates metadata and operations for a directory,
41
42
  * providing immutable path resolution, existence checks, and content enumeration.
42
43
  *
43
44
  * Features:
@@ -480,7 +481,7 @@ export default class DirectoryObject extends FS {
480
481
  *
481
482
  * @async
482
483
  * @param {object} [options] - Options to pass to fs.mkdir (e.g., {recursive: true, mode: 0o755})
483
- * @returns {Promise<void>}
484
+ * @returns {Promise<undefined>}
484
485
  * @throws {Sass} If directory creation fails for reasons other than already existing
485
486
  * @example
486
487
  * // Create directory recursively
@@ -555,7 +556,7 @@ export default class DirectoryObject extends FS {
555
556
  * a directory with contents, you must imperatively decide your deletion
556
557
  * strategy and handle it explicitly.
557
558
  *
558
- * @returns {Promise<void>} Resolves when directory is deleted
559
+ * @returns {Promise<undefined>} Resolves when directory is deleted
559
560
  * @throws {Sass} If the directory URL is invalid
560
561
  * @throws {Sass} If the directory does not exist
561
562
  * @throws {Error} If the directory is not empty (from fs.rmdir)
@@ -629,8 +630,6 @@ export default class DirectoryObject extends FS {
629
630
  getDirectory(newPath) {
630
631
  Valid.type(newPath, "String", {allowEmpty: false})
631
632
 
632
- // New direction: every path is relative to THIS path. Absolute?
633
- // ../../../..? up to this path and then down again if required.
634
633
  const thisPath = this.path
635
634
  const merged = FS.mergeOverlappingPaths(thisPath, newPath)
636
635
  const resolved = FS.resolvePath(thisPath, merged)
@@ -679,8 +678,6 @@ export default class DirectoryObject extends FS {
679
678
  getFile(filename) {
680
679
  Valid.type(filename, "String", {allowEmpty: false})
681
680
 
682
- // Every path is relative to THIS path. Absolute or .. paths
683
- // are constrained to this directory.
684
681
  const thisPath = this.path
685
682
  const merged = FS.mergeOverlappingPaths(thisPath, filename)
686
683
  const resolved = FS.resolvePath(thisPath, merged)