@gesslar/sassy 0.20.0 → 0.21.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/src/Theme.js CHANGED
@@ -22,6 +22,20 @@ import Term from "./Term.js"
22
22
  import ThemePool from "./ThemePool.js"
23
23
  import Util from "./Util.js"
24
24
 
25
+ const outputFileExtension = "color-theme.json"
26
+ const obviouslyASentinelYouCantMissSoShutUpAboutIt = "kakadoodoo"
27
+
28
+ // Symbol enums for magic values
29
+ const WriteStatus = {
30
+ DRY_RUN: Symbol("dry-run"),
31
+ SKIPPED: Symbol("skipped"),
32
+ WRITTEN: Symbol("written")
33
+ }
34
+
35
+ const PropertyKey = {
36
+ CONFIG: Symbol("config")
37
+ }
38
+
25
39
  /**
26
40
  * Theme class: manages the lifecycle of a theme compilation unit.
27
41
  * See file-level docstring for responsibilities.
@@ -30,7 +44,13 @@ export default class Theme {
30
44
  #sourceFile = null
31
45
  #source = null
32
46
  #options = null
33
- #dependencies = []
47
+ /**
48
+ * The dependencies of this theme.
49
+ *
50
+ * @type {Set<Dependency>}
51
+ * @private
52
+ */
53
+ #dependencies = new Set()
34
54
  #lookup = null
35
55
  #pool = null
36
56
  #cache = null
