@gesslar/sassy 0.19.0 → 0.20.2
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 +44 -45
- package/package.json +1 -1
- package/src/Compiler.js +11 -2
- package/src/Data.js +0 -22
- package/src/LintCommand.js +23 -14
- package/src/Session.js +11 -6
- package/src/cli.js +1 -0
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
|
|
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
|
|
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
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
colors: "./colours.yaml"
|
|
426
|
+
import:
|
|
427
|
+
- "./colours.yaml"
|
|
429
428
|
|
|
430
429
|
vars:
|
|
431
430
|
# Use imported colours
|
|
432
|
-
accent: $(
|
|
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
|
-
|
|
455
|
-
#
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
|
460
|
+
**Import Format:**
|
|
479
461
|
|
|
480
|
-
|
|
481
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 --
|
|
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
|
-
-
|
|
589
|
-
-
|
|
590
|
-
-
|
|
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
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 =
|
|
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
|
}
|
package/src/LintCommand.js
CHANGED
|
@@ -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
|
|
318
|
-
this
|
|
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
|
-
|
|
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
|
|
394
|
+
* Adds found variable names to the definedVars map.
|
|
387
395
|
*
|
|
388
396
|
* @param {any} vars - The vars data structure to search
|
|
389
|
-
* @param {
|
|
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.
|
|
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/Session.js
CHANGED
|
@@ -39,6 +39,15 @@ export default class Session {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
async run() {
|
|
42
|
+
|
|
43
|
+
this.#building = true
|
|
44
|
+
await this.#command.asyncEmit("building")
|
|
45
|
+
this.#command.asyncEmit("recordBuildStart", this.#theme)
|
|
46
|
+
await this.#buildPipeline()
|
|
47
|
+
|
|
48
|
+
// This must come after, or you will fuck up the watching!
|
|
49
|
+
// Themes won't have their dependencies yet unless you build them
|
|
50
|
+
// at least once. - Samwise Gamgee
|
|
42
51
|
if(this.#options.watch) {
|
|
43
52
|
this.#command.emitter.on("closeSession", async() =>
|
|
44
53
|
await this.#handleCloseSession())
|
|
@@ -54,11 +63,6 @@ export default class Session {
|
|
|
54
63
|
|
|
55
64
|
await this.#resetWatcher()
|
|
56
65
|
}
|
|
57
|
-
|
|
58
|
-
this.#building = true
|
|
59
|
-
await this.#command.asyncEmit("building")
|
|
60
|
-
this.#command.asyncEmit("recordBuildStart", this.#theme)
|
|
61
|
-
await this.#buildPipeline()
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
/**
|
|
@@ -206,9 +210,10 @@ export default class Session {
|
|
|
206
210
|
* Handles a file change event and triggers a rebuild for the theme.
|
|
207
211
|
*
|
|
208
212
|
* @param {string} changed - Path to the changed file
|
|
213
|
+
* @param {object} _stats - OS-level file stat information
|
|
209
214
|
* @returns {Promise<void>}
|
|
210
215
|
*/
|
|
211
|
-
async #handleFileChange(changed) {
|
|
216
|
+
async #handleFileChange(changed, _stats) {
|
|
212
217
|
try {
|
|
213
218
|
if(this.#building)
|
|
214
219
|
return
|
package/src/cli.js
CHANGED