@gesslar/toolkit 4.0.0 → 4.1.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 +1 -0
- package/package.json +1 -1
- package/src/browser/lib/Data.js +4 -4
- package/src/browser/lib/TypeSpec.js +115 -39
- package/src/node/index.js +2 -1
- package/src/node/lib/Cache.js +105 -35
- package/src/node/lib/Data.js +49 -0
- package/src/node/lib/DirectoryObject.js +4 -7
- package/src/node/lib/FileObject.js +89 -53
- package/src/node/lib/FileSystem.js +47 -2
- package/src/node/lib/Font.js +1 -1
- package/src/node/lib/Notify.js +6 -6
- package/src/node/lib/Term.js +1 -1
- package/src/node/lib/Util.js +3 -3
- package/src/node/lib/Watcher.js +118 -0
- package/types/browser/lib/Data.d.ts +2 -8
- package/types/browser/lib/Data.d.ts.map +1 -1
- package/types/browser/lib/TypeSpec.d.ts +21 -36
- package/types/browser/lib/TypeSpec.d.ts.map +1 -1
- package/types/node/index.d.ts +2 -1
- package/types/node/lib/Cache.d.ts +36 -5
- package/types/node/lib/Cache.d.ts.map +1 -1
- package/types/node/lib/Data.d.ts +19 -0
- package/types/node/lib/Data.d.ts.map +1 -0
- package/types/node/lib/DirectoryObject.d.ts +6 -5
- package/types/node/lib/DirectoryObject.d.ts.map +1 -1
- package/types/node/lib/FileObject.d.ts +54 -26
- package/types/node/lib/FileObject.d.ts.map +1 -1
- package/types/node/lib/FileSystem.d.ts +19 -0
- package/types/node/lib/FileSystem.d.ts.map +1 -1
- package/types/node/lib/Notify.d.ts +10 -10
- package/types/node/lib/Notify.d.ts.map +1 -1
- package/types/node/lib/Term.d.ts +2 -2
- package/types/node/lib/Term.d.ts.map +1 -1
- package/types/node/lib/Util.d.ts +6 -6
- package/types/node/lib/Util.d.ts.map +1 -1
- package/types/node/lib/Watcher.d.ts +38 -0
- 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
package/src/browser/lib/Data.js
CHANGED
|
@@ -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
|
-
* @
|
|
203
|
-
* @returns {Array<object>} An array of type specs.
|
|
202
|
+
* @returns {TypeSpec} A new TypeSpec instance.
|
|
204
203
|
*/
|
|
205
|
-
static newTypeSpec(string
|
|
206
|
-
return new TypeSpec(string
|
|
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
|
|
24
|
+
constructor(string) {
|
|
40
25
|
this.#specs = []
|
|
41
|
-
this.#parse(string
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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 {
|
|
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 {
|
|
164
|
+
* @param {TypeMatchOptions} [options] - Validation options
|
|
163
165
|
* @returns {Array<object>} Array of matching type specifications
|
|
164
166
|
*/
|
|
165
|
-
match(value,
|
|
166
|
-
|
|
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.
|
|
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
|
|
238
|
-
const
|
|
239
|
-
const
|
|
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
|
-
|
|
242
|
-
|
|
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: ${
|
|
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
|
-
|
|
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 "
|
|
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"
|
package/src/node/lib/Cache.js
CHANGED
|
@@ -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
|
|
9
|
-
* access the same dependency files, while ensuring data freshness
|
|
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,
|
|
14
|
-
#
|
|
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
|
|
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 {
|
|
25
|
-
* @returns {
|
|
36
|
+
* @param {FileObject} file - The file object to remove from cache
|
|
37
|
+
* @returns {undefined}
|
|
26
38
|
*/
|
|
27
39
|
#cleanup(file) {
|
|
28
|
-
this.#
|
|
29
|
-
|
|
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 {
|
|
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
|
|
46
|
-
|
|
110
|
+
async loadDataFromCache(fileObject, options={}) {
|
|
111
|
+
Valid.type(fileObject, "FileObject")
|
|
47
112
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
128
|
+
return await this.#loadFromCache(
|
|
129
|
+
fileObject, "raw", options)
|
|
130
|
+
}
|
|
66
131
|
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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<
|
|
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<
|
|
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)
|