@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
package/src/Session.js
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import chokidar from "chokidar"
|
|
2
|
+
|
|
3
|
+
import Command from "./Command.js"
|
|
4
|
+
import Sass from "./Sass.js"
|
|
5
|
+
import File from "./File.js"
|
|
6
|
+
import Term from "./Term.js"
|
|
7
|
+
import Theme from "./Theme.js"
|
|
8
|
+
import Util from "./Util.js"
|
|
9
|
+
|
|
10
|
+
export default class Session {
|
|
11
|
+
#theme = null
|
|
12
|
+
#command = null
|
|
13
|
+
#options = null
|
|
14
|
+
#watcher = null
|
|
15
|
+
#history = []
|
|
16
|
+
#stats = Object.seal({builds: 0, failures: 0})
|
|
17
|
+
#building = false
|
|
18
|
+
|
|
19
|
+
get theme() {
|
|
20
|
+
return this.#theme
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Creates a new Session instance for managing theme compilation lifecycle.
|
|
25
|
+
* Sessions provide persistent state across rebuilds, error tracking, and
|
|
26
|
+
* individual theme management within the build system.
|
|
27
|
+
*
|
|
28
|
+
* @param {Command} command - The parent build command instance
|
|
29
|
+
* @param {Theme} theme - The theme instance to manage
|
|
30
|
+
* @param {object} options - Build configuration options
|
|
31
|
+
* @param {boolean} [options.watch] - Whether to enable file watching
|
|
32
|
+
* @param {boolean} [options.nerd] - Whether to show verbose output
|
|
33
|
+
* @param {boolean} [options.dryRun] - Whether to skip file writes
|
|
34
|
+
*/
|
|
35
|
+
constructor(command, theme, options) {
|
|
36
|
+
this.#command = command
|
|
37
|
+
this.#theme = theme
|
|
38
|
+
this.#options = options
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async run() {
|
|
42
|
+
if(this.#options.watch) {
|
|
43
|
+
this.#command.emitter.on("closeSession", async() =>
|
|
44
|
+
await this.#handleCloseSession())
|
|
45
|
+
this.#command.emitter.on("rebuild", async() =>
|
|
46
|
+
await this.#handleRebuild())
|
|
47
|
+
this.#command.emitter.on("resetWatcher", async() =>
|
|
48
|
+
await this.#resetWatcher())
|
|
49
|
+
|
|
50
|
+
this.#command.emitter.on("recordBuildStart", arg =>
|
|
51
|
+
this.#recordBuildStart(arg))
|
|
52
|
+
this.#command.emitter.on("recordBuildFail", arg =>
|
|
53
|
+
this.#recordBuildFail(arg))
|
|
54
|
+
|
|
55
|
+
await this.#resetWatcher()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.#building = true
|
|
59
|
+
await this.#command.asyncEmit("building")
|
|
60
|
+
this.#command.asyncEmit("recordBuildStart", this.#theme)
|
|
61
|
+
await this.#buildPipeline()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Runs the build pipeline for a theme: load, build, and write.
|
|
66
|
+
*
|
|
67
|
+
* @param {boolean} [forceWrite] - Forces a write of the theme, used by rebuild option (defaults to false)
|
|
68
|
+
* @returns {Promise<void>} Nuttin', honey.
|
|
69
|
+
*/
|
|
70
|
+
async #buildPipeline(forceWrite=false) {
|
|
71
|
+
if(!this.#building)
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
this.#theme.reset()
|
|
75
|
+
|
|
76
|
+
const buildStart = Date.now()
|
|
77
|
+
let loadCost, buildCost, writeCost
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
/**
|
|
81
|
+
* ****************************************************************
|
|
82
|
+
* Have the theme load itself.
|
|
83
|
+
* ****************************************************************
|
|
84
|
+
*/
|
|
85
|
+
|
|
86
|
+
loadCost = (await Util.time(() => this.#theme.load())).cost
|
|
87
|
+
const bytes = await File.fileSize(this.#theme.sourceFile)
|
|
88
|
+
Term.status([
|
|
89
|
+
["success", Util.rightAlignText(`${loadCost.toLocaleString()}ms`, 10), ["[","]"]],
|
|
90
|
+
`${this.#theme.name} loaded`,
|
|
91
|
+
["info", `${bytes} bytes`, ["[","]"]]
|
|
92
|
+
], this.#options)
|
|
93
|
+
/**
|
|
94
|
+
* ****************************************************************
|
|
95
|
+
* Have the theme build itself.
|
|
96
|
+
* ****************************************************************
|
|
97
|
+
*/
|
|
98
|
+
|
|
99
|
+
buildCost = (await Util.time(() => this.#theme.build())).cost
|
|
100
|
+
|
|
101
|
+
const compileResult =
|
|
102
|
+
await Promise.allSettled(this.#theme.dependencies.map(async dep => {
|
|
103
|
+
|
|
104
|
+
return await (async fileObject => {
|
|
105
|
+
const fileName = File.relativeOrAbsolutePath(this.#command.cwd, fileObject)
|
|
106
|
+
const fileSize = await File.fileSize(fileObject)
|
|
107
|
+
return [fileName, fileSize]
|
|
108
|
+
})(dep)
|
|
109
|
+
|
|
110
|
+
}))
|
|
111
|
+
|
|
112
|
+
const rejected = compileResult.filter(result => result.status === "rejected")
|
|
113
|
+
if(rejected.length > 0) {
|
|
114
|
+
rejected.forEach(reject => Term.error(reject.reason))
|
|
115
|
+
throw new Error("Compilation failed")
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const dependencies = compileResult.slice(1).map(dep => dep.value)
|
|
119
|
+
const totalBytes = compileResult.reduce((acc,curr) => acc + curr.value[1], 0)
|
|
120
|
+
|
|
121
|
+
Term.status([
|
|
122
|
+
["success", Util.rightAlignText(`${buildCost.toLocaleString()}ms`, 10), ["[","]"]],
|
|
123
|
+
`${this.#theme.name} compiled`,
|
|
124
|
+
["success", `${compileResult[0].value[1].toLocaleString()} bytes`, ["[","]"]],
|
|
125
|
+
["info", `${totalBytes.toLocaleString()} total bytes`, ["(",")"]],
|
|
126
|
+
], this.#options)
|
|
127
|
+
|
|
128
|
+
if(this.#options.nerd) {
|
|
129
|
+
dependencies.forEach(f => {
|
|
130
|
+
const [fileName,fileSize] = f
|
|
131
|
+
|
|
132
|
+
Term.status([
|
|
133
|
+
`${" ".repeat(13)}`,
|
|
134
|
+
["muted", fileName],
|
|
135
|
+
["muted", `${fileSize.toLocaleString()} bytes`, ["{","}"]]
|
|
136
|
+
], this.#options)
|
|
137
|
+
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* ****************************************************************
|
|
143
|
+
* Lastly. Tom Riddle that shit into the IO! I would say just "O",
|
|
144
|
+
* but that wouldn't be very inclusive language. *I see you!*
|
|
145
|
+
* ****************************************************************
|
|
146
|
+
*/
|
|
147
|
+
|
|
148
|
+
const writeResult = await Util.time(() => this.#theme.write(forceWrite))
|
|
149
|
+
writeCost = writeResult.cost
|
|
150
|
+
const result = writeResult.result
|
|
151
|
+
const {
|
|
152
|
+
status: writeStatus,
|
|
153
|
+
file: outputFile,
|
|
154
|
+
bytes: writeBytes
|
|
155
|
+
} = result
|
|
156
|
+
const outputFilename = File.relativeOrAbsolutePath(this.#command.cwd, outputFile)
|
|
157
|
+
const status = [
|
|
158
|
+
["success", Util.rightAlignText(`${writeCost.toLocaleString()}ms`, 10), ["[","]"]],
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
if(writeStatus === "written") {
|
|
162
|
+
status.push(
|
|
163
|
+
`${outputFilename} written`,
|
|
164
|
+
["success", `${writeBytes.toLocaleString()} bytes`, ["[","]"]]
|
|
165
|
+
)
|
|
166
|
+
} else {
|
|
167
|
+
status.push(
|
|
168
|
+
`${outputFilename}`,
|
|
169
|
+
["warn", writeStatus.toLocaleUpperCase(), ["[","]"]]
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
Term.status(status, this.#options)
|
|
174
|
+
|
|
175
|
+
// Track successful build
|
|
176
|
+
this.#command.asyncEmit("recordBuildSucceed", this.#theme)
|
|
177
|
+
this.#history.push({
|
|
178
|
+
timestamp: buildStart,
|
|
179
|
+
loadTime: loadCost,
|
|
180
|
+
buildTime: buildCost,
|
|
181
|
+
writeTime: writeCost,
|
|
182
|
+
success: true
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
} catch(error) {
|
|
186
|
+
// Track failed build
|
|
187
|
+
await this.#command.asyncEmit("recordBuildFail", this.#theme)
|
|
188
|
+
this.#history.push({
|
|
189
|
+
timestamp: buildStart,
|
|
190
|
+
loadTime: loadCost || 0,
|
|
191
|
+
buildTime: buildCost || 0,
|
|
192
|
+
writeTime: writeCost || 0,
|
|
193
|
+
success: false,
|
|
194
|
+
error: error.message
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
if(error instanceof Sass)
|
|
198
|
+
error.report(this.#options.nerd)
|
|
199
|
+
} finally {
|
|
200
|
+
this.#building = false
|
|
201
|
+
this.#command.asyncEmit("finishedBuilding")
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Handles a file change event and triggers a rebuild for the theme.
|
|
207
|
+
*
|
|
208
|
+
* @param {string} changed - Path to the changed file
|
|
209
|
+
* @returns {Promise<void>}
|
|
210
|
+
*/
|
|
211
|
+
async #handleFileChange(changed) {
|
|
212
|
+
try {
|
|
213
|
+
if(this.#building)
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
this.#building = true
|
|
217
|
+
this.#command.asyncEmit("building")
|
|
218
|
+
|
|
219
|
+
const changedFile = this.#theme.dependencies.find(dep => dep.path === changed)
|
|
220
|
+
|
|
221
|
+
if(!changedFile)
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
const fileName = File.relativeOrAbsolutePath(this.#command.cwd, changedFile)
|
|
225
|
+
|
|
226
|
+
const message = [
|
|
227
|
+
["info", "REBUILDING", ["[","]"]],
|
|
228
|
+
this.#theme.name,
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
if(this.#options.nerd)
|
|
232
|
+
message.push(["muted", fileName])
|
|
233
|
+
|
|
234
|
+
Term.status(message)
|
|
235
|
+
|
|
236
|
+
await this.#resetWatcher()
|
|
237
|
+
await this.#buildPipeline()
|
|
238
|
+
} finally {
|
|
239
|
+
this.#building = false
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Displays a formatted summary of the session's build statistics and performance.
|
|
245
|
+
* Shows total builds, success/failure counts, success rate percentage, and timing
|
|
246
|
+
* information from the most recent build. Used during session cleanup to provide
|
|
247
|
+
* final statistics to the user.
|
|
248
|
+
*
|
|
249
|
+
* @returns {void}
|
|
250
|
+
*/
|
|
251
|
+
showSummary() {
|
|
252
|
+
const {builds, failures} = this.#stats
|
|
253
|
+
const successes = builds-failures
|
|
254
|
+
const successRate = builds > 0 ? ((successes / builds) * 100).toFixed(1) : "0.0"
|
|
255
|
+
|
|
256
|
+
Term.info()
|
|
257
|
+
|
|
258
|
+
Term.status([
|
|
259
|
+
[builds > 0 ? "success" : "error", "SESSION SUMMARY"],
|
|
260
|
+
[builds > 0 ? "info" : "error", this.#theme.name, ["[", "]"]]
|
|
261
|
+
], this.#options)
|
|
262
|
+
|
|
263
|
+
Term.status([
|
|
264
|
+
[builds > 0 ? "success" : "error", "Builds", ["[", "]"]],
|
|
265
|
+
builds.toLocaleString(),
|
|
266
|
+
[successes > 0 ? "success" : "error", "Successes", ["(", ")"]],
|
|
267
|
+
successes.toLocaleString(),
|
|
268
|
+
[failures > 0 ? "error" : "error", "Failures", ["(", ")"]],
|
|
269
|
+
failures.toLocaleString(),
|
|
270
|
+
[builds > 0 ? "info" : "error", `${successRate}%`, ["(", ")"]]
|
|
271
|
+
], this.#options)
|
|
272
|
+
|
|
273
|
+
if(this.#history.length > 0) {
|
|
274
|
+
const lastBuild = this.#history[this.#history.length - 1]
|
|
275
|
+
const totalTime = lastBuild.loadTime + lastBuild.buildTime + lastBuild.writeTime
|
|
276
|
+
|
|
277
|
+
Term.status([
|
|
278
|
+
[builds > 0 ? "info" : "muted", "Last Build", ["[", "]"]],
|
|
279
|
+
[builds > 0 ? "success" : "muted", `${totalTime.toLocaleString()}ms total`, ["(", ")"]]
|
|
280
|
+
], this.#options)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Handles a rebuild event, resetting and rebuilding all watched themes.
|
|
286
|
+
*
|
|
287
|
+
* @returns {Promise<void>}
|
|
288
|
+
*/
|
|
289
|
+
async #handleRebuild() {
|
|
290
|
+
if(this.#building)
|
|
291
|
+
return
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
this.#command.asyncEmit("recordBuildStart", this.#theme)
|
|
295
|
+
this.#building = true
|
|
296
|
+
await this.#resetWatcher()
|
|
297
|
+
this.#command.asyncEmit("building")
|
|
298
|
+
await this.#buildPipeline(true)
|
|
299
|
+
} catch(error) {
|
|
300
|
+
await this.#command.asyncEmit("recordBuildFail", this.#theme)
|
|
301
|
+
throw Sass.new("Handling rebuild request.", error)
|
|
302
|
+
} finally {
|
|
303
|
+
this.#building = false
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Resets the file watcher for a theme, setting up new dependencies.
|
|
309
|
+
*
|
|
310
|
+
* @returns {Promise<void>}
|
|
311
|
+
*/
|
|
312
|
+
async #resetWatcher() {
|
|
313
|
+
if(this.#watcher)
|
|
314
|
+
await this.#watcher.close()
|
|
315
|
+
|
|
316
|
+
const dependencies = this.#theme.dependencies.map(d => d.path)
|
|
317
|
+
this.#watcher = chokidar.watch(dependencies, {
|
|
318
|
+
// Prevent watching own output files
|
|
319
|
+
ignored: [this.#theme.outputFileName],
|
|
320
|
+
// Add some stability options
|
|
321
|
+
awaitWriteFinish: {
|
|
322
|
+
stabilityThreshold: 100,
|
|
323
|
+
pollInterval: 50
|
|
324
|
+
}
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
this.#watcher.on("change", this.#handleFileChange.bind(this))
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Handles quitting the watch mode and cleans up watchers.
|
|
332
|
+
*
|
|
333
|
+
* @returns {Promise<void>}
|
|
334
|
+
*/
|
|
335
|
+
async #handleCloseSession() {
|
|
336
|
+
this.showSummary()
|
|
337
|
+
|
|
338
|
+
if(this.#watcher) {
|
|
339
|
+
try {
|
|
340
|
+
await this.#watcher.close()
|
|
341
|
+
} catch(_) {
|
|
342
|
+
void _
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
#recordBuildStart(theme) {
|
|
348
|
+
if(theme !== this.#theme)
|
|
349
|
+
return
|
|
350
|
+
|
|
351
|
+
this.#stats.builds++
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
#recordBuildFail(theme) {
|
|
355
|
+
if(theme !== this.#theme)
|
|
356
|
+
return
|
|
357
|
+
|
|
358
|
+
this.#stats.failures++
|
|
359
|
+
}
|
|
360
|
+
}
|
package/src/Term.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import c from "@gesslar/colours"
|
|
2
|
+
// import colorSupport from "color-support"
|
|
3
|
+
import console from "node:console"
|
|
4
|
+
import process from "node:process"
|
|
5
|
+
|
|
6
|
+
import Sass from "./Sass.js"
|
|
7
|
+
|
|
8
|
+
export default class Term {
|
|
9
|
+
/**
|
|
10
|
+
* Log an informational message.
|
|
11
|
+
*
|
|
12
|
+
* @param {...any} arg - Values to log.
|
|
13
|
+
*/
|
|
14
|
+
static log(...arg) {
|
|
15
|
+
console.log(...arg)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Log an informational message.
|
|
20
|
+
*
|
|
21
|
+
* @param {...any} arg - Values to log.
|
|
22
|
+
*/
|
|
23
|
+
static info(...arg) {
|
|
24
|
+
console.info(...arg)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Log a warning message.
|
|
29
|
+
*
|
|
30
|
+
* @param {any} msg - Warning text / object.
|
|
31
|
+
*/
|
|
32
|
+
static warn(...msg) {
|
|
33
|
+
console.warn(...msg)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Log an error message (plus optional details).
|
|
38
|
+
*
|
|
39
|
+
* @param {...any} arg - Values to log.
|
|
40
|
+
*/
|
|
41
|
+
static error(...arg) {
|
|
42
|
+
console.error(...arg)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Log a debug message (no-op unless console.debug provided/visible by env).
|
|
47
|
+
*
|
|
48
|
+
* @param {...any} arg - Values to log.
|
|
49
|
+
*/
|
|
50
|
+
static debug(...arg) {
|
|
51
|
+
console.debug(...arg)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Emit a status line to the terminal.
|
|
56
|
+
*
|
|
57
|
+
* Accepts either a plain string or an array of message segments (see
|
|
58
|
+
* `terminalMessage()` for formatting options). If `silent` is true, output
|
|
59
|
+
* is suppressed.
|
|
60
|
+
*
|
|
61
|
+
* This is a convenient shortcut for logging status updates, with optional
|
|
62
|
+
* formatting and easy suppression.
|
|
63
|
+
*
|
|
64
|
+
* @param {string | Array<string | [string, string]>} args - Message or segments.
|
|
65
|
+
* @param {object} [options] - Behaviour flags.
|
|
66
|
+
* @param {boolean} options.silent - When true, suppress output.
|
|
67
|
+
* @returns {void}
|
|
68
|
+
*/
|
|
69
|
+
static status(args, {silent=false} = {}) {
|
|
70
|
+
if(silent)
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
return Term.info(Term.terminalMessage(args))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Constructs a formatted status line.
|
|
78
|
+
*
|
|
79
|
+
* Input forms:
|
|
80
|
+
* - string: printed as-is
|
|
81
|
+
* - array: each element is either:
|
|
82
|
+
* - a plain string (emitted unchanged), or
|
|
83
|
+
* - a tuple: [level, text] where `level` maps to an ansiColors alias
|
|
84
|
+
* (e.g. success, info, warn, error, modified).
|
|
85
|
+
* - a tuple: [level, text, [openBracket,closeBracket]] where `level` maps to an ansiColors alias
|
|
86
|
+
* (e.g. success, info, warn, error, modified). These are rendered as
|
|
87
|
+
* colourised bracketed segments: [TEXT].
|
|
88
|
+
*
|
|
89
|
+
* The function performs a shallow validation: tuple elements must both be
|
|
90
|
+
* strings; otherwise a TypeError is thrown. Nested arrays beyond depth 1 are
|
|
91
|
+
* not supported.
|
|
92
|
+
*
|
|
93
|
+
* Recursion: array input is normalised into a single string then re-dispatched
|
|
94
|
+
* through `status` to leverage the string branch (keeps logic DRY).
|
|
95
|
+
*
|
|
96
|
+
* @param {string | Array<string, string> | Array<string, string, string>} argList - Message spec.
|
|
97
|
+
* @returns {void}
|
|
98
|
+
*/
|
|
99
|
+
static terminalMessage(argList) {
|
|
100
|
+
if(typeof argList === "string")
|
|
101
|
+
return argList
|
|
102
|
+
|
|
103
|
+
if(Array.isArray(argList)) {
|
|
104
|
+
const message = argList
|
|
105
|
+
.map(args => {
|
|
106
|
+
// Bracketed
|
|
107
|
+
if(Array.isArray(args))
|
|
108
|
+
|
|
109
|
+
if(args.length === 3 && Array.isArray(args[2]))
|
|
110
|
+
return Term.terminalBracket(args)
|
|
111
|
+
|
|
112
|
+
else
|
|
113
|
+
return Term.terminalBracket([...args])
|
|
114
|
+
|
|
115
|
+
// Plain string, no decoration
|
|
116
|
+
if(typeof args === "string")
|
|
117
|
+
return args
|
|
118
|
+
})
|
|
119
|
+
.join(" ")
|
|
120
|
+
|
|
121
|
+
return Term.terminalMessage(message)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
throw Sass.new("Invalid arguments passed to terminalMessage")
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Construct a single coloured bracketed segment from a tuple specifying
|
|
129
|
+
* the style level and the text. The first element ("level") maps to an
|
|
130
|
+
* `ansiColors` alias (e.g. success, info, warn, error, modified) and is
|
|
131
|
+
* used both for the inner text colour and to locate its matching
|
|
132
|
+
* "-bracket" alias for the surrounding square brackets. The second
|
|
133
|
+
* element is the raw text to display.
|
|
134
|
+
*
|
|
135
|
+
* Input validation: every element of `parts` must be a string; otherwise
|
|
136
|
+
* an `Sass` error is thrown. (Additional elements beyond the first two are
|
|
137
|
+
* ignored – the method destructures only the first pair.)
|
|
138
|
+
*
|
|
139
|
+
* Example:
|
|
140
|
+
* terminalBracket(["success", "COMPILED"]) → "[COMPILED]" with coloured
|
|
141
|
+
* brackets + inner text (assuming colour support is available in the
|
|
142
|
+
* terminal).
|
|
143
|
+
*
|
|
144
|
+
* This method does not append trailing spaces; callers are responsible for
|
|
145
|
+
* joining multiple segments with appropriate separators.
|
|
146
|
+
*
|
|
147
|
+
* @param {string[]} parts - Tuple: [level, text]. Additional entries ignored.
|
|
148
|
+
* @returns {string} Colourised bracketed segment (e.g. "[TEXT]").
|
|
149
|
+
* @throws {Sass} If any element of `parts` is not a string.
|
|
150
|
+
*/
|
|
151
|
+
static terminalBracket([level, text, brackets=["",""]]) {
|
|
152
|
+
if(!(typeof level === "string" && typeof text === "string"))
|
|
153
|
+
throw Sass.new("Each element must be a string.")
|
|
154
|
+
|
|
155
|
+
return "" +
|
|
156
|
+
c`{${level}-bracket}${brackets[0]}{/}`
|
|
157
|
+
+ c`{${level}}${text}{/}`
|
|
158
|
+
+ c`{${level}-bracket}${brackets[1]}{/}`
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
static async resetTerminal() {
|
|
162
|
+
await Term.directWrite("\x1b[?25h")
|
|
163
|
+
process.stdin.setRawMode(false)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
static async clearLines(num) {
|
|
167
|
+
await Term.directWrite(`${"\r\x1b[2K\x1b[1A".repeat(num)}`)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
static directWrite(output) {
|
|
171
|
+
return new Promise(resolve => {
|
|
172
|
+
process.stdout.write(output, () => resolve())
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
}
|