@gesslar/sassy 0.20.2 → 0.21.1
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 +12 -8
- package/src/BuildCommand.js +8 -9
- package/src/Cache.js +1 -0
- package/src/Colour.js +8 -2
- package/src/Command.js +93 -32
- package/src/Compiler.js +62 -36
- package/src/Data.js +26 -16
- package/src/Evaluator.js +60 -7
- package/src/File.js +14 -2
- package/src/LintCommand.js +421 -185
- package/src/ResolveCommand.js +60 -29
- package/src/Sass.js +2 -1
- package/src/Session.js +202 -36
- package/src/Term.js +7 -7
- package/src/Theme.js +394 -54
- package/src/ThemePool.js +1 -1
- package/src/ThemeToken.js +1 -0
- package/src/Type.js +11 -10
- package/src/Util.js +2 -2
- package/src/Valid.js +1 -1
- package/src/cli.js +28 -15
package/src/Data.js
CHANGED
|
@@ -59,7 +59,10 @@ export default class Data {
|
|
|
59
59
|
*
|
|
60
60
|
* @type {string[]}
|
|
61
61
|
*/
|
|
62
|
-
static dataTypes = Object.freeze([
|
|
62
|
+
static dataTypes = Object.freeze([
|
|
63
|
+
...Data.primitives,
|
|
64
|
+
...Data.constructors.map(c => c.toLowerCase())
|
|
65
|
+
])
|
|
63
66
|
|
|
64
67
|
/**
|
|
65
68
|
* Array of type names that can be checked for emptiness.
|
|
@@ -155,12 +158,13 @@ export default class Data {
|
|
|
155
158
|
*
|
|
156
159
|
* @param {Array} arr - The array to pad.
|
|
157
160
|
* @param {number} length - The length to pad the array to.
|
|
158
|
-
* @param {
|
|
161
|
+
* @param {unknown} value - The value to pad the array with.
|
|
159
162
|
* @param {number} position - The position to pad the array at.
|
|
160
163
|
* @returns {Array} The padded array.
|
|
161
164
|
*/
|
|
162
165
|
static arrayPad(arr, length, value, position = 0) {
|
|
163
166
|
const diff = length - arr.length
|
|
167
|
+
|
|
164
168
|
if(diff <= 0)
|
|
165
169
|
return arr
|
|
166
170
|
|
|
@@ -199,8 +203,8 @@ export default class Data {
|
|
|
199
203
|
/**
|
|
200
204
|
* Allocates an object from a source array and a spec array or function.
|
|
201
205
|
*
|
|
202
|
-
* @param {
|
|
203
|
-
* @param {
|
|
206
|
+
* @param {unknown} source The source array
|
|
207
|
+
* @param {Array|function(unknown): unknown} spec The spec array or function
|
|
204
208
|
* @returns {Promise<object>} The allocated object
|
|
205
209
|
*/
|
|
206
210
|
static async allocateObject(source, spec) {
|
|
@@ -250,7 +254,7 @@ export default class Data {
|
|
|
250
254
|
* Maps an object using a transformer function
|
|
251
255
|
*
|
|
252
256
|
* @param {object} original The original object
|
|
253
|
-
* @param {
|
|
257
|
+
* @param {function(unknown): unknown} transformer The transformer function
|
|
254
258
|
* @param {boolean} mutate Whether to mutate the original object
|
|
255
259
|
* @returns {Promise<object>} The mapped object
|
|
256
260
|
*/
|
|
@@ -294,13 +298,15 @@ export default class Data {
|
|
|
294
298
|
/**
|
|
295
299
|
* Checks if a value is of a specified type
|
|
296
300
|
*
|
|
297
|
-
* @param {
|
|
301
|
+
* @param {unknown} value The value to check
|
|
298
302
|
* @param {string|TypeSpec} type The type to check for
|
|
299
303
|
* @param {object} options Additional options for checking
|
|
300
304
|
* @returns {boolean} Whether the value is of the specified type
|
|
301
305
|
*/
|
|
302
306
|
static isType(value, type, options = {}) {
|
|
303
|
-
const typeSpec = type instanceof TypeSpec
|
|
307
|
+
const typeSpec = type instanceof TypeSpec
|
|
308
|
+
? type
|
|
309
|
+
: Data.newTypeSpec(type, options)
|
|
304
310
|
|
|
305
311
|
return typeSpec.match(value, options)
|
|
306
312
|
}
|
|
@@ -320,7 +326,7 @@ export default class Data {
|
|
|
320
326
|
* function does not parse the type string, and only checks for primitive
|
|
321
327
|
* or constructor types.
|
|
322
328
|
*
|
|
323
|
-
* @param {
|
|
329
|
+
* @param {unknown} value - The value to check
|
|
324
330
|
* @param {string} type - The type to check for
|
|
325
331
|
* @returns {boolean} Whether the value is of the specified type
|
|
326
332
|
*/
|
|
@@ -359,7 +365,7 @@ export default class Data {
|
|
|
359
365
|
/**
|
|
360
366
|
* Returns the type of a value, whether it be a primitive, object, or function.
|
|
361
367
|
*
|
|
362
|
-
* @param {
|
|
368
|
+
* @param {unknown} value - The value to check
|
|
363
369
|
* @returns {string} The type of the value
|
|
364
370
|
*/
|
|
365
371
|
static typeOf(value) {
|
|
@@ -369,7 +375,7 @@ export default class Data {
|
|
|
369
375
|
/**
|
|
370
376
|
* Checks a value is undefined or null.
|
|
371
377
|
*
|
|
372
|
-
* @param {
|
|
378
|
+
* @param {unknown} value The value to check
|
|
373
379
|
* @returns {boolean} Whether the value is undefined or null
|
|
374
380
|
*/
|
|
375
381
|
static isNothing(value) {
|
|
@@ -380,7 +386,7 @@ export default class Data {
|
|
|
380
386
|
* Checks if a value is empty. This function is used to check if an object,
|
|
381
387
|
* array, or string is empty. Null and undefined values are considered empty.
|
|
382
388
|
*
|
|
383
|
-
* @param {
|
|
389
|
+
* @param {unknown} value The value to check
|
|
384
390
|
* @param {boolean} checkForNothing Whether to check for null or undefined
|
|
385
391
|
* values
|
|
386
392
|
* @returns {boolean} Whether the value is empty
|
|
@@ -423,7 +429,7 @@ export default class Data {
|
|
|
423
429
|
const value = obj[name]
|
|
424
430
|
|
|
425
431
|
// Recursively freeze nested objects
|
|
426
|
-
if(
|
|
432
|
+
if(typeof value === "object" && value !== null)
|
|
427
433
|
Data.deepFreezeObject(value)
|
|
428
434
|
}
|
|
429
435
|
|
|
@@ -442,8 +448,10 @@ export default class Data {
|
|
|
442
448
|
static assureObjectPath(obj, keys) {
|
|
443
449
|
let current = obj // a moving reference to internal objects within obj
|
|
444
450
|
const len = keys.length
|
|
451
|
+
|
|
445
452
|
for(let i = 0; i < len; i++) {
|
|
446
453
|
const elem = keys[i]
|
|
454
|
+
|
|
447
455
|
if(!current[elem])
|
|
448
456
|
current[elem] = {}
|
|
449
457
|
|
|
@@ -459,8 +467,8 @@ export default class Data {
|
|
|
459
467
|
* the structure if it does not exist.
|
|
460
468
|
*
|
|
461
469
|
* @param {object} obj - The target object to set the value in
|
|
462
|
-
* @param {string
|
|
463
|
-
* @param {
|
|
470
|
+
* @param {Array<string>} keys - Array of keys representing the path to the target property
|
|
471
|
+
* @param {unknown} value - The value to set at the target location
|
|
464
472
|
*/
|
|
465
473
|
static setNestedValue(obj, keys, value) {
|
|
466
474
|
const nested = Data.assureObjectPath(obj, keys.slice(0, -1))
|
|
@@ -475,7 +483,8 @@ export default class Data {
|
|
|
475
483
|
* @returns {object} The merged object
|
|
476
484
|
*/
|
|
477
485
|
static mergeObject(...sources) {
|
|
478
|
-
const isObject = obj =>
|
|
486
|
+
const isObject = obj => typeof obj === "object" && obj !== null && !Array.isArray(obj)
|
|
487
|
+
|
|
479
488
|
return sources.reduce((acc, obj) => {
|
|
480
489
|
if(!isObject(obj))
|
|
481
490
|
return acc
|
|
@@ -512,11 +521,12 @@ export default class Data {
|
|
|
512
521
|
* Applies the predicate to all items in parallel and returns filtered results.
|
|
513
522
|
*
|
|
514
523
|
* @param {Array} arr - The array to filter
|
|
515
|
-
* @param {
|
|
524
|
+
* @param {function(unknown): Promise<boolean>} predicate - Async predicate function that returns a promise resolving to boolean
|
|
516
525
|
* @returns {Promise<Array>} Promise resolving to the filtered array
|
|
517
526
|
*/
|
|
518
527
|
static async asyncFilter(arr, predicate) {
|
|
519
528
|
const results = await Promise.all(arr.map(predicate))
|
|
529
|
+
|
|
520
530
|
return arr.filter((_, index) => results[index])
|
|
521
531
|
}
|
|
522
532
|
|
package/src/Evaluator.js
CHANGED
|
@@ -56,6 +56,37 @@ export default class Evaluator {
|
|
|
56
56
|
*/
|
|
57
57
|
static func = /(?<captured>(?<func>\w+)\((?<args>[^()]+)\))/
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Extracts a variable name from a string containing variable syntax.
|
|
61
|
+
* Supports $(var), $var, and ${var} patterns.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} [str] - String that may contain a variable reference
|
|
64
|
+
* @returns {string|null} The variable name or null if none found
|
|
65
|
+
*/
|
|
66
|
+
static extractVariableName(str="") {
|
|
67
|
+
const {none, parens, braces} = Evaluator.sub.exec(str)?.groups ?? {}
|
|
68
|
+
|
|
69
|
+
return none || parens || braces || null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Extracts function name and arguments from a string containing function syntax.
|
|
74
|
+
* Supports functionName(args) patterns.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} [str] - String that may contain a function call
|
|
77
|
+
* @returns {{func:string, args:string}|null} Object with {func, args} or null if none found
|
|
78
|
+
*/
|
|
79
|
+
static extractFunctionCall(str="") {
|
|
80
|
+
const match = Evaluator.func.exec(str)
|
|
81
|
+
|
|
82
|
+
if(!match?.groups)
|
|
83
|
+
return null
|
|
84
|
+
|
|
85
|
+
const {func, args} = match.groups
|
|
86
|
+
|
|
87
|
+
return {func, args}
|
|
88
|
+
}
|
|
89
|
+
|
|
59
90
|
#pool = new ThemePool()
|
|
60
91
|
get pool() {
|
|
61
92
|
return this.#pool
|
|
@@ -80,19 +111,34 @@ export default class Evaluator {
|
|
|
80
111
|
* - Input array is mutated in-place (`value` fields change).
|
|
81
112
|
* - No return value. Evident by the absence of a return statement.
|
|
82
113
|
*
|
|
83
|
-
* @param {Array<{flatPath:string,value:
|
|
114
|
+
* @param {Array<{flatPath:string,value:unknown}>} decomposed - Variable entries to resolve.
|
|
115
|
+
* @example
|
|
116
|
+
* // Example decomposed input with variables and theme references
|
|
117
|
+
* const evaluator = new Evaluator();
|
|
118
|
+
* const decomposed = [
|
|
119
|
+
* { flatPath: 'vars.primary', value: '#3366cc' },
|
|
120
|
+
* { flatPath: 'theme.colors.background', value: '$(vars.primary)' },
|
|
121
|
+
* { flatPath: 'theme.colors.accent', value: 'lighten($(vars.primary), 20)' }
|
|
122
|
+
* ];
|
|
123
|
+
* evaluator.evaluate(decomposed);
|
|
124
|
+
* // After evaluation, values are resolved:
|
|
125
|
+
* // decomposed[1].value === '#3366cc'
|
|
126
|
+
* // decomposed[2].value === '#5588dd' (lightened color)
|
|
84
127
|
*/
|
|
85
128
|
evaluate(decomposed) {
|
|
86
129
|
let it = 0
|
|
130
|
+
|
|
87
131
|
do {
|
|
88
132
|
decomposed.forEach(item => {
|
|
89
133
|
const trail = new Array()
|
|
90
134
|
|
|
91
135
|
if(typeof item.value === "string") {
|
|
92
136
|
const raw = item.value
|
|
137
|
+
|
|
93
138
|
item.value = this.#evaluateValue(trail, item.flatPath, raw)
|
|
94
139
|
// Keep lookup in sync with latest resolved value for chained deps.
|
|
95
140
|
const token = this.#pool.findToken(item.flatPath)
|
|
141
|
+
|
|
96
142
|
this.#pool.resolve(item.flatPath, item.value)
|
|
97
143
|
this.#pool.rawResolve(raw, item.value)
|
|
98
144
|
|
|
@@ -137,7 +183,7 @@ export default class Evaluator {
|
|
|
137
183
|
* @param {string} parentTokenKeyString - Key string for parent token.
|
|
138
184
|
* @param {string} value - Raw tokenised string to resolve.
|
|
139
185
|
* @returns {string?} Fully resolved string.
|
|
140
|
-
* @throws If we've reached maximum iterations.
|
|
186
|
+
* @throws {Sass} If we've reached maximum iterations.
|
|
141
187
|
*/
|
|
142
188
|
#evaluateValue(trail, parentTokenKeyString, value) {
|
|
143
189
|
let it = 0
|
|
@@ -194,6 +240,7 @@ export default class Evaluator {
|
|
|
194
240
|
|
|
195
241
|
// Check if this is a color function (like oklch, rgb, hsl, etc.)
|
|
196
242
|
const parsedColor = parse(value)
|
|
243
|
+
|
|
197
244
|
if(parsedColor) {
|
|
198
245
|
token.setParsedColor(parsedColor)
|
|
199
246
|
}
|
|
@@ -225,8 +272,8 @@ export default class Evaluator {
|
|
|
225
272
|
* @returns {ThemeToken|null} The resolved token or null.
|
|
226
273
|
*/
|
|
227
274
|
#resolveVariable(value) {
|
|
228
|
-
const {captured
|
|
229
|
-
const work =
|
|
275
|
+
const {captured} = Evaluator.sub.exec(value).groups
|
|
276
|
+
const work = Evaluator.extractVariableName(value)
|
|
230
277
|
const existing = this.#pool.findToken(work)
|
|
231
278
|
|
|
232
279
|
if(!existing)
|
|
@@ -249,13 +296,19 @@ export default class Evaluator {
|
|
|
249
296
|
* @returns {ThemeToken|null} The resolved token or null.
|
|
250
297
|
*/
|
|
251
298
|
#resolveFunction(value) {
|
|
252
|
-
const {captured
|
|
299
|
+
const {captured} = Evaluator.func.exec(value).groups
|
|
300
|
+
const result = Evaluator.extractFunctionCall(value)
|
|
301
|
+
|
|
302
|
+
if(!result)
|
|
303
|
+
return null
|
|
304
|
+
|
|
305
|
+
const {func, args} = result
|
|
253
306
|
const split = args?.split(",").map(a => a.trim()) ?? []
|
|
254
307
|
|
|
255
308
|
// Look up source tokens for arguments to preserve color space
|
|
256
309
|
const sourceTokens = split.map(arg => {
|
|
257
310
|
return this.#pool.findToken(arg) ||
|
|
258
|
-
this.#pool.getTokens?.get?.(arg) ||
|
|
311
|
+
this.#pool.getTokens()?.get?.(arg) ||
|
|
259
312
|
null
|
|
260
313
|
})
|
|
261
314
|
|
|
@@ -336,7 +389,7 @@ export default class Evaluator {
|
|
|
336
389
|
* Predicate: does this item's value still contain variable or function tokens?
|
|
337
390
|
*
|
|
338
391
|
* @private
|
|
339
|
-
* @param {{value:
|
|
392
|
+
* @param {{value:unknown}} item - Entry to test.
|
|
340
393
|
* @returns {boolean} True if token patterns present.
|
|
341
394
|
*/
|
|
342
395
|
#tokenCheck(item) {
|
package/src/File.js
CHANGED
|
@@ -39,6 +39,7 @@ export default class File {
|
|
|
39
39
|
return url.pathToFileURL(pathName).href
|
|
40
40
|
} catch(e) {
|
|
41
41
|
void e // stfu linter
|
|
42
|
+
|
|
42
43
|
return pathName
|
|
43
44
|
}
|
|
44
45
|
}
|
|
@@ -52,6 +53,7 @@ export default class File {
|
|
|
52
53
|
static async canReadFile(file) {
|
|
53
54
|
try {
|
|
54
55
|
await fs.access(file.path, fs.constants.R_OK)
|
|
56
|
+
|
|
55
57
|
return true
|
|
56
58
|
} catch(_) {
|
|
57
59
|
return false
|
|
@@ -67,6 +69,7 @@ export default class File {
|
|
|
67
69
|
static async canWriteFile(file) {
|
|
68
70
|
try {
|
|
69
71
|
await fs.access(file.path, fs.constants.W_OK)
|
|
72
|
+
|
|
70
73
|
return true
|
|
71
74
|
} catch(_error) {
|
|
72
75
|
return false
|
|
@@ -82,6 +85,7 @@ export default class File {
|
|
|
82
85
|
static async fileExists(file) {
|
|
83
86
|
try {
|
|
84
87
|
await fs.access(file.path, fs.constants.R_OK)
|
|
88
|
+
|
|
85
89
|
return true
|
|
86
90
|
} catch(_) {
|
|
87
91
|
return false
|
|
@@ -97,6 +101,7 @@ export default class File {
|
|
|
97
101
|
static async fileSize(file) {
|
|
98
102
|
try {
|
|
99
103
|
const stat = await fs.stat(file.path)
|
|
104
|
+
|
|
100
105
|
return stat.size
|
|
101
106
|
} catch(_) {
|
|
102
107
|
return null
|
|
@@ -113,6 +118,7 @@ export default class File {
|
|
|
113
118
|
static async fileModified(file) {
|
|
114
119
|
try {
|
|
115
120
|
const stat = await fs.stat(file.path)
|
|
121
|
+
|
|
116
122
|
return stat.mtime
|
|
117
123
|
} catch(_) {
|
|
118
124
|
return null
|
|
@@ -179,8 +185,13 @@ export default class File {
|
|
|
179
185
|
*/
|
|
180
186
|
static async getFiles(glob) {
|
|
181
187
|
Valid.assert(
|
|
182
|
-
(
|
|
183
|
-
|
|
188
|
+
(
|
|
189
|
+
(typeof glob === "string" && glob.length > 0) ||
|
|
190
|
+
(
|
|
191
|
+
Array.isArray(glob) && Data.uniformStringArray(glob) &&
|
|
192
|
+
glob.length > 0
|
|
193
|
+
)
|
|
194
|
+
),
|
|
184
195
|
"glob must be a non-empty string or array of strings.",
|
|
185
196
|
1
|
|
186
197
|
)
|
|
@@ -225,6 +236,7 @@ export default class File {
|
|
|
225
236
|
found.map(async dirent => {
|
|
226
237
|
const fullPath = path.join(directory, dirent.name)
|
|
227
238
|
const stat = await fs.stat(fullPath)
|
|
239
|
+
|
|
228
240
|
return {dirent, stat, fullPath}
|
|
229
241
|
}),
|
|
230
242
|
)
|