@gesslar/sassy 0.19.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 +605 -0
- package/UNLICENSE.txt +24 -0
- package/package.json +60 -0
- package/src/BuildCommand.js +183 -0
- package/src/Cache.js +73 -0
- package/src/Colour.js +414 -0
- package/src/Command.js +212 -0
- package/src/Compiler.js +310 -0
- package/src/Data.js +545 -0
- package/src/DirectoryObject.js +188 -0
- package/src/Evaluator.js +348 -0
- package/src/File.js +334 -0
- package/src/FileObject.js +226 -0
- package/src/LintCommand.js +498 -0
- package/src/ResolveCommand.js +433 -0
- package/src/Sass.js +165 -0
- package/src/Session.js +360 -0
- package/src/Term.js +175 -0
- package/src/Theme.js +289 -0
- package/src/ThemePool.js +139 -0
- package/src/ThemeToken.js +280 -0
- package/src/Type.js +206 -0
- package/src/Util.js +132 -0
- package/src/Valid.js +50 -0
- package/src/cli.js +155 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import c from "@gesslar/colours"
|
|
2
|
+
// import colorSupport from "color-support"
|
|
3
|
+
|
|
4
|
+
import Command from "./Command.js"
|
|
5
|
+
import Sass from "./Sass.js"
|
|
6
|
+
import Colour from "./Colour.js"
|
|
7
|
+
import Evaluator from "./Evaluator.js"
|
|
8
|
+
import Term from "./Term.js"
|
|
9
|
+
import Theme from "./Theme.js"
|
|
10
|
+
import Util from "./Util.js"
|
|
11
|
+
import Data from "./Data.js"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
// ansiColors.enabled = colorSupport.hasBasic
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Command handler for resolving theme tokens and variables to their final values.
|
|
18
|
+
* Provides introspection into the theme resolution process and variable dependencies.
|
|
19
|
+
*/
|
|
20
|
+
export default class ResolveCommand extends Command {
|
|
21
|
+
/**
|
|
22
|
+
* Creates a new ResolveCommand instance.
|
|
23
|
+
*
|
|
24
|
+
* @param {object} base - Base configuration containing cwd and packageJson
|
|
25
|
+
*/
|
|
26
|
+
constructor(base) {
|
|
27
|
+
super(base)
|
|
28
|
+
|
|
29
|
+
this.cliCommand = "resolve <file>"
|
|
30
|
+
this.cliOptions = {
|
|
31
|
+
"color": ["-c, --color <key>", "resolve a color key to its final evaluated value"],
|
|
32
|
+
"tokenColor": ["-t, --tokenColor <scope>", "resolve a tokenColors scope to its final evaluated value"],
|
|
33
|
+
"semanticTokenColor": ["-s, --semanticTokenColor <scope>", "resolve a semanticTokenColors scope to its final evaluated value"],
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Executes the resolve command for a given theme file and option.
|
|
39
|
+
* Validates mutual exclusivity of options and delegates to appropriate resolver.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} inputArg - Path to the theme file to resolve
|
|
42
|
+
* @param {object} options - Resolution options (token, etc.)
|
|
43
|
+
* @returns {Promise<void>} Resolves when resolution is complete
|
|
44
|
+
*/
|
|
45
|
+
async execute(inputArg, options={}) {
|
|
46
|
+
const intersection =
|
|
47
|
+
Data.arrayIntersection(this.cliOptionNames, Object.keys(options))
|
|
48
|
+
|
|
49
|
+
if(intersection.length > 1)
|
|
50
|
+
throw Sass.new(
|
|
51
|
+
`The options ${this.cliOptionNames.join(", ")} are ` +
|
|
52
|
+
`mutually exclusive and may only have one expressed in the request.`
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
const {cwd} = this
|
|
56
|
+
const optionName = Object.keys(options??{})
|
|
57
|
+
.find(o => this.cliOptionNames.includes(o))
|
|
58
|
+
|
|
59
|
+
if(!optionName) {
|
|
60
|
+
throw Sass.new(
|
|
61
|
+
`No valid option provided. Please specify one of: ${this.cliOptionNames.join(", ")}.`
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const resolveFunctionName = `resolve${Util.capitalize(optionName)}`
|
|
66
|
+
const optionValue = options[optionName]
|
|
67
|
+
const resolverFunction = this[resolveFunctionName]
|
|
68
|
+
|
|
69
|
+
if(!(resolverFunction && typeof resolverFunction === "function"))
|
|
70
|
+
throw Sass.new(`No such function ${resolveFunctionName}`)
|
|
71
|
+
|
|
72
|
+
const fileObject = await this.resolveThemeFileName(inputArg, cwd)
|
|
73
|
+
const theme = new Theme(fileObject, cwd, options)
|
|
74
|
+
theme.cache = this.cache
|
|
75
|
+
|
|
76
|
+
await theme.load()
|
|
77
|
+
await theme.build()
|
|
78
|
+
|
|
79
|
+
await resolverFunction.call(this, theme, optionValue)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Resolves a specific color to its final value and displays the resolution trail.
|
|
84
|
+
* Shows the complete dependency chain for the requested color.
|
|
85
|
+
*
|
|
86
|
+
* @param {object} theme - The compiled theme object with pool
|
|
87
|
+
* @param {string} colorName - The color key to resolve
|
|
88
|
+
* @returns {void}
|
|
89
|
+
*/
|
|
90
|
+
async resolveColor(theme, colorName) {
|
|
91
|
+
const pool = theme.pool
|
|
92
|
+
if(!pool || !pool.has(colorName))
|
|
93
|
+
return Term.info(`'${colorName}' not found.`)
|
|
94
|
+
|
|
95
|
+
const tokens = pool.getTokens
|
|
96
|
+
const token = tokens.get(colorName)
|
|
97
|
+
const trail = token.getTrail()
|
|
98
|
+
const fullTrail = this.#buildCompleteTrail(token, trail)
|
|
99
|
+
// Get the final resolved value
|
|
100
|
+
const finalValue = token.getValue()
|
|
101
|
+
const [formattedFinalValue] = this.#formatLeaf(finalValue)
|
|
102
|
+
|
|
103
|
+
const output = c`\n{head}${colorName}{/}:\n\n${this.#formatOutput(fullTrail)}\n\n{head}${"Resolution:"}{/} ${formattedFinalValue}`
|
|
104
|
+
|
|
105
|
+
Term.info(output)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Resolves a specific tokenColors scope to its final value and displays the resolution trail.
|
|
110
|
+
* Shows all matching scopes with disambiguation when multiple matches are found.
|
|
111
|
+
*
|
|
112
|
+
* @param {object} theme - The compiled theme object with output
|
|
113
|
+
* @param {string} scopeName - The scope to resolve (e.g., "entity.name.class" or "entity.name.class.1")
|
|
114
|
+
* @returns {void}
|
|
115
|
+
*/
|
|
116
|
+
async resolveTokenColor(theme, scopeName) {
|
|
117
|
+
const tokenColors = theme.output?.tokenColors || []
|
|
118
|
+
|
|
119
|
+
// Check if this is a disambiguated scope (ends with .1, .2, etc.)
|
|
120
|
+
const disambiguatedMatch = scopeName.match(/^(.+)\.(\d+)$/)
|
|
121
|
+
|
|
122
|
+
if(disambiguatedMatch) {
|
|
123
|
+
const [, baseScope, indexStr] = disambiguatedMatch
|
|
124
|
+
const index = parseInt(indexStr) - 1 // Convert to 0-based index
|
|
125
|
+
|
|
126
|
+
const matches = this.#findScopeMatches(tokenColors, baseScope)
|
|
127
|
+
|
|
128
|
+
if(index >= 0 && index < matches.length) {
|
|
129
|
+
const match = matches[index]
|
|
130
|
+
await this.#resolveScopeMatch(theme, match, `${baseScope}.${indexStr}`)
|
|
131
|
+
return
|
|
132
|
+
} else {
|
|
133
|
+
return Term.info(`'${scopeName}' not found. Available: ${baseScope}.1 through ${baseScope}.${matches.length}`)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Find all matching scopes
|
|
138
|
+
const matches = this.#findScopeMatches(tokenColors, scopeName)
|
|
139
|
+
|
|
140
|
+
if(matches.length === 0) {
|
|
141
|
+
return Term.info(`No tokenColors entries found for scope '${scopeName}'`)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if(matches.length === 1) {
|
|
145
|
+
// Single match - resolve directly
|
|
146
|
+
await this.#resolveScopeMatch(theme, matches[0], scopeName)
|
|
147
|
+
} else {
|
|
148
|
+
// Multiple matches - show disambiguation options
|
|
149
|
+
Term.info(`Multiple entries found for '${scopeName}', please try again with the specific query:\n`)
|
|
150
|
+
matches.forEach((match, index) => {
|
|
151
|
+
const name = match.name || `Entry ${index + 1}`
|
|
152
|
+
Term.info(`${name}: ${scopeName}.${index + 1}`)
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#findScopeMatches(tokenColors, targetScope) {
|
|
158
|
+
return tokenColors.filter(entry => {
|
|
159
|
+
if(!entry.scope)
|
|
160
|
+
return false
|
|
161
|
+
|
|
162
|
+
// Handle comma-separated scopes
|
|
163
|
+
const scopes = entry.scope.split(",").map(s => s.trim())
|
|
164
|
+
return scopes.includes(targetScope)
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async #resolveScopeMatch(theme, match, displayName) {
|
|
169
|
+
const pool = theme.pool
|
|
170
|
+
const settings = match.settings || {}
|
|
171
|
+
const name = match.name || "Unnamed"
|
|
172
|
+
|
|
173
|
+
// Look for the foreground property specifically
|
|
174
|
+
const foreground = settings.foreground
|
|
175
|
+
if(!foreground) {
|
|
176
|
+
return Term.info(`${displayName} (${name})\n\n(no foreground property)`)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// First, try to find the token by looking for variables that resolve to this value
|
|
180
|
+
// but prioritize source variable names over computed results
|
|
181
|
+
const tokens = pool ? pool.getTokens : new Map()
|
|
182
|
+
let bestToken = null
|
|
183
|
+
|
|
184
|
+
// First try to find a scope.* token that matches
|
|
185
|
+
for(const [tokenName, token] of tokens) {
|
|
186
|
+
if(token.getValue() === foreground && tokenName.startsWith("scope.")) {
|
|
187
|
+
bestToken = token
|
|
188
|
+
break
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// If no scope token found, look for other variable-like tokens
|
|
193
|
+
if(!bestToken) {
|
|
194
|
+
for(const [tokenName, token] of tokens) {
|
|
195
|
+
if(token.getValue() === foreground) {
|
|
196
|
+
// Prefer tokens that look like variable names (scope.*, colors.*, etc.)
|
|
197
|
+
// over computed function results
|
|
198
|
+
if(tokenName.includes(".") && !tokenName.includes("(") && !tokenName.includes("#")) {
|
|
199
|
+
bestToken = token
|
|
200
|
+
break
|
|
201
|
+
} else if(!bestToken) {
|
|
202
|
+
bestToken = token // fallback to any matching token
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if(!bestToken) {
|
|
209
|
+
return Term.info(`${displayName} (${name})\n\n(resolved to static value: ${foreground})`)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const trail = bestToken.getTrail()
|
|
213
|
+
const fullTrail = this.#buildCompleteTrail(bestToken, trail)
|
|
214
|
+
const finalValue = bestToken.getValue()
|
|
215
|
+
const [formattedFinalValue] = this.#formatLeaf(finalValue)
|
|
216
|
+
|
|
217
|
+
const output = c`{head}${displayName}{/} {hex}${(`${name}`)}{/}\n${this.#formatOutput(fullTrail)}\n\n{head}${"Resolution:"}{/} ${formattedFinalValue}`
|
|
218
|
+
|
|
219
|
+
Term.info(output)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Resolves a specific semanticTokenColors scope to its final value.
|
|
224
|
+
* Uses the same logic as tokenColors since they have identical structure.
|
|
225
|
+
*
|
|
226
|
+
* @param {object} theme - The compiled theme object with output
|
|
227
|
+
* @param {string} scopeName - The scope to resolve (e.g., "keyword" or "keyword.1")
|
|
228
|
+
* @returns {void}
|
|
229
|
+
*/
|
|
230
|
+
async resolveSemanticTokenColor(theme, scopeName) {
|
|
231
|
+
// semanticTokenColors has the same structure as tokenColors, so we can reuse the logic
|
|
232
|
+
// but we need to look at the semanticTokenColors array instead
|
|
233
|
+
const originalTokenColors = theme.output?.tokenColors
|
|
234
|
+
|
|
235
|
+
// Temporarily replace tokenColors with semanticTokenColors for resolution
|
|
236
|
+
if(theme.output?.semanticTokenColors) {
|
|
237
|
+
theme.output.tokenColors = theme.output.semanticTokenColors
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
await this.resolveTokenColor(theme, scopeName)
|
|
241
|
+
|
|
242
|
+
// Restore original tokenColors
|
|
243
|
+
if(originalTokenColors) {
|
|
244
|
+
theme.output.tokenColors = originalTokenColors
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
#buildCompleteTrail(rootToken, trail) {
|
|
249
|
+
const steps = []
|
|
250
|
+
const seen = new Set()
|
|
251
|
+
|
|
252
|
+
// Add the root token's original expression
|
|
253
|
+
const rootRaw = rootToken.getRawValue()
|
|
254
|
+
if(rootRaw !== rootToken.getName()) {
|
|
255
|
+
steps.push({
|
|
256
|
+
value: rootRaw,
|
|
257
|
+
type: "expression",
|
|
258
|
+
level: 0
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Build a flattened sequence showing the resolution process
|
|
263
|
+
const processToken = (token, level) => {
|
|
264
|
+
if(!token)
|
|
265
|
+
return
|
|
266
|
+
|
|
267
|
+
const id = `${token.getName()}-${token.getRawValue()}`
|
|
268
|
+
if(seen.has(id))
|
|
269
|
+
return
|
|
270
|
+
|
|
271
|
+
seen.add(id)
|
|
272
|
+
|
|
273
|
+
const rawValue = token.getRawValue()
|
|
274
|
+
const finalValue = token.getValue()
|
|
275
|
+
const dependency = token.getDependency()
|
|
276
|
+
const kind = token.getKind()
|
|
277
|
+
|
|
278
|
+
// Add the current step
|
|
279
|
+
if(!steps.some(s => s.value === rawValue)) {
|
|
280
|
+
steps.push({
|
|
281
|
+
value: rawValue,
|
|
282
|
+
type: kind === "function" ? "function" : "variable",
|
|
283
|
+
level
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// For variables, show what they resolve to
|
|
288
|
+
if(dependency) {
|
|
289
|
+
const depRaw = dependency.getRawValue()
|
|
290
|
+
const depFinal = dependency.getValue()
|
|
291
|
+
|
|
292
|
+
// Add dependency's expression if it's a function call
|
|
293
|
+
if(depRaw !== dependency.getName() && !steps.some(s => s.value === depRaw)) {
|
|
294
|
+
steps.push({
|
|
295
|
+
value: depRaw,
|
|
296
|
+
type: "expression",
|
|
297
|
+
level: level + 1
|
|
298
|
+
})
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Process dependency's trail
|
|
302
|
+
const depTrail = dependency.getTrail()
|
|
303
|
+
if(depTrail && depTrail.length > 0) {
|
|
304
|
+
depTrail.forEach(depToken => processToken(depToken, level + 1))
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Add resolved color if different
|
|
308
|
+
if(depRaw !== depFinal && !steps.some(s => s.value === depFinal)) {
|
|
309
|
+
steps.push({
|
|
310
|
+
value: depFinal,
|
|
311
|
+
type: "result",
|
|
312
|
+
level: level + 1
|
|
313
|
+
})
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Add final result for this token
|
|
318
|
+
if(rawValue !== finalValue && !steps.some(s => s.value === finalValue)) {
|
|
319
|
+
steps.push({
|
|
320
|
+
value: finalValue,
|
|
321
|
+
type: "result",
|
|
322
|
+
level
|
|
323
|
+
})
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
trail.forEach(token => processToken(token, 1))
|
|
328
|
+
|
|
329
|
+
// Normalize levels to reduce excessive nesting
|
|
330
|
+
const levelMap = new Map()
|
|
331
|
+
let normalizedLevel = 0
|
|
332
|
+
|
|
333
|
+
steps.forEach(step => {
|
|
334
|
+
if(!levelMap.has(step.level)) {
|
|
335
|
+
levelMap.set(step.level, Math.min(normalizedLevel++, 4)) // Cap at depth 4
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
step.depth = levelMap.get(step.level)
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
return steps
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Formats a list of resolution steps into a visually indented tree structure for display.
|
|
345
|
+
*
|
|
346
|
+
* Each step represents a part of the theme token resolution process, including variables,
|
|
347
|
+
* function calls, expressions, and final results. The output is colorized and indented
|
|
348
|
+
* according to the step's depth and type, making the dependency chain and resolution
|
|
349
|
+
* process easy to follow in terminal output.
|
|
350
|
+
*
|
|
351
|
+
* - Hex color results are indented one extra level and prefixed with an arrow for emphasis.
|
|
352
|
+
* - Other steps (variables, functions, literals) are indented according to their depth.
|
|
353
|
+
*
|
|
354
|
+
* @param {Array} steps - List of resolution steps, each with {value, depth, type}.
|
|
355
|
+
* - value: The string value to display (token, expression, result, etc.)
|
|
356
|
+
* - depth: Indentation level for the step
|
|
357
|
+
* - type: The kind of step ("result", "variable", "function", "expression", etc.)
|
|
358
|
+
* @returns {string} Formatted, colorized, and indented output for terminal display.
|
|
359
|
+
*/
|
|
360
|
+
#formatOutput(steps) {
|
|
361
|
+
if(steps.length === 0)
|
|
362
|
+
return ""
|
|
363
|
+
|
|
364
|
+
const out = []
|
|
365
|
+
|
|
366
|
+
steps.forEach(step => {
|
|
367
|
+
const {value, depth, type} = step
|
|
368
|
+
const [line, kind] = this.#formatLeaf(value)
|
|
369
|
+
|
|
370
|
+
// Simple logic: only hex results get extra indentation with arrow, everything else is clean
|
|
371
|
+
if(type === "result" && kind === "hex") {
|
|
372
|
+
// Hex results are indented one extra level with just spaces and arrow
|
|
373
|
+
const prefix = " ".repeat(depth + 1)
|
|
374
|
+
const arrow = c`{arrow}→{/} `
|
|
375
|
+
out.push(`${prefix}${arrow}${line}`)
|
|
376
|
+
} else {
|
|
377
|
+
// Everything else just gets clean indentation
|
|
378
|
+
const prefix = " ".repeat(depth)
|
|
379
|
+
out.push(`${prefix}${line}`)
|
|
380
|
+
}
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
return out.join("\n")
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
#func = /^(?<func>\w+)(?<open>\()(?<args>.*)(?<close>\)$)$/
|
|
387
|
+
#sub = Evaluator.sub
|
|
388
|
+
#hex = value => Colour.isHex(value)
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Formats a single ThemeToken for display in the theme resolution output,
|
|
392
|
+
* applying colour and style based on its type.
|
|
393
|
+
*
|
|
394
|
+
* @param {string} value - The man, the mystrery, the value.
|
|
395
|
+
* @returns {string} The formatted and colourised representation of the token.
|
|
396
|
+
*
|
|
397
|
+
* Uses the token's kind property to determine formatting instead of regex matching.
|
|
398
|
+
* Provides clear visual distinction between tokens, functions, colours, and variables.
|
|
399
|
+
*/
|
|
400
|
+
#formatLeaf(value) {
|
|
401
|
+
if(this.#hex(value)) {
|
|
402
|
+
const {colour,alpha} = Colour.longHex.test(value)
|
|
403
|
+
? Colour.longHex.exec(value).groups
|
|
404
|
+
: Colour.shortHex.exec(value).groups
|
|
405
|
+
|
|
406
|
+
return [
|
|
407
|
+
c`{hash}#{/}{hex}${colour.slice(1)}{/}${alpha?`{hexAlpha}${alpha}{/}`:""}`,
|
|
408
|
+
"hex"
|
|
409
|
+
]
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if(this.#func.test(value)) {
|
|
413
|
+
const {func,args} = this.#func.exec(value).groups
|
|
414
|
+
return [
|
|
415
|
+
c`{func}${func}{/}{parens}${"("}{/}{leaf}${args}{/}{parens}${")"}{/}`,
|
|
416
|
+
"function"
|
|
417
|
+
]
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
if(this.#sub.test(value)) {
|
|
422
|
+
const {parens,none,braces} = Evaluator.sub.exec(value)?.groups || {}
|
|
423
|
+
const style = (braces && ["{","}"]) || (parens && ["(",")"]) || (none && ["",""])
|
|
424
|
+
const varValue = braces || parens || none || value
|
|
425
|
+
return [
|
|
426
|
+
c`{func}{/}{parens}${style[0]}{/}{leaf}${varValue}{/}{parens}${style[1]}{/}`,
|
|
427
|
+
"variable"
|
|
428
|
+
]
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return [c`{leaf}${value}{/}`, "literal"]
|
|
432
|
+
}
|
|
433
|
+
}
|
package/src/Sass.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Sass.js
|
|
3
|
+
*
|
|
4
|
+
* Defines the Sass class, a custom error type for theme compilation
|
|
5
|
+
* errors.
|
|
6
|
+
*
|
|
7
|
+
* Supports error chaining, trace management, and formatted reporting for both
|
|
8
|
+
* user-friendly and verbose (nerd) output.
|
|
9
|
+
*
|
|
10
|
+
* Used throughout the theme engine for structured error handling and
|
|
11
|
+
* debugging.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import Term from "./Term.js"
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Custom error class for Sassy theme compilation errors.
|
|
18
|
+
* Provides error chaining, trace management, and formatted error reporting.
|
|
19
|
+
*/
|
|
20
|
+
export default class Sass extends Error {
|
|
21
|
+
#trace = []
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Creates a new Sass instance.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} message - The error message
|
|
27
|
+
* @param {...any} arg - Additional arguments passed to parent Error constructor
|
|
28
|
+
*/
|
|
29
|
+
constructor(message, ...arg) {
|
|
30
|
+
super(message, ...arg)
|
|
31
|
+
|
|
32
|
+
this.trace = message
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Gets the error trace array.
|
|
37
|
+
*
|
|
38
|
+
* @returns {Array<string>} Array of trace messages
|
|
39
|
+
*/
|
|
40
|
+
get trace() {
|
|
41
|
+
return this.#trace
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Adds a message to the beginning of the trace array.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} message - The trace message to add
|
|
48
|
+
*/
|
|
49
|
+
set trace(message) {
|
|
50
|
+
this.#trace.unshift(message)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Adds a trace message and returns this instance for chaining.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} message - The trace message to add
|
|
57
|
+
* @returns {this} This Sass instance for method chaining
|
|
58
|
+
*/
|
|
59
|
+
addTrace(message) {
|
|
60
|
+
if(typeof message !== "string")
|
|
61
|
+
throw Sass.new(`Sass.addTrace expected string, got ${JSON.stringify(message)}`)
|
|
62
|
+
|
|
63
|
+
this.trace = message
|
|
64
|
+
|
|
65
|
+
return this
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Reports the error to the terminal with formatted output.
|
|
70
|
+
* Optionally includes detailed stack trace information.
|
|
71
|
+
*
|
|
72
|
+
* @param {boolean} [nerdMode] - Whether to include detailed stack trace
|
|
73
|
+
*/
|
|
74
|
+
report(nerdMode=false) {
|
|
75
|
+
Term.error(
|
|
76
|
+
`${Term.terminalBracket(["error", "Something Went Wrong"])}\n` +
|
|
77
|
+
this.trace.join("\n")
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if(nerdMode) {
|
|
81
|
+
Term.error(
|
|
82
|
+
"\n" +
|
|
83
|
+
`${Term.terminalBracket(["error", "Nerd Vittles"])}\n` +
|
|
84
|
+
this.#fullBodyMassage(this.stack)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
this.cause?.stack && Term.error(
|
|
88
|
+
"\n" +
|
|
89
|
+
`${Term.terminalBracket(["error", "Rethrown From"])}\n` +
|
|
90
|
+
this.#fullBodyMassage(this.cause?.stack)
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Formats the stack trace for display, removing the first line and
|
|
97
|
+
* formatting each line with appropriate indentation.
|
|
98
|
+
*
|
|
99
|
+
* Note: Returns formatted stack trace or undefined if no stack available.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} stack - The error stack to massage.
|
|
102
|
+
* @returns {string|undefined} Formatted stack trace or undefined
|
|
103
|
+
*/
|
|
104
|
+
#fullBodyMassage(stack) {
|
|
105
|
+
// Remove the first line, it's already been reported
|
|
106
|
+
|
|
107
|
+
stack = stack ?? ""
|
|
108
|
+
|
|
109
|
+
const {rest} = stack.match(/^.*?\n(?<rest>[\s\S]+)$/m)?.groups ?? {}
|
|
110
|
+
const lines = []
|
|
111
|
+
|
|
112
|
+
if(rest) {
|
|
113
|
+
lines.push(
|
|
114
|
+
...rest
|
|
115
|
+
.split("\n")
|
|
116
|
+
.map(line => {
|
|
117
|
+
const at = line.match(/^\s{4}at\s(?<at>.*)$/)?.groups?.at ?? {}
|
|
118
|
+
return at
|
|
119
|
+
? `* ${at}`
|
|
120
|
+
: line
|
|
121
|
+
})
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return lines.join("\n")
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Creates an Sass from an existing Error object with additional
|
|
130
|
+
* trace message.
|
|
131
|
+
*
|
|
132
|
+
* @param {Error} error - The original error object
|
|
133
|
+
* @param {string} message - Additional trace message to add
|
|
134
|
+
* @returns {Sass} New Sass instance with trace from the original error
|
|
135
|
+
* @throws {Sass} If the first parameter is not an Error instance
|
|
136
|
+
*/
|
|
137
|
+
static from(error, message) {
|
|
138
|
+
if(!(error instanceof Error))
|
|
139
|
+
throw Sass.new("Sass.from must take an Error object.")
|
|
140
|
+
|
|
141
|
+
const oldMessage = error.message
|
|
142
|
+
const newError = new Sass(oldMessage, {cause: error}).addTrace(message)
|
|
143
|
+
|
|
144
|
+
return newError
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Factory method to create or enhance Sass instances.
|
|
149
|
+
* If error parameter is provided, enhances existing Sass or wraps
|
|
150
|
+
* other errors. Otherwise creates a new Sass instance.
|
|
151
|
+
*
|
|
152
|
+
* @param {string} message - The error message
|
|
153
|
+
* @param {Error|Sass} [error] - Optional existing error to wrap or enhance
|
|
154
|
+
* @returns {Sass} New or enhanced Sass instance
|
|
155
|
+
*/
|
|
156
|
+
static new(message, error) {
|
|
157
|
+
if(error) {
|
|
158
|
+
return error instanceof Sass
|
|
159
|
+
? error.addTrace(message)
|
|
160
|
+
: Sass.from(error, message)
|
|
161
|
+
} else {
|
|
162
|
+
return new Sass(message)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|