@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 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": "0.23.0",
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.7.1",
47
- "@gesslar/toolkit": "^3.6.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": "^0.5.1",
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
- "test": "node -p \"require('fs').readFileSync('TESTING.txt', 'utf8')\"",
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",
@@ -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
- * @param {string} [options.output-dir] - Custom output directory path
47
- * @param {boolean} [options.dry-run] - Print JSON to stdout without writing files
48
- * @param {boolean} [options.silent] - Silent mode, only show errors or dry-run output
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 Promise.allSettled(
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(sessionResults.some(theme => theme.status === "rejected")) {
78
- const rejected = sessionResults.filter(result => result.status === "rejected")
97
+ if(Promised.hasRejected(sessionResults))
98
+ Promised.throw("Creating sessions.", sessionResults)
79
99
 
80
- rejected.forEach(item => Term.error(item.reason))
81
- process.exit(1)
82
- }
100
+ // if(sessionResults.some(theme => theme.status === "rejected")) {
101
+ // const rejected = sessionResults.filter(result => result.status === "rejected")
83
102
 
84
- const sessions = sessionResults.map(result => result.value)
85
- const firstRun = await Promise.allSettled(sessions.map(
86
- async session => await session.run()))
87
- const rejected = firstRun.filter(reject => reject.status === "rejected")
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
- if(rejected.length > 0) {
90
- rejected.forEach(reject => Term.error(reject.reason))
110
+ const sessions = Promised.values(sessionResults)
111
+ const firstRun = await Promised.settle(sessions.map(
112
+ async session => await session.run()))
91
113
 
92
- if(firstRun.length === rejected.length)
93
- await Util.asyncEmit(this.emitter, "quit")
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 {Util, Sass} from "@gesslar/toolkit"
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 {Sass, FileObject} from "@gesslar/toolkit"
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 {object} cwd - The current working directory object
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
- const fileObject = new FileObject(fileName, cwd)
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.path}`)
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 {Data, FS, FileObject, Sass, Term, Util} from "@gesslar/toolkit"
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((themeData,file) => {
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(!Data.isArrayUniform(imports, "string"))
152
- throw new Sass(
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 = new FileObject(importing, theme.getSourceFile().directory)
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", `${FS.relativeOrAbsolutePath(theme.getCwd(),file)}`],
180
+ ["muted", `${file.relativeTo(cwd)}`],
172
181
  ["muted", `${theme.getName()}`,["(",")"]],
173
182
  ], theme.getOptions())
174
183
  }
@@ -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
 
@@ -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, Data} from "@gesslar/toolkit"
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
- Data.arrayIntersection(cliOptionNames, Object.keys(options))
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 {Sass, FS, Term, Util} from "@gesslar/toolkit"
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 FS.fileSize(this.#theme.getSourceFile())
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 compileResult = await Promise
240
- .allSettled(dependencyFiles.map(async fileObject => {
241
- if(!fileObject) {
242
- throw new Error("Invalid dependency file object")
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
- const fileName = FS.relativeOrAbsolutePath(
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
- const rejected = compileResult.filter(result => result.status === "rejected")
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 = compileResult
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 = compileResult.reduce(
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", `${compileResult[0].value[1].toLocaleString()} bytes`, ["[","]"]],
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 = FS.relativeOrAbsolutePath(
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).report(this.#options.nerd)
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 => dep.getSourceFile().path === changed
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 fileName = FS.relativeOrAbsolutePath(
385
- this.#command.getCwd(), changedFile
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 performance.
407
- * Shows total builds, success/failure counts, success rate percentage, and timing
408
- * information from the most recent build. Used during session cleanup to provide
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
- throw Sass.new("Handling rebuild request.", error)
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 => d.getSourceFile().path)
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, DirectoryObject, FS, FileObject, Term, Util} from "@gesslar/toolkit"
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.path}`
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 outputDir = new DirectoryObject(this.#options.outputDir)
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 FS.readFile(file))
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 FS.assureDirectory(outputDir, {recursive: true})
553
+ if(!await this.#outputDir.exists)
554
+ await this.#outputDir.assureExists()
538
555
 
539
- await FS.writeFile(file, output)
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, Sass, DirectoryObject, FileObject, Term} from "@gesslar/toolkit"
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 cr = new DirectoryObject(url.fileURLToPath(new url.URL("..", import.meta.url)))
93
- const cwd = new DirectoryObject(process.cwd())
94
- const packageJson = new FileObject("package.json", cr)
95
- const pkgJsonResult = await cache.loadCachedData(packageJson)
96
- const pkgJson = pkgJsonResult
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.new("Starting Sassy.", error)
148
- .report(sassyOptions.nerd || true)
126
+ Sass
127
+ .from(error, "Starting Sassy.")
128
+ .report(sassyOptions.nerd ?? false)
149
129
 
150
130
  process.exit(1)
151
131
  }