@gesslar/sassy 0.23.0 → 1.1.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 +4 -13
- package/package.json +8 -6
- package/src/BuildCommand.js +40 -19
- package/src/Colour.js +6 -1
- package/src/Command.js +13 -7
- package/src/Compiler.js +19 -10
- package/src/LintCommand.js +4 -0
- package/src/ResolveCommand.js +2 -2
- package/src/Session.js +69 -43
- package/src/Theme.js +26 -9
- package/src/cli.js +12 -32
package/README.md
CHANGED
|
@@ -98,7 +98,7 @@ npx @gesslar/sassy lint my-theme.yaml
|
|
|
98
98
|
### Build Command Options
|
|
99
99
|
|
|
100
100
|
| Option | Description |
|
|
101
|
-
|
|
101
|
+
| -------- | ------------- |
|
|
102
102
|
| `-w, --watch` | Watch files and rebuild on changes |
|
|
103
103
|
| `-o, --output-dir <dir>` | Specify output directory |
|
|
104
104
|
| `-n, --dry-run` | Print JSON to stdout instead of writing files |
|
|
@@ -132,7 +132,7 @@ output.
|
|
|
132
132
|
### Resolve Command Options
|
|
133
133
|
|
|
134
134
|
| Option | Description |
|
|
135
|
-
|
|
135
|
+
| -------- | ------------- |
|
|
136
136
|
| `-c, --color <key>` | Resolve a specific color property to its final value |
|
|
137
137
|
| `-t, --tokenColor <scope>` | Resolve tokenColors for a specific scope |
|
|
138
138
|
| `-s, --semanticTokenColor <token>` | Resolve semantic token colors for a specific token type |
|
|
@@ -222,7 +222,7 @@ to least specific.
|
|
|
222
222
|
### Lint Command Options
|
|
223
223
|
|
|
224
224
|
| Option | Description |
|
|
225
|
-
|
|
225
|
+
| -------- | ------------- |
|
|
226
226
|
| `--nerd` | Show detailed error traces if linting fails |
|
|
227
227
|
|
|
228
228
|
## Basic Theme Structure
|
|
@@ -297,33 +297,24 @@ functions in the [Culori documentation](https://culorijs.org/).
|
|
|
297
297
|
Make colours that work together:
|
|
298
298
|
|
|
299
299
|
| Function | Example | Result |
|
|
300
|
-
|
|
300
|
+
| ---------- | --------- | -------- |
|
|
301
301
|
| `lighten(colour, %=0-100)` | `lighten($(bg), 25)` | 25% lighter background |
|
|
302
302
|
| `darken(colour, %=0-100)` | `darken($(accent), 30)` | 30% darker accent |
|
|
303
|
-
| || |
|
|
304
303
|
| `alpha(colour, alpha=0-1)` | `alpha($(brand), 0.5)` | Set exact transparency |
|
|
305
304
|
| `fade(colour, alpha=0-1)` | `fade($(accent), 0.5)` | Reduce opacity by 50% |
|
|
306
305
|
| `solidify(colour, alpha=0-1)` | `solidify($(bg.accent), 0.3)` | Increase opacity by 30% |
|
|
307
|
-
| || |
|
|
308
306
|
| `mix(colour1, colour2, %=0-100)` | `mix($(fg), $(accent), 20)` | Blend 20% accent |
|
|
309
307
|
| `mix(colour1, colour2)` | `mix($(fg), $(accent))` | Blend 50% accent |
|
|
310
|
-
| || |
|
|
311
308
|
| `invert(colour)` | `invert($(fg))` | Perfect opposite |
|
|
312
|
-
| || |
|
|
313
309
|
| `hsv(h=0-255, s=0-255, v=0-255)` | `hsv(50, 200, 180)` | HSV colour (hue 50, saturation 200, value 180) |
|
|
314
310
|
| `hsva(h=0-255, s=0-255, v=0-255, a=0-1)` | `hsva(50, 200, 180, 0.5)` | HSV with 50% opacity |
|
|
315
|
-
| || |
|
|
316
311
|
| `hsl(h=0-360, s=0-100, l=0-100)` | `hsl(200, 50, 40)` | HSL colour (200° hue, 50% saturation, 40% lightness) |
|
|
317
312
|
| `hsla(h=0-360, s=0-100, l=0-100, a=0-1)` | `hsla(200, 50, 40, 0.5)` | HSL with 50% opacity |
|
|
318
|
-
| || |
|
|
319
313
|
| `rgb(r=0-255, g=0-255, b=0-255)` | `rgb(139, 152, 255)` | RGB colour (139 red, 152 green, 255 blue) |
|
|
320
314
|
| `rgba(r=0-255, g=0-255, b=0-255, a=0-1)` | `rgba(139, 152, 255, 0.5)` | RGB with 50% opacity |
|
|
321
|
-
| || |
|
|
322
315
|
| `oklch(l=0-1, c=0-100, h=0-360)` | `oklch(0.7, 25, 180)` | OKLCH colour (70% lightness, 25 chroma, 180° hue) |
|
|
323
316
|
| `oklcha(l=0-1, c=0-100, h=0-360, a=0-1)` | `oklcha(0.5, 30, 45, 0.8)` | OKLCH with 80% opacity |
|
|
324
|
-
| || |
|
|
325
317
|
| `css(name)` | `css(tomato)` | CSS named colour (tomato, skyblue, etc.) |
|
|
326
|
-
| || |
|
|
327
318
|
|
|
328
319
|
> **Note:** In all of these functions, `colour` can be a raw hex (`#ff66cc`),
|
|
329
320
|
a variable (`$(accent)`), a CSS named colour (`css(tomato)`), or another colour
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"name": "gesslar",
|
|
6
6
|
"url": "https://gesslar.dev"
|
|
7
7
|
},
|
|
8
|
-
"version": "
|
|
8
|
+
"version": "1.1.0",
|
|
9
9
|
"license": "Unlicense",
|
|
10
10
|
"homepage": "https://github.com/gesslar/sassy#readme",
|
|
11
11
|
"repository": {
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"node": ">=22"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@gesslar/colours": "^0.
|
|
47
|
-
"@gesslar/toolkit": "^3.
|
|
46
|
+
"@gesslar/colours": "^0.8.0",
|
|
47
|
+
"@gesslar/toolkit": "^3.29.0",
|
|
48
48
|
"chokidar": "^5.0.0",
|
|
49
49
|
"color-support": "^1.1.3",
|
|
50
50
|
"commander": "^14.0.2",
|
|
@@ -54,19 +54,21 @@
|
|
|
54
54
|
"yaml": "^2.8.2"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
|
-
"@gesslar/uglier": "^
|
|
57
|
+
"@gesslar/uglier": "^1.2.0",
|
|
58
58
|
"eslint": "^9.39.2",
|
|
59
59
|
"typescript": "^5.9.3"
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
62
|
"clean": "rm -rfv ./dist",
|
|
63
63
|
"build": "mkdir -pv ./dist && pnpm pack --pack-destination ./dist/",
|
|
64
|
+
"types": "node -e \"require('fs').rmSync('types',{recursive:true,force:true});\" && tsc -p tsconfig.types.json",
|
|
64
65
|
"exec": "node ./src/cli.js",
|
|
65
66
|
"lint": "eslint src/",
|
|
67
|
+
"test": "node --test tests/**/*.test.js",
|
|
68
|
+
"test:coverage": "node --experimental-test-coverage --test tests/**/*.test.js",
|
|
66
69
|
"submit": "pnpm publish --access public --//registry.npmjs.org/:_authToken=\"${NPM_ACCESS_TOKEN}\"",
|
|
67
70
|
"examples": "node ./examples/validator.js",
|
|
68
|
-
"
|
|
69
|
-
"update": "pnpm up --latest --recursive",
|
|
71
|
+
"update": "pnpm self-update && pnpx npm-check-updates -u && pnpm install",
|
|
70
72
|
"pr": "gt submit -p --ai",
|
|
71
73
|
"patch": "pnpm version patch",
|
|
72
74
|
"minor": "pnpm version minor",
|
package/src/BuildCommand.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {EventEmitter} from "node:events"
|
|
2
2
|
import process from "node:process"
|
|
3
3
|
|
|
4
|
-
import {Sass, Term, Util} from "@gesslar/toolkit"
|
|
4
|
+
import {Promised, Sass, Term, Util} from "@gesslar/toolkit"
|
|
5
5
|
import Command from "./Command.js"
|
|
6
6
|
import Session from "./Session.js"
|
|
7
7
|
import Theme from "./Theme.js"
|
|
@@ -36,6 +36,18 @@ export default class BuildCommand extends Command {
|
|
|
36
36
|
})
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Emits an event asynchronously using the internal emitter.
|
|
41
|
+
* This method wraps Util.asyncEmit for convenience.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} event - The event name to emit
|
|
44
|
+
* @param {...any} args - Arguments to pass to the event handlers
|
|
45
|
+
* @returns {Promise<void>} Resolves when all event handlers have completed
|
|
46
|
+
*/
|
|
47
|
+
async asyncEmit(event, ...args) {
|
|
48
|
+
return await Util.asyncEmit(this.emitter, event, ...args)
|
|
49
|
+
}
|
|
50
|
+
|
|
39
51
|
/**
|
|
40
52
|
* Executes the build command for the provided theme files.
|
|
41
53
|
* Processes each file in parallel, optionally watching for changes.
|
|
@@ -43,13 +55,21 @@ export default class BuildCommand extends Command {
|
|
|
43
55
|
* @param {string[]} fileNames - Array of theme file paths to process
|
|
44
56
|
* @param {object} options - Build options
|
|
45
57
|
* @param {boolean} [options.watch] - Enable watch mode for file changes
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
58
|
+
* @param {string} [options.outputDir] - Custom output directory path
|
|
59
|
+
* @param {boolean} [options.dryRun] - Print JSON to stdout without writing files
|
|
60
|
+
* @param {boolean} [options.silent] - Silent mode, only show errors or dry-run output
|
|
49
61
|
* @returns {Promise<void>} Resolves when all files are processed
|
|
50
62
|
* @throws {Error} When theme compilation fails
|
|
51
63
|
*/
|
|
52
64
|
async execute(fileNames, options) {
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @typedef {object} BuildCommandOptions
|
|
68
|
+
* @property {boolean} [watch] Enable watch mode for file changes
|
|
69
|
+
* @property {string} [outputDir] Custom output directory path
|
|
70
|
+
* @property {boolean} [dryRun] Print JSON to stdout without writing files
|
|
71
|
+
* @property {boolean} [silent] Silent mode, only show errors or dry-run output
|
|
72
|
+
*/
|
|
53
73
|
const cwd = this.getCwd()
|
|
54
74
|
|
|
55
75
|
if(options.watch) {
|
|
@@ -64,7 +84,7 @@ export default class BuildCommand extends Command {
|
|
|
64
84
|
this.emitter.on("printPrompt", () => this.#printPrompt())
|
|
65
85
|
}
|
|
66
86
|
|
|
67
|
-
const sessionResults = await
|
|
87
|
+
const sessionResults = await Promised.settle(
|
|
68
88
|
fileNames.map(async fileName => {
|
|
69
89
|
const fileObject = await this.resolveThemeFileName(fileName, cwd)
|
|
70
90
|
const theme = new Theme(fileObject, cwd, options)
|
|
@@ -74,24 +94,25 @@ export default class BuildCommand extends Command {
|
|
|
74
94
|
})
|
|
75
95
|
)
|
|
76
96
|
|
|
77
|
-
if(
|
|
78
|
-
|
|
97
|
+
if(Promised.hasRejected(sessionResults))
|
|
98
|
+
Promised.throw("Creating sessions.", sessionResults)
|
|
79
99
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
100
|
+
// if(sessionResults.some(theme => theme.status === "rejected")) {
|
|
101
|
+
// const rejected = sessionResults.filter(result => result.status === "rejected")
|
|
83
102
|
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
103
|
+
// rejected.forEach(item => {
|
|
104
|
+
// const sassError = Sass.new("Creating session for theme file.", item.reason)
|
|
105
|
+
// sassError.report(options.nerd)
|
|
106
|
+
// })
|
|
107
|
+
// process.exit(1)
|
|
108
|
+
// }
|
|
88
109
|
|
|
89
|
-
|
|
90
|
-
|
|
110
|
+
const sessions = Promised.values(sessionResults)
|
|
111
|
+
const firstRun = await Promised.settle(sessions.map(
|
|
112
|
+
async session => await session.run()))
|
|
91
113
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
114
|
+
if(Promised.hasRejected(firstRun))
|
|
115
|
+
Promised.throw("Executing sessions.", firstRun)
|
|
95
116
|
}
|
|
96
117
|
|
|
97
118
|
/**
|
package/src/Colour.js
CHANGED
|
@@ -13,7 +13,12 @@ import {
|
|
|
13
13
|
parse
|
|
14
14
|
} from "culori"
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import {Sass, Util} from "@gesslar/toolkit"
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @import {ThemeToken} from "./ThemeToken.js"
|
|
20
|
+
*/
|
|
21
|
+
|
|
17
22
|
// Cache for parsed colours to improve performance
|
|
18
23
|
const _colourCache = new Map()
|
|
19
24
|
|
package/src/Command.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {FileSystem, Sass} from "@gesslar/toolkit"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @import {DirectoryObject} from "@gesslar/toolkit"
|
|
5
|
+
* @import {FileObject} from "@gesslar/toolkit"
|
|
6
|
+
* @import {Cache} from "@gesslar/toolkit"
|
|
7
|
+
*/
|
|
2
8
|
|
|
3
9
|
/**
|
|
4
10
|
* Base class for command-line interface commands.
|
|
@@ -17,11 +23,10 @@ export default class Command {
|
|
|
17
23
|
/**
|
|
18
24
|
* Creates a new Command instance.
|
|
19
25
|
*
|
|
20
|
-
* @param {object} config - Configuration object
|
|
21
26
|
* @param {DirectoryObject} config.cwd - Current working directory object
|
|
22
27
|
* @param {object} config.packageJson - Package.json data
|
|
23
28
|
*/
|
|
24
|
-
constructor({cwd,packageJson}) {
|
|
29
|
+
constructor({cwd, packageJson}) {
|
|
25
30
|
this.#cwd = cwd
|
|
26
31
|
this.#packageJson = packageJson
|
|
27
32
|
}
|
|
@@ -221,17 +226,18 @@ export default class Command {
|
|
|
221
226
|
* Resolves a theme file name to a FileObject and validates its existence.
|
|
222
227
|
*
|
|
223
228
|
* @param {string} fileName - The theme file name or path
|
|
224
|
-
* @param {
|
|
229
|
+
* @param {DirectoryObject} cwd - The current working directory object
|
|
225
230
|
* @returns {Promise<FileObject>} The resolved and validated FileObject
|
|
226
231
|
* @throws {Sass} If the file does not exist
|
|
227
232
|
*/
|
|
228
233
|
async resolveThemeFileName(fileName, cwd) {
|
|
229
|
-
|
|
234
|
+
fileName = FileSystem.relativeOrAbsolutePath(cwd.path, fileName)
|
|
235
|
+
|
|
236
|
+
const fileObject = cwd.getFile(fileName)
|
|
230
237
|
|
|
231
238
|
if(!await fileObject.exists)
|
|
232
|
-
throw Sass.new(`No such file 🤷: ${fileObject.
|
|
239
|
+
throw Sass.new(`No such file 🤷: ${fileObject.relativeTo(cwd)}`)
|
|
233
240
|
|
|
234
241
|
return fileObject
|
|
235
242
|
}
|
|
236
|
-
|
|
237
243
|
}
|
package/src/Compiler.js
CHANGED
|
@@ -11,9 +11,14 @@
|
|
|
11
11
|
* Supports extension points for custom phases and output formats.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import {Collection, Data, Sass, Term, Util} from "@gesslar/toolkit"
|
|
15
|
+
|
|
15
16
|
import Evaluator from "./Evaluator.js"
|
|
16
17
|
|
|
18
|
+
/**
|
|
19
|
+
* @import {Theme} from "./Theme.js"
|
|
20
|
+
*/
|
|
21
|
+
|
|
17
22
|
/**
|
|
18
23
|
* Main compiler class for processing theme source files.
|
|
19
24
|
* Handles the complete compilation pipeline from source to VS Code theme output.
|
|
@@ -39,6 +44,7 @@ export default class Compiler {
|
|
|
39
44
|
const config = this.#decomposeObject(sourceConfig)
|
|
40
45
|
|
|
41
46
|
evaluate(config)
|
|
47
|
+
|
|
42
48
|
const recompConfig = this.#composeObject(config)
|
|
43
49
|
|
|
44
50
|
const header = {
|
|
@@ -49,12 +55,11 @@ export default class Compiler {
|
|
|
49
55
|
|
|
50
56
|
// Let's get all of the imports!
|
|
51
57
|
const imports = recompConfig.import ?? []
|
|
52
|
-
const {imported,importByFile} =
|
|
53
|
-
await this.#import(imports, theme)
|
|
58
|
+
const {imported,importByFile} = await this.#import(imports, theme)
|
|
54
59
|
|
|
55
|
-
importByFile.forEach(
|
|
56
|
-
theme.addDependency(file,themeData)
|
|
57
|
-
|
|
60
|
+
importByFile.forEach(
|
|
61
|
+
(themeData, file) => theme.addDependency(file,themeData)
|
|
62
|
+
)
|
|
58
63
|
|
|
59
64
|
// Handle tokenColors separately - imports first, then main source
|
|
60
65
|
// (append-only)
|
|
@@ -148,16 +153,19 @@ export default class Compiler {
|
|
|
148
153
|
? [imports]
|
|
149
154
|
: imports
|
|
150
155
|
|
|
151
|
-
if(!
|
|
152
|
-
throw new
|
|
156
|
+
if(!Collection.isArrayUniform(imports, "string"))
|
|
157
|
+
throw Sass.new(
|
|
153
158
|
`All import entries must be strings. Got ${JSON.stringify(imports)}`
|
|
154
159
|
)
|
|
155
160
|
|
|
156
161
|
const loaded = new Map()
|
|
157
162
|
|
|
163
|
+
const themeSource = theme.getSourceFile()
|
|
164
|
+
const themeDirectory = themeSource.parent
|
|
165
|
+
|
|
158
166
|
for(const importing of imports) {
|
|
159
167
|
try {
|
|
160
|
-
const file =
|
|
168
|
+
const file = themeDirectory.getFile(importing)
|
|
161
169
|
|
|
162
170
|
// Get the cached version or a new version. Who knows? I don't know.
|
|
163
171
|
const {result, cost} = await Util.time(async() => {
|
|
@@ -165,10 +173,11 @@ export default class Compiler {
|
|
|
165
173
|
})
|
|
166
174
|
|
|
167
175
|
if(theme.getOptions().nerd) {
|
|
176
|
+
const cwd = theme.getCwd()
|
|
168
177
|
Term.status([
|
|
169
178
|
["muted", Util.rightAlignText(`${cost.toLocaleString()}ms`, 10), ["[","]"]],
|
|
170
179
|
"",
|
|
171
|
-
["muted", `${
|
|
180
|
+
["muted", `${file.relativeTo(cwd)}`],
|
|
172
181
|
["muted", `${theme.getName()}`,["(",")"]],
|
|
173
182
|
], theme.getOptions())
|
|
174
183
|
}
|
package/src/LintCommand.js
CHANGED
|
@@ -23,6 +23,10 @@ import Evaluator from "./Evaluator.js"
|
|
|
23
23
|
import Theme from "./Theme.js"
|
|
24
24
|
import {Term} from "@gesslar/toolkit"
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* @import {ThemePool} from "./ThemePool.js"
|
|
28
|
+
*/
|
|
29
|
+
|
|
26
30
|
// oops, need to have @gesslar/colours support this, too!
|
|
27
31
|
// ansiColors.enabled = colorSupport.hasBasic
|
|
28
32
|
|
package/src/ResolveCommand.js
CHANGED
|
@@ -2,7 +2,7 @@ import c from "@gesslar/colours"
|
|
|
2
2
|
// import colorSupport from "color-support"
|
|
3
3
|
|
|
4
4
|
import Command from "./Command.js"
|
|
5
|
-
import {Sass, Term, Util
|
|
5
|
+
import {Collection, Sass, Term, Util} from "@gesslar/toolkit"
|
|
6
6
|
import Colour from "./Colour.js"
|
|
7
7
|
import Evaluator from "./Evaluator.js"
|
|
8
8
|
import Theme from "./Theme.js"
|
|
@@ -41,7 +41,7 @@ export default class ResolveCommand extends Command {
|
|
|
41
41
|
async execute(inputArg, options={}) {
|
|
42
42
|
const cliOptionNames = this.getCliOptionNames()
|
|
43
43
|
const intersection =
|
|
44
|
-
|
|
44
|
+
Collection.intersection(cliOptionNames, Object.keys(options))
|
|
45
45
|
|
|
46
46
|
if(intersection.length > 1)
|
|
47
47
|
throw Sass.new(
|
package/src/Session.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import chokidar from "chokidar"
|
|
2
|
+
import path from "node:path"
|
|
2
3
|
|
|
3
|
-
import {
|
|
4
|
+
import {Promised, Sass, Term, Util} from "@gesslar/toolkit"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @import {Command} from "./Command.js"
|
|
8
|
+
* @import {Theme} from "./Theme.js"
|
|
9
|
+
*/
|
|
4
10
|
|
|
5
11
|
/**
|
|
6
12
|
* @typedef {object} SessionOptions
|
|
@@ -19,6 +25,11 @@ import {Sass, FS, Term, Util} from "@gesslar/toolkit"
|
|
|
19
25
|
* @property {string} [error] - Error message when success is false
|
|
20
26
|
*/
|
|
21
27
|
|
|
28
|
+
/**
|
|
29
|
+
* @import {Theme} from "./Theme.js"
|
|
30
|
+
* @import {Command} from "./Command.js"
|
|
31
|
+
*/
|
|
32
|
+
|
|
22
33
|
export default class Session {
|
|
23
34
|
/**
|
|
24
35
|
* The theme instance managed by this session.
|
|
@@ -170,8 +181,9 @@ export default class Session {
|
|
|
170
181
|
async run() {
|
|
171
182
|
|
|
172
183
|
this.#building = true
|
|
184
|
+
|
|
173
185
|
await this.#command.asyncEmit("building")
|
|
174
|
-
this.#command.asyncEmit("recordBuildStart", this.#theme)
|
|
186
|
+
await this.#command.asyncEmit("recordBuildStart", this.#theme)
|
|
175
187
|
await this.#buildPipeline()
|
|
176
188
|
|
|
177
189
|
// This must come after, or you will fuck up the watching!
|
|
@@ -217,13 +229,14 @@ export default class Session {
|
|
|
217
229
|
*/
|
|
218
230
|
|
|
219
231
|
loadCost = (await Util.time(() => this.#theme.load())).cost
|
|
220
|
-
const bytes = await
|
|
232
|
+
const bytes = await this.#theme.getSourceFile().size()
|
|
221
233
|
|
|
222
234
|
Term.status([
|
|
223
235
|
["success", Util.rightAlignText(`${loadCost.toLocaleString()}ms`, 10), ["[","]"]],
|
|
224
236
|
`${this.#theme.getName()} loaded`,
|
|
225
237
|
["info", `${bytes.toLocaleString()} bytes`, ["[","]"]]
|
|
226
238
|
], this.#options)
|
|
239
|
+
|
|
227
240
|
/**
|
|
228
241
|
* ****************************************************************
|
|
229
242
|
* Have the theme build itself.
|
|
@@ -236,33 +249,24 @@ export default class Session {
|
|
|
236
249
|
.map(d => d.getSourceFile())
|
|
237
250
|
.filter(f => f != null) // Filter out any null/undefined files
|
|
238
251
|
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
252
|
+
const cwd = this.#theme.getCwd()
|
|
253
|
+
const settled = await Promised
|
|
254
|
+
.settle(dependencyFiles.map(async fileObject => {
|
|
255
|
+
if(!fileObject)
|
|
256
|
+
throw Sass.new("Invalid dependency file object")
|
|
244
257
|
|
|
245
|
-
|
|
246
|
-
this.#command.getCwd(), fileObject
|
|
247
|
-
)
|
|
248
|
-
const fileSize = await FS.fileSize(fileObject)
|
|
249
|
-
|
|
250
|
-
return [fileName, fileSize]
|
|
258
|
+
return [fileObject.relativeTo(cwd), await fileObject.size()]
|
|
251
259
|
}))
|
|
252
260
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if(rejected.length > 0) {
|
|
256
|
-
rejected.forEach(reject => Term.error(reject.reason))
|
|
257
|
-
throw new Error("Compilation failed")
|
|
258
|
-
}
|
|
261
|
+
if(Promised.hasRejected(settled))
|
|
262
|
+
throw Sass.new("Compiling dependencies.", settled)
|
|
259
263
|
|
|
260
|
-
const dependencies =
|
|
264
|
+
const dependencies = Promised.values(settled)
|
|
261
265
|
.slice(1)
|
|
262
266
|
.map(dep => dep?.value)
|
|
263
267
|
.filter(Boolean)
|
|
264
268
|
|
|
265
|
-
const totalBytes =
|
|
269
|
+
const totalBytes = settled.reduce(
|
|
266
270
|
(acc,curr) => acc + (curr?.value[1] ?? 0), 0
|
|
267
271
|
)
|
|
268
272
|
|
|
@@ -273,7 +277,7 @@ export default class Session {
|
|
|
273
277
|
["[","]"]
|
|
274
278
|
],
|
|
275
279
|
`${this.#theme.getName()} compiled`,
|
|
276
|
-
["success", `${
|
|
280
|
+
["success", `${settled[0].value[1].toLocaleString()} bytes`, ["[","]"]],
|
|
277
281
|
["info", `${totalBytes.toLocaleString()} total bytes`, ["(",")"]],
|
|
278
282
|
], this.#options)
|
|
279
283
|
|
|
@@ -305,9 +309,7 @@ export default class Session {
|
|
|
305
309
|
file: outputFile,
|
|
306
310
|
bytes: writeBytes
|
|
307
311
|
} = writeResult.result
|
|
308
|
-
const outputFilename =
|
|
309
|
-
this.#command.getCwd(), outputFile
|
|
310
|
-
)
|
|
312
|
+
const outputFilename = outputFile.relativeTo(cwd)
|
|
311
313
|
const status = [
|
|
312
314
|
[
|
|
313
315
|
"success",
|
|
@@ -331,7 +333,7 @@ export default class Session {
|
|
|
331
333
|
Term.status(status, this.#options)
|
|
332
334
|
|
|
333
335
|
// Track successful build
|
|
334
|
-
this.#command.asyncEmit("recordBuildSucceed", this.#theme)
|
|
336
|
+
await this.#command.asyncEmit("recordBuildSucceed", this.#theme)
|
|
335
337
|
this.#history.push({
|
|
336
338
|
timestamp: buildStart,
|
|
337
339
|
loadTime: loadCost,
|
|
@@ -352,38 +354,47 @@ export default class Session {
|
|
|
352
354
|
error: error.message
|
|
353
355
|
})
|
|
354
356
|
|
|
355
|
-
Sass.new("Build process failed.", error)
|
|
357
|
+
throw Sass.new("Build process failed.", error)
|
|
356
358
|
} finally {
|
|
357
359
|
this.#building = false
|
|
358
|
-
this.#command.asyncEmit("finishedBuilding")
|
|
360
|
+
await this.#command.asyncEmit("finishedBuilding")
|
|
359
361
|
}
|
|
360
362
|
}
|
|
361
363
|
|
|
362
364
|
/**
|
|
363
365
|
* Handles a file change event and triggers a rebuild for the theme.
|
|
364
366
|
*
|
|
365
|
-
* @param {string} changed - Path to the changed file
|
|
367
|
+
* @param {string} changed - Path to the changed file (from chokidar)
|
|
366
368
|
* @param {object} _stats - OS-level file stat information
|
|
367
369
|
* @returns {Promise<void>}
|
|
368
370
|
*/
|
|
369
371
|
async #handleFileChange(changed, _stats) {
|
|
372
|
+
let startedPipeline = false
|
|
373
|
+
|
|
370
374
|
try {
|
|
371
375
|
if(this.#building)
|
|
372
376
|
return
|
|
373
377
|
|
|
374
378
|
this.#building = true
|
|
375
|
-
this.#command.asyncEmit("building")
|
|
379
|
+
await this.#command.asyncEmit("building")
|
|
380
|
+
|
|
381
|
+
// Normalize the changed path from chokidar for comparison
|
|
382
|
+
const normalizedChanged = path.resolve(changed)
|
|
376
383
|
|
|
377
384
|
const changedFile = Array.from(this.#theme.getDependencies()).find(
|
|
378
|
-
dep =>
|
|
385
|
+
dep => {
|
|
386
|
+
const depPath = dep.getSourceFile().real.path
|
|
387
|
+
const normalizedDepPath = path.resolve(depPath)
|
|
388
|
+
|
|
389
|
+
return normalizedDepPath === normalizedChanged
|
|
390
|
+
}
|
|
379
391
|
)?.getSourceFile()
|
|
380
392
|
|
|
381
393
|
if(!changedFile)
|
|
382
394
|
return
|
|
383
395
|
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
)
|
|
396
|
+
const cwd = this.#theme.getCwd()
|
|
397
|
+
const fileName = changedFile.relativeTo(cwd)
|
|
387
398
|
|
|
388
399
|
const message = [
|
|
389
400
|
["info", "REBUILDING", ["[","]"]],
|
|
@@ -396,17 +407,24 @@ export default class Session {
|
|
|
396
407
|
Term.status(message)
|
|
397
408
|
|
|
398
409
|
await this.#resetWatcher()
|
|
410
|
+
startedPipeline = true
|
|
399
411
|
await this.#buildPipeline()
|
|
412
|
+
} catch(error) {
|
|
413
|
+
const sassError = Sass.new("Handling file change.", error)
|
|
414
|
+
sassError.report(this.#options?.nerd)
|
|
415
|
+
|
|
416
|
+
if(!startedPipeline)
|
|
417
|
+
await this.#command.asyncEmit("finishedBuilding")
|
|
400
418
|
} finally {
|
|
401
419
|
this.#building = false
|
|
402
420
|
}
|
|
403
421
|
}
|
|
404
422
|
|
|
405
423
|
/**
|
|
406
|
-
* Displays a formatted summary of the session's build statistics and
|
|
407
|
-
* Shows total builds, success/failure counts, success rate
|
|
408
|
-
* information from the most recent build. Used during
|
|
409
|
-
* final statistics to the user.
|
|
424
|
+
* Displays a formatted summary of the session's build statistics and
|
|
425
|
+
* performance. Shows total builds, success/failure counts, success rate
|
|
426
|
+
* percentage, and timing information from the most recent build. Used during
|
|
427
|
+
* session cleanup to provide final statistics to the user.
|
|
410
428
|
*
|
|
411
429
|
* @returns {void}
|
|
412
430
|
*/
|
|
@@ -454,14 +472,16 @@ export default class Session {
|
|
|
454
472
|
return
|
|
455
473
|
|
|
456
474
|
try {
|
|
457
|
-
this.#command.asyncEmit("recordBuildStart", this.#theme)
|
|
475
|
+
await this.#command.asyncEmit("recordBuildStart", this.#theme)
|
|
458
476
|
this.#building = true
|
|
459
477
|
await this.#resetWatcher()
|
|
460
|
-
this.#command.asyncEmit("building")
|
|
478
|
+
await this.#command.asyncEmit("building")
|
|
461
479
|
await this.#buildPipeline(true)
|
|
462
480
|
} catch(error) {
|
|
463
481
|
await this.#command.asyncEmit("recordBuildFail", this.#theme)
|
|
464
|
-
|
|
482
|
+
const sassError = Sass.new("Handling rebuild request.", error)
|
|
483
|
+
sassError.report(this.#options?.nerd)
|
|
484
|
+
throw sassError
|
|
465
485
|
} finally {
|
|
466
486
|
this.#building = false
|
|
467
487
|
}
|
|
@@ -476,9 +496,15 @@ export default class Session {
|
|
|
476
496
|
if(this.#watcher)
|
|
477
497
|
await this.#watcher.close()
|
|
478
498
|
|
|
499
|
+
// Get real paths for chokidar (normalized for consistency)
|
|
479
500
|
const dependencies = Array.from(this.#theme
|
|
480
501
|
.getDependencies())
|
|
481
|
-
.map(d =>
|
|
502
|
+
.map(d => {
|
|
503
|
+
const filePath = d.getSourceFile().real.path
|
|
504
|
+
|
|
505
|
+
// Normalize to absolute path for chokidar
|
|
506
|
+
return path.resolve(filePath)
|
|
507
|
+
})
|
|
482
508
|
|
|
483
509
|
this.#watcher = chokidar.watch(dependencies, {
|
|
484
510
|
// Prevent watching own output files
|
package/src/Theme.js
CHANGED
|
@@ -13,10 +13,16 @@
|
|
|
13
13
|
* - Write output files, supporting dry-run and hash-based skip
|
|
14
14
|
* - Support watch mode for live theme development
|
|
15
15
|
*/
|
|
16
|
-
import {Sass,
|
|
16
|
+
import {Sass, Term, Util} from "@gesslar/toolkit"
|
|
17
17
|
import Compiler from "./Compiler.js"
|
|
18
18
|
import ThemePool from "./ThemePool.js"
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* @import {Cache} from "@gesslar/toolkit"
|
|
22
|
+
* @import {DirectoryObject} from "@gesslar/toolkit"
|
|
23
|
+
* @import {FileObject} from "@gesslar/toolkit"
|
|
24
|
+
*/
|
|
25
|
+
|
|
20
26
|
const outputFileExtension = "color-theme.json"
|
|
21
27
|
const obviouslyASentinelYouCantMissSoShutUpAboutIt = "kakadoodoo"
|
|
22
28
|
|
|
@@ -39,6 +45,7 @@ export default class Theme {
|
|
|
39
45
|
#sourceFile = null
|
|
40
46
|
#source = null
|
|
41
47
|
#options = null
|
|
48
|
+
|
|
42
49
|
/**
|
|
43
50
|
* The dependencies of this theme.
|
|
44
51
|
*
|
|
@@ -56,6 +63,8 @@ export default class Theme {
|
|
|
56
63
|
#outputJson = null
|
|
57
64
|
#outputFileName = null
|
|
58
65
|
#outputHash = null
|
|
66
|
+
#outputFile = null
|
|
67
|
+
#outputDir = null
|
|
59
68
|
|
|
60
69
|
#cwd = null
|
|
61
70
|
|
|
@@ -72,6 +81,16 @@ export default class Theme {
|
|
|
72
81
|
this.#outputFileName = `${this.#name}.${outputFileExtension}`
|
|
73
82
|
this.#options = options
|
|
74
83
|
this.#cwd = cwd
|
|
84
|
+
|
|
85
|
+
// Let's create the output directory, since we're gonna needs it.
|
|
86
|
+
// If outputDir is not provided or is ".", use the cwd itself
|
|
87
|
+
const outputDir = options.outputDir && options.outputDir !== "."
|
|
88
|
+
? cwd.getDirectory(options.outputDir)
|
|
89
|
+
: cwd
|
|
90
|
+
const outputFile = outputDir.getFile(this.#outputFileName)
|
|
91
|
+
|
|
92
|
+
this.#outputFile = outputFile
|
|
93
|
+
this.#outputDir = outputDir
|
|
75
94
|
}
|
|
76
95
|
|
|
77
96
|
/**
|
|
@@ -479,7 +498,7 @@ export default class Theme {
|
|
|
479
498
|
|
|
480
499
|
if(!source[PropertyKey.CONFIG.description])
|
|
481
500
|
throw Sass.new(
|
|
482
|
-
`Source file does not contain '${PropertyKey.CONFIG.description}' property: ${this.#sourceFile.
|
|
501
|
+
`Source file does not contain '${PropertyKey.CONFIG.description}' property: ${this.#sourceFile.relativeTo(this.#cwd)}`
|
|
483
502
|
)
|
|
484
503
|
|
|
485
504
|
this.#source = source
|
|
@@ -497,7 +516,6 @@ export default class Theme {
|
|
|
497
516
|
*/
|
|
498
517
|
async build() {
|
|
499
518
|
const compiler = new Compiler()
|
|
500
|
-
|
|
501
519
|
await compiler.compile(this)
|
|
502
520
|
|
|
503
521
|
return this
|
|
@@ -512,8 +530,7 @@ export default class Theme {
|
|
|
512
530
|
*/
|
|
513
531
|
async write(force=false) {
|
|
514
532
|
const output = this.#outputJson
|
|
515
|
-
const
|
|
516
|
-
const file = new FileObject(this.#outputFileName, outputDir)
|
|
533
|
+
const file = this.#outputFile
|
|
517
534
|
|
|
518
535
|
if(this.#options.dryRun) {
|
|
519
536
|
Term.log(this.#outputJson)
|
|
@@ -525,7 +542,7 @@ export default class Theme {
|
|
|
525
542
|
if(!force) {
|
|
526
543
|
const nextHash = this.#outputHash
|
|
527
544
|
const lastHash = await file.exists
|
|
528
|
-
? Util.hashOf(await
|
|
545
|
+
? Util.hashOf(await file.read())
|
|
529
546
|
: obviouslyASentinelYouCantMissSoShutUpAboutIt
|
|
530
547
|
|
|
531
548
|
if(lastHash === nextHash)
|
|
@@ -533,10 +550,10 @@ export default class Theme {
|
|
|
533
550
|
}
|
|
534
551
|
|
|
535
552
|
// Real write (timed)
|
|
536
|
-
if(!await outputDir.exists)
|
|
537
|
-
await
|
|
553
|
+
if(!await this.#outputDir.exists)
|
|
554
|
+
await this.#outputDir.assureExists()
|
|
538
555
|
|
|
539
|
-
await
|
|
556
|
+
await file.write(output)
|
|
540
557
|
|
|
541
558
|
return {status: WriteStatus.WRITTEN, bytes: output.length, file}
|
|
542
559
|
}
|
package/src/cli.js
CHANGED
|
@@ -36,7 +36,7 @@ import process from "node:process"
|
|
|
36
36
|
import url from "node:url"
|
|
37
37
|
import c from "@gesslar/colours"
|
|
38
38
|
|
|
39
|
-
import {Cache,
|
|
39
|
+
import {Cache, DirectoryObject, FileObject, Sass, Term} from "@gesslar/toolkit"
|
|
40
40
|
import BuildCommand from "./BuildCommand.js"
|
|
41
41
|
import LintCommand from "./LintCommand.js"
|
|
42
42
|
import ResolveCommand from "./ResolveCommand.js"
|
|
@@ -74,9 +74,11 @@ void (async function main() {
|
|
|
74
74
|
c.alias.set("modified-bracket", "{F165}")
|
|
75
75
|
c.alias.set("muted", "{F240}")
|
|
76
76
|
c.alias.set("muted-bracket", "{F244}")
|
|
77
|
+
|
|
77
78
|
// Lint command
|
|
78
79
|
c.alias.set("context", "{F159}")
|
|
79
80
|
c.alias.set("loc", "{F148}")
|
|
81
|
+
|
|
80
82
|
// Resolve command
|
|
81
83
|
c.alias.set("head", "{F220}")
|
|
82
84
|
c.alias.set("leaf", "{F151}")
|
|
@@ -89,11 +91,12 @@ void (async function main() {
|
|
|
89
91
|
c.alias.set("arrow", "{F033}")
|
|
90
92
|
|
|
91
93
|
const cache = new Cache()
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
const cwd = DirectoryObject.fromCwd()
|
|
95
|
+
const packageJson = new FileObject(
|
|
96
|
+
"package.json",
|
|
97
|
+
url.fileURLToPath(new url.URL("..", import.meta.url))
|
|
98
|
+
)
|
|
99
|
+
const pkgJson = await packageJson.loadData()
|
|
97
100
|
|
|
98
101
|
// These are available to all subcommands in addition to whatever they
|
|
99
102
|
// provide.
|
|
@@ -116,36 +119,13 @@ void (async function main() {
|
|
|
116
119
|
command.addCliOptions(alwaysAvailable, false)
|
|
117
120
|
}
|
|
118
121
|
|
|
119
|
-
// // Add the build subcommand
|
|
120
|
-
// const buildCommand = new BuildCommand({cwd, packageJson: pkgJson})
|
|
121
|
-
|
|
122
|
-
// buildCommand.cache = cache
|
|
123
|
-
|
|
124
|
-
// void(await buildCommand.buildCli(program))
|
|
125
|
-
// .addCliOptions(alwaysAvailable, false)
|
|
126
|
-
|
|
127
|
-
// // Add the resolve subcommand
|
|
128
|
-
// const resolveCommand = new ResolveCommand({cwd, packageJson: pkgJson})
|
|
129
|
-
|
|
130
|
-
// resolveCommand.cache = cache
|
|
131
|
-
|
|
132
|
-
// void(await resolveCommand.buildCli(program))
|
|
133
|
-
// .addCliOptions(alwaysAvailable, false)
|
|
134
|
-
|
|
135
|
-
// // Add the lint subcommand
|
|
136
|
-
// const lintCommand = new LintCommand({cwd, packageJson: pkgJson})
|
|
137
|
-
|
|
138
|
-
// lintCommand.cache = cache
|
|
139
|
-
|
|
140
|
-
// void(await lintCommand.buildCli(program))
|
|
141
|
-
// .addCliOptions(alwaysAvailable, false)
|
|
142
|
-
|
|
143
122
|
// Let'er rip, bitches! VROOM VROOM, motherfucker!!
|
|
144
123
|
await program.parseAsync()
|
|
145
124
|
|
|
146
125
|
} catch(error) {
|
|
147
|
-
Sass
|
|
148
|
-
.
|
|
126
|
+
Sass
|
|
127
|
+
.from(error, "Starting Sassy.")
|
|
128
|
+
.report(sassyOptions.nerd ?? false)
|
|
149
129
|
|
|
150
130
|
process.exit(1)
|
|
151
131
|
}
|