@gesslar/sassy 0.19.0 → 0.20.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 CHANGED
@@ -107,7 +107,7 @@ npx @gesslar/sassy lint my-theme.yaml
107
107
 
108
108
  ### Debugging Your Themes
109
109
 
110
- **See what a color variable resolves to:**
110
+ **See what a colour variable resolves to:**
111
111
 
112
112
  ```bash
113
113
  npx @gesslar/sassy resolve --color editor.background my-theme.yaml
@@ -119,7 +119,7 @@ npx @gesslar/sassy resolve --color editor.background my-theme.yaml
119
119
  npx @gesslar/sassy resolve --tokenColor keyword.control my-theme.yaml
120
120
  ```
121
121
 
122
- **Debug semantic token colors:**
122
+ **Debug semantic token colours:**
123
123
 
124
124
  ```bash
125
125
  npx @gesslar/sassy resolve --semanticTokenColor variable.readonly my-theme.yaml
@@ -147,7 +147,7 @@ npx @gesslar/sassy lint my-theme.yaml
147
147
  ```
148
148
 
149
149
  The lint command performs comprehensive validation of your theme files to catch
150
- common issues that could cause unexpected behavior or poor maintainability.
150
+ common issues that could cause unexpected behaviour or poor maintainability.
151
151
 
152
152
  ### Lint Command Checks
153
153
 
@@ -423,13 +423,12 @@ vars:
423
423
  config:
424
424
  name: "My Theme"
425
425
  type: dark
426
- imports:
427
- vars:
428
- colors: "./colours.yaml"
426
+ import:
427
+ - "./colours.yaml"
429
428
 
430
429
  vars:
431
430
  # Use imported colours
432
- accent: $(colors.palette.primary)
431
+ accent: $(palette.primary)
433
432
 
434
433
  # Build your design system
435
434
  std:
@@ -451,49 +450,48 @@ Sassy supports importing different types of theme components:
451
450
 
452
451
  ```yaml
453
452
  config:
454
- imports:
455
- # Import variables (merged into your vars section)
456
- vars:
457
- colors: "./shared/colours.yaml"
458
- # Can import multiple files
459
- typography: ["./shared/fonts.yaml", "./shared/sizes.yaml"]
460
-
461
- # Import global configuration
462
- global:
463
- base: "./shared/base-config.yaml"
464
-
465
- # Import VS Code colour definitions
466
- colors:
467
- ui: "./shared/ui-colours.yaml"
468
-
469
- # Import syntax highlighting rules
470
- tokenColors:
471
- syntax: "./shared/syntax.yaml"
472
-
473
- # Import semantic token colours
474
- semanticTokenColors:
475
- semantic: "./shared/semantic.yaml"
453
+ import:
454
+ - "./shared/colours.yaml" # Variables and base config
455
+ - "./shared/ui-colours.yaml" # VS Code color definitions
456
+ - "./shared/syntax.yaml" # Syntax highlighting rules
457
+ - "./shared/semantic.yaml" # Semantic token colours
476
458
  ```
477
459
 
478
- **Import Format Options:**
460
+ **Import Format:**
479
461
 
480
- - **Single file:** `"./path/to/file.yaml"`
481
- - **Multiple files:** `["./file1.yaml", "./file2.yaml"]`
462
+ Imports are a simple array of file paths. Each file gets merged into your theme:
463
+
464
+ - **Files:** `["./file1.yaml", "./file2.yaml", "./file3.yaml"]`
482
465
  - **File types:** Both `.yaml` and `.json5` are supported
483
466
 
484
467
  **Merge Order:**
485
468
 
