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