@@ -54,7 +74,7 @@ export default class Theme {
54
74
  constructor(themeFile, cwd, options) {
55
75
  this.#sourceFile = themeFile
56
76
  this.#name = themeFile.module
57
- this.#outputFileName = `${this.#name}.color-theme.json`
77
+ this.#outputFileName = `${this.#name}.${outputFileExtension}`
58
78
  this.#options = options
59
79
  this.#cwd = cwd
60
80
  }
@@ -71,75 +91,223 @@ export default class Theme {
71
91
  this.#pool = null
72
92
  }
73
93
 
74
- get cwd() {
94
+ /**
95
+ * Gets the current working directory.
96
+ *
97
+ * @returns {DirectoryObject} The current working directory
98
+ */
99
+ getCwd() {
75
100
  return this.#cwd
76
101
  }
77
102
 
78
- get options() {
103
+ /**
104
+ * Gets the compilation options.
105
+ *
106
+ * @returns {object} The compilation options object
107
+ */
108
+ getOptions() {
79
109
  return this.#options
80
110
  }
81
111
 
82
- set cache(cache) {
83
- if(!this.cache)
112
+ /**
113
+ * Gets a specific compilation option.
114
+ *
115
+ * @param {string} option - The option name to retrieve
116
+ * @returns {*} The option value or undefined if not set
117
+ */
118
+ getOption(option) {
119
+ return this.#options?.[option] ?? undefined
120
+ }
121
+
122
+ /**
123
+ * Sets the cache instance for theme compilation.
124
+ *
125
+ * @param {Cache} cache - The cache instance to use for file operations
126
+ * @returns {this} Returns this instance for method chaining
127
+ */
128
+ setCache(cache) {
129
+ if(!this.#cache)
84
130
  this.#cache=cache
131
+
132
+ return this
85
133
  }
86
134
 
87
- get cache() {
135
+ /**
136
+ * Gets the cache instance.
137
+ *
138
+ * @returns {Cache|null} The cache instance or null if not set
139
+ */
140
+ getCache() {
88
141
  return this.#cache
89
142
  }
90
143
 
91
- get name() {
144
+ /**
145
+ * Gets the theme name.
146
+ *
147
+ * @returns {string} The theme name derived from the source file
148
+ */
149
+ getName() {
92
150
  return this.#name
93
151
  }
94
152
 
153
+ /**
154
+ * Gets the output file name for the compiled theme.
155
+ *
156
+ * @returns {string} The output file name with extension
157
+ */
158
+ getOutputFileName() {
159
+ return this.#outputFileName
160
+ }
161
+
95
162
  /**
96
163
  * Gets the source file object.
97
164
  *
98
165
  * @returns {FileObject} The source theme file
99
166
  */
100
- get sourceFile() {
167
+ getSourceFile() {
101
168
  return this.#sourceFile
102
169
  }
103
170
 
171
+ /**
172
+ * Sets the compiled theme output object and updates derived JSON and hash.
173
+ *
174
+ * @param {object} data - The compiled theme output object
175
+ * @returns {this} Returns this instance for method chaining
176
+ */
177
+ setOutput(data) {
178
+ this.#output = data
179
+ this.#outputJson = JSON.stringify(data, null, 2) + "\n"
180
+ this.#outputHash = Util.hashOf(this.#outputJson)
181
+
182
+ return this
183
+ }
184
+
104
185
  /**
105
186
  * Gets the compiled theme output object.
106
187
  *
107
188
  * @returns {object|null} The compiled theme output
108
189
  */
109
- get output() {
190
+ getOutput() {
110
191
  return this.#output
111
192
  }
112
193
 
113
194
  /**
114
- * Sets the compiled theme output object and updates derived JSON and hash.
195
+ * Checks if the source has colors defined.
115
196
  *
116
- * @param {object} data - The compiled theme output object
197
+ * @returns {boolean} True if source has theme colors
117
198
  */
118
- set output(data) {
119
- this.#output = data
120
- this.#outputJson = JSON.stringify(data, null, 2) + "\n"
121
- this.#outputHash = Util.hashOf(this.#outputJson)
199
+ sourceHasColors() {
200
+ return !!this.#source?.theme?.colors
201
+ }
202
+
203
+ /**
204
+ * Checks if the source has token colors defined.
205
+ *
206
+ * @returns {boolean} True if source has theme token colors
207
+ */
208
+ sourceHasTokenColors() {
209
+ return !!this.#source?.theme?.tokenColors
210
+ }
211
+
212
+ /**
213
+ * Checks if the source has semantic token colors defined.
214
+ *
215
+ * @returns {boolean} True if source has theme semantic token colors
216
+ */
217
+ sourceHasSemanticTokenColors() {
218
+ return !!this.#source?.theme?.semanticTokenColors
219
+ }
220
+
221
+ /**
222
+ * Checks if the source has theme configuration.
223
+ *
224
+ * @returns {boolean} True if source has theme data
225
+ */
226
+ sourceHasTheme() {
227
+ return !!this.#source?.theme
228
+ }
229
+
230
+ /**
231
+ * Checks if the source has variables.
232
+ *
233
+ * @returns {boolean} True if source has vars section
234
+ */
235
+ sourceHasVars() {
236
+ return !!this.#source?.vars
237
+ }
238
+
239
+ /**
240
+ * Checks if the source has config section.
241
+ *
242
+ * @returns {boolean} True if source has config
243
+ */
244
+ sourceHasConfig() {
245
+ return !!this.#source?.config
246
+ }
247
+
248
+ /**
249
+ * Gets the source colors data.
250
+ *
251
+ * @returns {object|null} The colors object or null if not defined
252
+ */
253
+ getSourceColors() {
254
+ if(!this.sourceHasColors())
255
+ return null
256
+
257
+ return this.#source.theme.colors
258
+ }
259
+
260
+ /**
261
+ * Gets the source token colors data.
262
+ *
263
+ * @returns {Array|null} The token colors array or null if not defined
264
+ */
265
+ getSourceTokenColors() {
266
+ if(!this.sourceHasTokenColors())
267
+ return null
268
+
269
+ return this.#source.theme.tokenColors
270
+ }
271
+
272
+ /**
273
+ * Gets the source semantic token colors data.
274
+ *
275
+ * @returns {object|null} The semantic token colors object or null if not defined
276
+ */
277
+ getSourceSemanticTokenColors() {
278
+ if(!this.sourceHasSemanticTokenColors())
279
+ return null
280
+
281
+ return this.#source.theme.semanticTokenColors
122
282
  }
123
283
 
124
284
  /**
125
285
  * Gets the array of file dependencies.
126
286
  *
127
- * @returns {FileObject[]} Array of dependency files
287
+ * @returns {Set<Dependency>} Array of dependency files
128
288
  */
129
- get dependencies() {
289
+ getDependencies() {
130
290
  return this.#dependencies
131
291
  }
132
292
 
133
293
  /**
134
- * Sets the array of file dependencies.
294
+ * Adds a dependency to the theme with its source data.
135
295
  *
136
- * @param {FileObject[]} data - Array of dependency files
296
+ * @param {FileObject} file - The dependency file object
297
+ * @param {object} source - The parsed source data from the file
298
+ * @returns {this} Returns this instance for method chaining
137
299
  */
138
- set dependencies(data) {
139
- this.#dependencies = data
300
+ addDependency(file, source) {
301
+ this.#dependencies.add(
302
+ new Dependency()
303
+ .setSourceFile(file)
304
+ .setSource(source))
305
+
306
+ return this
307
+ }
140
308
 
141
- if(!this.#dependencies.includes(this.#sourceFile))
142
- this.#dependencies.unshift(this.#sourceFile)
309
+ hasDependencies() {
310
+ return this.#dependencies.size > 0
143
311
  }
144
312
 
145
313
  /**
@@ -147,7 +315,7 @@ export default class Theme {
147
315
  *
148
316
  * @returns {object|null} The parsed source data
149
317
  */
150
- get source() {
318
+ getSource() {
151
319
  return this.#source
152
320
  }
153
321
 
@@ -156,7 +324,7 @@ export default class Theme {
156
324
  *
157
325
  * @returns {object|null} The lookup data object
158
326
  */
159
- get lookup() {
327
+ getLookup() {
160
328
  return this.#lookup
161
329
  }
162
330
 
@@ -164,9 +332,12 @@ export default class Theme {
164
332
  * Sets the variable lookup data for theme compilation.
165
333
  *
166
334
  * @param {object} data - The lookup data object
335
+ * @returns {this} Returns this instance for method chaining
167
336
  */
168
- set lookup(data) {
337
+ setLookup(data) {
169
338
  this.#lookup = data
339
+
340
+ return this
170
341
  }
171
342
 
172
343
  /**
@@ -175,7 +346,7 @@ export default class Theme {
175
346
  *
176
347
  * @returns {ThemePool|null} The pool for this theme.
177
348
  */
178
- get pool() {
349
+ getPool() {
179
350
  return this.#pool
180
351
  }
181
352
 
@@ -186,13 +357,14 @@ export default class Theme {
186
357
  * @see reset
187
358
  *
188
359
  * @param {ThemePool} pool - The pool to assign to this theme
189
- * @throws If there is already a pool.
360
+ * @throws {Error} If there is already a pool.
361
+ * @returns {this} Returns this instance for method chaining
190
362
  */
191
- set pool(pool) {
192
- if(this.#pool)
193
- throw Sass.new("Cannot override existing pool.")
363
+ setPool(pool) {
364
+ if(!this.#pool)
365
+ this.#pool = pool
194
366
 
195
- this.#pool = pool
367
+ return this
196
368
  }
197
369
 
198
370
  /**
@@ -204,36 +376,115 @@ export default class Theme {
204
376
  return this.#pool instanceof ThemePool
205
377
  }
206
378
 
379
+ /**
380
+ * Checks if the theme has compiled output.
381
+ *
382
+ * @returns {boolean} True if theme has been compiled
383
+ */
384
+ hasOutput() {
385
+ return this.#output !== null
386
+ }
387
+
388
+ /**
389
+ * Checks if the theme has loaded source data.
390
+ *
391
+ * @returns {boolean} True if source data is available
392
+ */
393
+ hasSource() {
394
+ return this.#source !== null
395
+ }
396
+
397
+ /**
398
+ * Checks if the theme has a cache instance.
399
+ *
400
+ * @returns {boolean} True if cache is available
401
+ */
402
+ hasCache() {
403
+ return this.#cache !== null
404
+ }
405
+
406
+ /**
407
+ * Checks if the theme has lookup data.
408
+ *
409
+ * @returns {boolean} True if lookup data exists
410
+ */
411
+ hasLookup() {
412
+ return this.#lookup !== null
413
+ }
414
+
415
+ /**
416
+ * Checks if the theme is ready to be compiled.
417
+ * Requires source data and cache to be available.
418
+ *
419
+ * @returns {boolean} True if theme can be compiled
420
+ */
421
+ isReady() {
422
+ return this.hasSource() && this.hasCache()
423
+ }
424
+
425
+ /**
426
+ * Checks if the theme has been fully compiled.
427
+ * Requires output, pool, and lookup data to be present.
428
+ *
429
+ * @returns {boolean} True if theme is fully compiled
430
+ */
431
+ isCompiled() {
432
+ return this.hasOutput() && this.hasPool() && this.hasLookup()
433
+ }
434
+
435
+ /**
436
+ * Checks if the theme can be built/compiled.
437
+ * Same as isReady() but with more semantic naming.
438
+ *
439
+ * @returns {boolean} True if build can proceed
440
+ */
441
+ canBuild() {
442
+ return this.isReady()
443
+ }
444
+
445
+ /**
446
+ * Checks if the theme can be written to output.
447
+ * Requires the theme to be compiled.
448
+ *
449
+ * @returns {boolean} True if write can proceed
450
+ */
451
+ canWrite() {
452
+ return this.hasOutput()
453
+ }
454
+
455
+ /**
456
+ * Checks if the theme is in a valid state for operation.
457
+ * Basic validation that core properties are set.
458
+ *
459
+ * @returns {boolean} True if theme state is valid
460
+ */
461
+ isValid() {
462
+ return this.#sourceFile !== null && this.#name !== null
463
+ }
464
+
207
465
  /**
208
466
  * Loads and parses the theme source file.
209
467
  * Validates that the source contains required configuration.
468
+ * Skips loading if no cache is available (extension use case).
210
469
  *
211
470
  * @returns {Promise<this>} Returns this instance for method chaining
212
471
  * @throws {Sass} If source file lacks required 'config' property
213
472
  */
214
473
  async load() {
474
+ // Skip loading if no cache (extension use case)
475
+ if(!this.#cache)
476
+ return this
477
+
215
478
  const source = await this.#cache.loadCachedData(this.#sourceFile)
216
479
 
217
- if(!source.config)
480
+ if(!source[PropertyKey.CONFIG.description])
218
481
  throw Sass.new(
219
- `Source file does not contain 'config' property: ${this.#sourceFile.path}`
482
+ `Source file does not contain '${PropertyKey.CONFIG.description}' property: ${this.#sourceFile.path}`
220
483
  )
221
484
 
222
485
  this.#source = source
223
- }
224
486
 
225
- /**
226
- * Adds a file dependency to the theme.
227
- *
228
- * @param {FileObject} file - The file to add as a dependency
229
- * @returns {this} Returns this instance for method chaining
230
- * @throws {Sass} If the file parameter is not a valid file
231
- */
232
- addDependency(file) {
233
- if(!file.isFile)
234
- throw Sass.new("File must be a dependency.")
235
-
236
- this.#dependencies.push(file)
487
+ this.addDependency(this.#sourceFile, this.#source)
237
488
 
238
489
  return this
239
490
  }
@@ -242,11 +493,14 @@ export default class Theme {
242
493
  * Builds the theme by compiling source data into final output.
243
494
  * Main entry point for theme compilation process.
244
495
  *
245
- * @returns {Promise<void>} Resolves when build is complete.
496
+ * @returns {Promise<this>} Returns this instance for method chaining
246
497
  */
247
498
  async build() {
248
499
  const compiler = new Compiler()
500
+
249
501
  await compiler.compile(this)
502
+
503
+ return this
250
504
  }
251
505
 
252
506
  /**
@@ -264,7 +518,7 @@ export default class Theme {
264
518
  if(this.#options.dryRun) {
265
519
  Term.log(this.#outputJson)
266
520
 
267
- return {status: "dry-run", file}
521
+ return {status: WriteStatus.DRY_RUN, file}
268
522
  }
269
523
 
270
524
  // Skip identical bytes
@@ -272,10 +526,10 @@ export default class Theme {
272
526
  const nextHash = this.#outputHash
273
527
  const lastHash = await file.exists
274
528
  ? Util.hashOf(await File.readFile(file))
275
- : "kakadoodoo"
529
+ : obviouslyASentinelYouCantMissSoShutUpAboutIt
276
530
 
277
531
  if(lastHash === nextHash)
278
- return {status: "skipped", file}
532
+ return {status: WriteStatus.SKIPPED, file}
279
533
  }
280
534
 
281
535
  // Real write (timed)
@@ -284,6 +538,77 @@ export default class Theme {
284
538
 
285
539
  await File.writeFile(file, output)
286
540
 
287
- return {status: "written", bytes: output.length, file}
541
+ return {status: WriteStatus.WRITTEN, bytes: output.length, file}
542
+ }
543
+ }
544
+
545
+ export class Dependency {
546
+ #sourceFile = null
547
+ #source = null
548
+
549
+ /**
550
+ * Sets the file object for this dependency.
551
+ *
552
+ * @param {FileObject} file - The file object of this dependency.
553
+ * @returns {this} This.
554
+ */
555
+ setSourceFile(file) {
556
+ if(!this.#sourceFile)
557
+ this.#sourceFile = file
558
+
559
+ return this
560
+ }
561
+
562
+ /**
563
+ * Get the file object for this depenency.
564
+ *
565
+ * @returns {FileObject} The file object of this dependency.
566
+ */
567
+ getSourceFile() {
568
+ return this.#sourceFile
569
+ }
570
+
571
+ /**
572
+ * Sets the source object for this dependency.
573
+ *
574
+ * @param {object} source - The parsed JSON from the file after loading.
575
+ * @returns {this} This.
576
+ */
577
+ setSource(source) {
578
+ if(!this.#source)
579
+ this.#source = source
580
+
581
+ return this
582
+ }
583
+
584
+ getSource() {
585
+ return this.#source
586
+ }
587
+
588
+ /**
589
+ * Checks if the dependency has a source file.
590
+ *
591
+ * @returns {boolean} True if source file is set
592
+ */
593
+ hasSourceFile() {
594
+ return this.#sourceFile !== null
595
+ }
596
+
597
+ /**
598
+ * Checks if the dependency has parsed source data.
599
+ *
600
+ * @returns {boolean} True if source data is available
601
+ */
602
+ hasSource() {
603
+ return this.#source !== null
604
+ }
605
+
606
+ /**
607
+ * Checks if the dependency is fully initialized.
608
+ *
609
+ * @returns {boolean} True if both file and source are set
610
+ */
611
+ isComplete() {
612
+ return this.hasSourceFile() && this.hasSource()
288
613
  }
289
614
  }
package/src/ThemePool.js CHANGED
@@ -25,7 +25,7 @@ export default class ThemePool {
25
25
  *
26
26
  * @returns {Map<string, ThemeToken>} Map of tokens to their children.
27
27
  */
28
- get getTokens() {
28
+ getTokens() {
29
29
  return this.#tokens
30
30
  }
31
31
 
package/src/ThemeToken.js CHANGED
@@ -229,6 +229,7 @@ export default class ThemeToken {
229
229
  */
230
230
  setParsedColor(parsedColor) {
231
231
  this.#parsedColor = parsedColor
232
+
232
233
  return this
233
234
  }
234
235
 
package/src/Type.js CHANGED
@@ -60,7 +60,7 @@ export default class TypeSpec {
60
60
  /**
61
61
  * Executes a provided function once for each type specification.
62
62
  *
63
- * @param {Function} callback - Function to execute for each spec
63
+ * @param {function(unknown): void} callback - Function to execute for each spec
64
64
  */
65
65
  forEach(callback) {
66
66
  this.#specs.forEach(callback)
@@ -69,7 +69,7 @@ export default class TypeSpec {
69
69
  /**
70
70
  * Tests whether all type specifications pass the provided test function.
71
71
  *
72
- * @param {Function} callback - Function to test each spec
72
+ * @param {function(unknown): boolean} callback - Function to test each spec
73
73
  * @returns {boolean} True if all specs pass the test
74
74
  */
75
75
  every(callback) {
@@ -79,7 +79,7 @@ export default class TypeSpec {
79
79
  /**
80
80
  * Tests whether at least one type specification passes the provided test function.
81
81
  *
82
- * @param {Function} callback - Function to test each spec
82
+ * @param {function(unknown): boolean} callback - Function to test each spec
83
83
  * @returns {boolean} True if at least one spec passes the test
84
84
  */
85
85
  some(callback) {
@@ -89,7 +89,7 @@ export default class TypeSpec {
89
89
  /**
90
90
  * Creates a new array with all type specifications that pass the provided test function.
91
91
  *
92
- * @param {Function} callback - Function to test each spec
92
+ * @param {function(unknown): boolean} callback - Function to test each spec
93
93
  * @returns {Array} New array with filtered specs
94
94
  */
95
95
  filter(callback) {
@@ -99,7 +99,7 @@ export default class TypeSpec {
99
99
  /**
100
100
  * Creates a new array populated with the results of calling the provided function on every spec.
101
101
  *
102
- * @param {Function} callback - Function to call on each spec
102
+ * @param {function(unknown): unknown} callback - Function to call on each spec
103
103
  * @returns {Array} New array with mapped values
104
104
  */
105
105
  map(callback) {
@@ -109,9 +109,9 @@ export default class TypeSpec {
109
109
  /**
110
110
  * Executes a reducer function on each spec, resulting in a single output value.
111
111
  *
112
- * @param {Function} callback - Function to execute on each spec
113
- * @param {*} initialValue - Initial value for the accumulator
114
- * @returns {*} The final accumulated value
112
+ * @param {function(unknown, unknown): unknown} callback - Function to execute on each spec
113
+ * @param {unknown} initialValue - Initial value for the accumulator
114
+ * @returns {unknown} The final accumulated value
115
115
  */
116
116
  reduce(callback, initialValue) {
117
117
  return this.#specs.reduce(callback, initialValue)
@@ -120,7 +120,7 @@ export default class TypeSpec {
120
120
  /**
121
121
  * Returns the first type specification that satisfies the provided testing function.
122
122
  *
123
- * @param {Function} callback - Function to test each spec
123
+ * @param {function(unknown): boolean} callback - Function to test each spec
124
124
  * @returns {object|undefined} The first spec that matches, or undefined
125
125
  */
126
126
  find(callback) {
@@ -131,7 +131,7 @@ export default class TypeSpec {
131
131
  * Tests whether a value matches any of the type specifications.
132
132
  * Handles array types, union types, and empty value validation.
133
133
  *
134
- * @param {*} value - The value to test against the type specifications
134
+ * @param {unknown} value - The value to test against the type specifications
135
135
  * @param {object} options - Validation options
136
136
  * @param {boolean} options.allowEmpty - Whether empty values are allowed
137
137
  * @returns {boolean} True if the value matches any type specification
@@ -191,6 +191,7 @@ export default class TypeSpec {
191
191
 
192
192
  this.#specs = parts.map(part => {
193
193
  const typeMatches = /(\w+)(\[\])?/.exec(part)
194
+
194
195
  if(!typeMatches || typeMatches.length !== 3)
195
196
  throw Sass.new(`Invalid type: ${part}`)
196
197
 
package/src/Util.js CHANGED
@@ -102,7 +102,7 @@ export default class Util {
102
102
  * Wrapper around Promise.all for consistency with other utility methods.
103
103
  *
104
104
  * @param {Promise[]} promises - Array of promises to await
105
- * @returns {Promise<any[]>} Results of all promises
105
+ * @returns {Promise<unknown[]>} Results of all promises
106
106
  */
107
107
  static async awaitAll(promises) {
108
108
  return await Promise.all(promises)
@@ -124,7 +124,7 @@ export default class Util {
124
124
  * Wrapper around Promise.race for consistency with other utility methods.
125
125
  *
126
126
  * @param {Promise[]} promises - Array of promises to race
127
- * @returns {Promise<any>} Result of the first settled promise
127
+ * @returns {Promise<unknown>} Result of the first settled promise
128
128
  */
129
129
  static async race(promises) {
130
130
  return await Promise.race(promises)