486
- The merge happens in a precise order with each level overriding the previous:
469
+ The merge behaviour depends on the type of theme content:
470
+
471
+ **Objects (composable):** `colors`, `semanticTokenColors`, `vars`, `config`
472
+
473
+ 1. Imported files (merged in import order)
474
+ 2. Your theme file's own definitions (final override)
475
+
476
+ Later sources override earlier ones using deep object merging.
477
+
478
+ **Arrays (append-only):** `tokenColors`
479
+
480
+ 1. All imported `tokenColors` (in import order)
481
+ 2. Your theme file's `tokenColors` (appended last)
482
+
483
+ **Why different?** VS Code reads `tokenColors` from top to bottom and stops at the first matching rule. This means:
484
+
485
+ - **Imported rules** = specific styling (e.g., "make function names blue")
486
+ - **Your main file rules** = fallbacks (e.g., "if nothing else matched, make it white")
487
+
488
+ **Examples:**
489
+
490
+ - If an import defines `keyword.control` and your main file also defines `keyword.control`, VS Code will use the imported version because it appears first in the final array.
487
491
 
488
- 1. `global` imports (merged first)
489
- 2. `colors` imports
490
- 3. `tokenColors` imports
491
- 4. `semanticTokenColors` imports
492
- 5. Your theme file's own definitions (final override)
492
+ - If your import has a broad rule like `storage` and your main file has a specific rule like `storage.type`, the broad `storage` rule will match first and your specific `storage.type` rule will never be used.
493
493
 
494
- Within each section, if you import multiple files, they merge in array order.
495
- This layered approach gives you fine-grained control over which definitions
496
- take precedence.
494
+ > **Tip:** If you're unsure about rule precedence or conflicts, run `npx @gesslar/sassy lint your-theme.yaml` to see exactly what's happening with your `tokenColors`.
497
495
 
498
496
  ### Watch Mode for Development
499
497
 
@@ -574,7 +572,7 @@ different approaches and techniques.
574
572
  npx @gesslar/sassy build --nerd my-theme.yaml
575
573
 
576
574
  # Check what a specific variable resolves to
577
- npx @gesslar/sassy resolve --token problematic.variable my-theme.yaml
575
+ npx @gesslar/sassy resolve --color problematic.variable my-theme.yaml
578
576
  ```
579
577
 
580
578
  **Variables not resolving:**
@@ -585,9 +583,10 @@ npx @gesslar/sassy resolve --token problematic.variable my-theme.yaml
585
583
 
586
584
  **Watch mode not updating:**
587
585
 
588
- - Check that files aren't being saved outside the watched directory
589
- - Try restarting watch mode
590
- - Verify file permissions
586
+ - Ensure you're editing the original `.yaml` file (not the compiled `.color-theme.json`)
587
+ - Check that imported files are in the same directory tree as your main theme
588
+ - Try restarting watch mode if it seems stuck
589
+ - Verify file permissions allow reading your theme files
591
590
 
592
591
  ## Getting Help
593
592
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gesslar/sassy",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "displayName": "Sassy",
5
5
  "description": "Make gorgeous themes that speak as boldly as you do.",
6
6
  "publisher": "gesslar",
package/src/Compiler.js CHANGED
@@ -58,16 +58,25 @@ export default class Compiler {
58
58
 
59
59
  theme.dependencies = importedFiles
60
60
 
61
+ // Handle tokenColors separately - imports first, then main source
62
+ // (append-only)
63
+ const mergedTokenColors = [
64
+ ...(imported.tokenColors ?? []),
65
+ ...(sourceTheme?.tokenColors ?? [])
66
+ ]
67
+
61
68
  const merged = Data.mergeObject({},
62
69
  imported,
63
70
  {
64
71
  vars: sourceVars ?? {},
65
72
  colors: sourceTheme?.colors ?? {},
66
- tokenColors: sourceTheme?.tokenColors ?? [],
67
73
  semanticTokenColors: sourceTheme?.semanticTokenColors ?? {},
68
74
  }
69
75
  )
70
76
 
77
+ // Add tokenColors after merging to avoid mergeObject processing
78
+ merged.tokenColors = mergedTokenColors
79
+
71
80
  // Shred them up! Kinda. And evaluate the variables in place
72
81
  const vars = this.#decomposeObject(merged.vars)
73
82
  evaluate(vars)
@@ -173,7 +182,7 @@ export default class Compiler {
173
182
 
174
183
  imported.vars = Data.mergeObject(imported.vars, vars)
175
184
  imported.colors = Data.mergeObject(imported.colors, colors)
176
- imported.tokenColors = Data.mergeArray(imported.tokenColors, tokenColors)
185
+ imported.tokenColors = [...imported.tokenColors, ...tokenColors]
177
186
  })
178
187
 
179
188
  return {imported,importedFiles}
package/src/Data.js CHANGED
@@ -520,26 +520,4 @@ export default class Data {
520
520
  return arr.filter((_, index) => results[index])
521
521
  }
522
522
 
523
- /**
524
- * Shallowly merges multiple arrays, deduplicating while preserving order.
525
- *
526
- * @param {...any[]} sources - Arrays to merge
527
- * @returns {Array} A new merged array
528
- * @throws {Error} If the sources are not all arrays
529
- */
530
- static mergeArray(...sources) {
531
- if(sources.some(source => !Array.isArray(source)))
532
- throw Sass.new("All sources to mergeArray must be arrays.")
533
-
534
- return sources.reduce((acc, curr) => {
535
- const accSet = new Set(acc)
536
-
537
- curr.forEach(value => {
538
- accSet.has(value) && accSet.delete(value)
539
- accSet.add(value)
540
- })
541
-
542
- return Array.from(accSet)
543
- }, [])
544
- }
545
523
  }
@@ -20,6 +20,8 @@ import c from "@gesslar/colours"
20
20
 
21
21
  import Command from "./Command.js"
22
22
  import Evaluator from "./Evaluator.js"
23
+ import File from "./File.js"
24
+ import FileObject from "./FileObject.js"
23
25
  import Term from "./Term.js"
24
26
  import Theme from "./Theme.js"
25
27
  import ThemePool from "./ThemePool.js"
@@ -190,7 +192,7 @@ export default class LintCommand extends Command {
190
192
 
191
193
  switch(issue.type) {
192
194
  case "duplicate-scope": {
193
- const rules = issue.occurrences.map(occ => `'${occ.name}'`).join(", ")
195
+ const rules = issue.occurrences.map(occ => `{loc}'${occ.name}{/}'`).join(", ")
194
196
  Term.info(c`${indicator} Scope '{context}${issue.scope}{/}' is duplicated in ${rules}`)
195
197
  break
196
198
  }
@@ -201,15 +203,15 @@ export default class LintCommand extends Command {
201
203
  }
202
204
 
203
205
  case "unused-variable": {
204
- Term.info(c`${indicator} Variable '{context}${issue.variable}{/}' is defined but never used`)
206
+ Term.info(c`${indicator} Variable '{context}${issue.variable}{/}' is defined in '{loc}${issue.occurence}{/}', but is never used`)
205
207
  break
206
208
  }
207
209
 
208
210
  case "precedence-issue": {
209
211
  if(issue.broadIndex === issue.specificIndex) {
210
- Term.info(c`${indicator} Scope '{context}${issue.broadScope}{/}' makes more specific '{context}${issue.specificScope}' redundant in '${issue.broadRule}{/}'`)
212
+ Term.info(c`${indicator} Scope '{context}${issue.broadScope}{/}' makes more specific '{context}${issue.specificScope}{/}' redundant in '{loc}${issue.broadRule}{/}'`)
211
213
  } else {
212
- Term.info(c`${indicator} Scope '{context}${issue.broadScope}{/}' in '${issue.broadRule}' masks more specific '{context}${issue.specificScope}{/}' in '${issue.specificRule}'`)
214
+ Term.info(c`${indicator} Scope '{context}${issue.broadScope}{/}' in '{loc}${issue.broadRule}{/}' masks more specific '{context}${issue.specificScope}{/}' in '{loc}${issue.specificRule}{/}'`)
213
215
  }
214
216
 
215
217
  break
@@ -314,8 +316,11 @@ export default class LintCommand extends Command {
314
316
  return issues
315
317
 
316
318
  // Get variables defined in the vars section only
317
- const definedVars = new Set()
318
- this.collectVarsDefinitions(theme.source.vars, definedVars)
319
+ const definedVars = new Map()
320
+ const {cwd} = this
321
+ const mainFile = new FileObject(theme.sourceFile.path)
322
+ const relativeMainPath = File.relativeOrAbsolutePath(cwd, mainFile)
323
+ this.collectVarsDefinitions(theme.source.vars, definedVars, "", relativeMainPath)
319
324
 
320
325
  // Also check dependencies for vars definitions
321
326
  if(theme.dependencies) {
@@ -323,7 +328,9 @@ export default class LintCommand extends Command {
323
328
  try {
324
329
  const depData = theme.cache?.loadCachedDataSync?.(dependency)
325
330
  if(depData?.vars) {
326
- this.collectVarsDefinitions(depData.vars, definedVars)
331
+ const depFile = new FileObject(dependency.path)
332
+ const relativeDependencyPath = File.relativeOrAbsolutePath(cwd, depFile)
333
+ this.collectVarsDefinitions(depData.vars, definedVars, "", relativeDependencyPath)
327
334
  }
328
335
  } catch {
329
336
  // Ignore cache errors
@@ -368,12 +375,13 @@ export default class LintCommand extends Command {
368
375
  }
369
376
 
370
377
  // Find vars-defined variables that are never used in content sections
371
- for(const varName of definedVars) {
378
+ for(const [varName, filename] of definedVars) {
372
379
  if(!usedVars.has(varName)) {
373
380
  issues.push({
374
381
  type: "unused-variable",
375
382
  severity: "low",
376
- variable: `$${varName}`
383
+ variable: `$${varName}`,
384
+ occurence: filename,
377
385
  })
378
386
  }
379
387
  }
@@ -383,23 +391,24 @@ export default class LintCommand extends Command {
383
391
 
384
392
  /**
385
393
  * Recursively collects variable names defined in the vars section.
386
- * Adds found variable names to the definedVars set.
394
+ * Adds found variable names to the definedVars map.
387
395
  *
388
396
  * @param {any} vars - The vars data structure to search
389
- * @param {Set} definedVars - Set to add found variable names to
397
+ * @param {Map} definedVars - Map to add found variable names and filenames to
390
398
  * @param {string} prefix - Current prefix for nested vars
399
+ * @param {string} filename - The filename where this variable is defined
391
400
  */
392
- collectVarsDefinitions(vars, definedVars, prefix = "") {
401
+ collectVarsDefinitions(vars, definedVars, prefix = "", filename = "") {
393
402
  if(!vars || typeof vars !== "object")
394
403
  return
395
404
 
396
405
  for(const [key, value] of Object.entries(vars)) {
397
406
  const varName = prefix ? `${prefix}.${key}` : key
398
- definedVars.add(varName)
407
+ definedVars.set(varName, filename)
399
408
 
400
409
  // If the value is an object, recurse for nested definitions
401
410
  if(value && typeof value === "object" && !Array.isArray(value)) {
402
- this.collectVarsDefinitions(value, definedVars, varName)
411
+ this.collectVarsDefinitions(value, definedVars, varName, filename)
403
412
  }
404
413
  }
405
414
  }
package/src/cli.js CHANGED
@@ -80,6 +80,7 @@ void (async function main() {
80
80
  c.alias.set("muted-bracket", "{F244}")
81
81
  // Lint command
82
82
  c.alias.set("context", "{F159}")
83
+ c.alias.set("loc", "{F148}")
83
84
  // Resolve command
84
85
  c.alias.set("head", "{F220}")
85
86
  c.alias.set("leaf", "{F151}")