@gesslar/toolkit 0.2.7 → 0.2.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gesslar/toolkit",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "Get in, bitches, we're going toolkitting.",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
@@ -49,6 +49,7 @@
49
49
  "license": "Unlicense",
50
50
  "homepage": "https://github.com/gesslar/toolkit#readme",
51
51
  "dependencies": {
52
+ "@gesslar/colours": "^0.0.1",
52
53
  "globby": "^15.0.0",
53
54
  "json5": "^2.2.3",
54
55
  "yaml": "^2.8.1"
package/src/index.js CHANGED
@@ -9,6 +9,7 @@ export {default as Collection} from "./lib/Collection.js"
9
9
  export {default as Data} from "./lib/Data.js"
10
10
  export {default as Glog} from "./lib/Glog.js"
11
11
  export {default as Sass} from "./lib/Sass.js"
12
+ export {default as Tantrum} from "./lib/Tantrum.js"
12
13
  export {default as Term} from "./lib/Term.js"
13
14
  export {default as Type} from "./lib/TypeSpec.js"
14
15
  export {default as Util} from "./lib/Util.js"
@@ -0,0 +1,144 @@
1
+ import Valid from "./Valid.js"
2
+
3
+ /** @typedef {import("./ActionRunner.js").default} ActionRunner */
4
+
5
+ /**
6
+ * Activity bit flags recognised by {@link ActionBuilder#act}. The flag decides
7
+ * how results are accumulated for each activity.
8
+ *
9
+ * @readonly
10
+ * @enum {number}
11
+ */
12
+ export const ACTIVITY = Object.freeze({
13
+ ONCE: 1<<1,
14
+ MANY: 1<<2,
15
+ PARALLEL: 1<<3,
16
+ })
17
+
18
+ /**
19
+ * Fluent builder for describing how an action should process the context that
20
+ * flows through the {@link ActionRunner}. Consumers register named activities,
21
+ * optional hook pairs, and nested parallel pipelines before handing the
22
+ * builder back to the runner for execution.
23
+ *
24
+ * Typical usage:
25
+ *
26
+ * ```js
27
+ * const pipeline = new ActionBuilder(myAction)
28
+ * .act("prepare", ACTIVITY.ONCE, ctx => ctx.initialise())
29
+ * .parallel(parallel => parallel
30
+ * .act("step", ACTIVITY.MANY, ctx => ctx.consume())
31
+ * )
32
+ * .act("finalise", ACTIVITY.ONCE, ctx => ctx.complete())
33
+ * .build()
34
+ * ```
35
+ *
36
+ * @class ActionBuilder
37
+ */
38
+ export default class ActionBuilder {
39
+ #action = null
40
+ #activities = new Map([])
41
+
42
+ /**
43
+ * Creates a new ActionBuilder instance with the provided action callback.
44
+ *
45
+ * @param {(ctx: object) => void} action Base action invoked by the runner when a block
46
+ * satisfies the configured structure.
47
+ */
48
+ constructor(action) {
49
+ this.#action = action
50
+ }
51
+
52
+ /**
53
+ * Returns the underlying action that will receive the extracted context.
54
+ *
55
+ * @returns {(ctx: object) => void} The action callback function that processes the extracted context.
56
+ */
57
+ get action() {
58
+ return this.#action
59
+ }
60
+
61
+ /**
62
+ * Returns the registered activities keyed by their name.
63
+ *
64
+ * @returns {Map<string | symbol, {op: (context: object) => unknown, kind: number, hooks: {before: ((context: object) => void)|null, after: ((context: object) => void)|null}}>} Map of registered activities and their metadata.
65
+ */
66
+ get activities() {
67
+ return this.#activities
68
+ }
69
+
70
+ /**
71
+ * Registers a named activity that will run for each matching block.
72
+ *
73
+ * @param {string} name Unique activity identifier.
74
+ * @param {number} kind Activity accumulation strategy (see {@link ACTIVITY}).
75
+ * @param {(context: object) => unknown} op Work function executed with the runner context.
76
+ * @param {{before?: (context: object) => void, after?: (context: object) => void}} [hooks] Optional hooks to run before or after the activity operation.
77
+ * @returns {ActionBuilder} Builder instance for chaining.
78
+ */
79
+ act(name, kind, op, hooks={}) {
80
+ this.#validActivityKind(kind)
81
+ this.#dupeActivityCheck(name)
82
+
83
+ hooks = this.#normalizeHooks(hooks)
84
+
85
+ this.#activities.set(name, {op, kind, hooks})
86
+
87
+ return this
88
+ }
89
+
90
+ #normalizeHooks({before=null, after=null}) {
91
+ return {before, after}
92
+ }
93
+
94
+ /**
95
+ * Defines a nested pipeline that will run with the {@link ACTIVITY} flag PARALLEL.
96
+ *
97
+ * The callback receives a fresh {@link ActionBuilder} scoped to the current
98
+ * action. The callback must return the configured builder so the runner can
99
+ * execute the nested pipeline.
100
+ *
101
+ * @param {(builder: ActionBuilder) => ActionBuilder} func Callback configuring a nested builder.
102
+ * @returns {ActionBuilder} Builder instance for chaining.
103
+ */
104
+ parallel(func) {
105
+ const activityName = Symbol(performance.now())
106
+
107
+ this.#activities.set(activityName, {
108
+ op: func.call(this.action, new ActionBuilder(this.action)),
109
+ kind: ACTIVITY.PARALLEL
110
+ })
111
+
112
+ return this
113
+ }
114
+
115
+ #validActivityKind(kind) {
116
+ Valid.assert(
117
+ Object.values(ACTIVITY).includes(kind),
118
+ "Invalid activity kind."
119
+ )
120
+ }
121
+
122
+ /**
123
+ * Validates that an activity name has not been reused.
124
+ *
125
+ * @private
126
+ * @param {string|symbol} name Activity identifier.
127
+ */
128
+ #dupeActivityCheck(name) {
129
+ Valid.assert(
130
+ !this.#activities.has(name),
131
+ `Activity '${String(name)}' has already been registered.`
132
+ )
133
+ }
134
+
135
+ /**
136
+ * Finalises the builder and returns a payload that can be consumed by the
137
+ * runner.
138
+ *
139
+ * @returns {{action: (context: object) => unknown, build: ActionBuilder}} Payload consumed by the {@link ActionRunner} constructor.
140
+ */
141
+ build() {
142
+ return {action: this.#action, build: this}
143
+ }
144
+ }
@@ -0,0 +1,109 @@
1
+ import ActionBuilder, {ACTIVITY} from "./ActionBuilder.js"
2
+ import Data from "./Data.js"
3
+ import Piper from "./Piper.js"
4
+ import Sass from "./Sass.js"
5
+ import Glog from "./Glog.js"
6
+
7
+ /**
8
+ * Orchestrates execution of {@link ActionBuilder}-produced pipelines.
9
+ *
10
+ * Activities run in insertion order, with support for once-off work, repeated
11
+ * loops, and nested parallel pipelines. Each activity receives a mutable
12
+ * context object under `result.value` that can be replaced or enriched.
13
+ */
14
+ export default class ActionRunner {
15
+ #action = null
16
+ #build = null
17
+ #logger = null
18
+
19
+ constructor({action, build, logger}) {
20
+ this.#action = action
21
+ this.#build = build
22
+ this.#logger = logger ?? Glog.newDebug()
23
+ }
24
+
25
+ /**
26
+ * Executes the configured action pipeline.
27
+ *
28
+ * @param {unknown} content Seed value passed to the first activity.
29
+ * @returns {Promise<unknown>} Final value produced by the pipeline, or null when a parallel stage reports failures.
30
+ * @throws {Sass} When no activities are registered or required parallel builders are missing.
31
+ */
32
+ async run(content) {
33
+ const AR = ActionRunner
34
+ const result = {value: content}
35
+ const action = this.#action
36
+ const activities = this.#build.activities
37
+
38
+ if(!activities.size)
39
+ throw Sass.new("No activities defined in action.")
40
+
41
+ for(const [_,activity] of activities) {
42
+ const {op} = activity
43
+
44
+ if(activity.kind === ACTIVITY.ONCE) {
45
+
46
+ if(Data.typeOf(activity.hooks?.before) === "Function")
47
+ await activity.hooks.before.call(action, result)
48
+
49
+ const activityResult = await op.call(action, result)
50
+
51
+ if(!activityResult)
52
+ break
53
+
54
+ if(Data.typeOf(activity.hooks?.after) === "Function")
55
+ await activity.hooks.after.call(action, result)
56
+
57
+ } else if(activity.kind == ACTIVITY.MANY) {
58
+ for(;;) {
59
+
60
+ if(Data.typeOf(activity.hooks?.before) === "Function")
61
+ await activity.hooks.before.call(action, result)
62
+
63
+ const activityResult = await op.call(action, result)
64
+
65
+ if(!activityResult)
66
+ break
67
+
68
+ if(Data.typeOf(activity.hooks?.after) === "Function")
69
+ await activity.hooks.after.call(action, result)
70
+ }
71
+ } else if(activity.kind === ACTIVITY.PARALLEL) {
72
+ if(op === undefined)
73
+ throw Sass.new("Missing action builder. Did you return the builder?")
74
+
75
+ if(!op)
76
+ throw Sass.new("Okay, cheeky monkey, you need to return the builder for this to work.")
77
+
78
+ const piper = new Piper({logger: this.#logger})
79
+ .addStep(c => new AR(op.build()).run(c))
80
+
81
+ result.value = await piper.pipe()
82
+ Glog(result)
83
+ throw Sass.new("Nope")
84
+
85
+ // // wheeeeeeeeeeeeee! ZOOMZOOM!
86
+ // const settled = await Util.settleAll(
87
+ // result.value.map()
88
+ // )
89
+
90
+ // const rejected = settled
91
+ // .filter(r => r.status === "rejected")
92
+ // .map(r => {
93
+ // return r.reason instanceof Sass
94
+ // ? r.reason
95
+ // : Sass.new("Running structured parsing.", r.reason)
96
+ // })
97
+ // .map(r => r.report(true))
98
+
99
+ // if(rejected.length)
100
+ // return null
101
+
102
+ // result.value = settled.map(s => s.value)
103
+ // .sort((a,b) => a.index-b.index)
104
+ }
105
+ }
106
+
107
+ return result.value
108
+ }
109
+ }
@@ -353,12 +353,12 @@ export default class FileObject extends FS {
353
353
  async read(encoding="utf8") {
354
354
  const filePath = this.path
355
355
 
356
- if(!(await this.exists))
357
- throw Sass.new(`No such file '${filePath}'`)
358
-
359
356
  if(!filePath)
360
357
  throw Sass.new("No absolute path in file map")
361
358
 
359
+ if(!(await this.exists))
360
+ throw Sass.new(`No such file '${filePath}'`)
361
+
362
362
  return await fs.readFile(filePath, encoding)
363
363
  }
364
364
 
@@ -409,4 +409,21 @@ export default class FileObject extends FS {
409
409
 
410
410
  throw Sass.new(`Content is neither valid JSON5 nor valid YAML:\n'${this.path}'`)
411
411
  }
412
+
413
+ /**
414
+ * Loads a file as a module and returns it.
415
+ *
416
+ * @returns {Promise<object>} The file contents as a module.
417
+ */
418
+ async import() {
419
+ const fileUri = this.uri
420
+
421
+ if(!fileUri)
422
+ throw Sass.new("No URI in file map")
423
+
424
+ if(!(await this.exists))
425
+ throw Sass.new(`No such file '${fileUri}'`)
426
+
427
+ return await import(fileUri)
428
+ }
412
429
  }
package/src/lib/Glog.js CHANGED
@@ -1,136 +1,374 @@
1
1
  import Data from "./Data.js"
2
- import console from "node:console"
2
+ import Util from "./Util.js"
3
+ import c from "@gesslar/colours"
4
+ import Term from "./Term.js"
5
+ // ErrorStackParser will be dynamically imported when needed
3
6
 
4
7
  /**
5
- * Global logging utility with configurable log levels and prefixes.
6
- * Provides a flexible logging system that can be used as both a class and
7
- * a callable function, with support for log level filtering and custom
8
- * prefixes for better log organization.
8
+ * Enhanced Global logging utility that combines simple logging with advanced Logger features.
9
9
  *
10
- * The Glog class uses a proxy to enable both class-style and function-style
11
- * usage patterns, making it convenient for different coding preferences.
12
- *
13
- * @example
14
- * // Set up logging configuration
15
- * Glog.setLogLevel(3).setLogPrefix('[MyApp]')
16
- *
17
- * // Log messages with different levels
18
- * Glog(0, 'Critical error') // Always shown
19
- * Glog(2, 'Debug info') // Shown if logLevel >= 2
20
- * Glog('Simple message') // Level 0 by default
10
+ * Can be used in multiple ways:
11
+ * 1. Simple function call: Glog(data)
12
+ * 2. With levels: Glog(2, "debug message")
13
+ * 3. Configured instance: new Glog(options)
14
+ * 4. Fluent setup: Glog.create().withName("App").withColors()
15
+ * 5. Traditional logger: logger.debug("message", level)
21
16
  */
17
+
18
+ // Enhanced color system using @gesslar/colours
19
+ export const loggerColours = {
20
+ debug: [
21
+ "{F019}", // Debug level 0: Dark blue
22
+ "{F027}", // Debug level 1: Medium blue
23
+ "{F033}", // Debug level 2: Light blue
24
+ "{F039}", // Debug level 3: Teal
25
+ "{F044}", // Debug level 4: Blue-tinted cyan
26
+ ],
27
+ info: "{F036}", // Medium Spring Green
28
+ warn: "{F214}", // Orange1
29
+ error: "{F196}", // Red1
30
+ reset: "{/}", // Reset
31
+ }
32
+
33
+ // Set up convenient aliases for common log colors
34
+ c.alias.set("debug", "{F033}")
35
+ c.alias.set("info", "{F036}")
36
+ c.alias.set("warn", "{F214}")
37
+ c.alias.set("error", "{F196}")
38
+ c.alias.set("success", "{F046}")
39
+ c.alias.set("muted", "{F244}")
40
+ c.alias.set("bold", "{<B}")
41
+ c.alias.set("dim", "{<D}")
42
+
22
43
  class Glog {
23
- /** @type {number} Current log level threshold (0-5) */
44
+ // Static properties (for global usage)
24
45
  static logLevel = 0
25
- /** @type {string} Prefix to prepend to all log messages */
26
46
  static logPrefix = ""
47
+ static colors = null
48
+ static stackTrace = false
49
+ static name = ""
50
+
51
+ // Instance properties (for configured loggers)
52
+ #logLevel = 0
53
+ #logPrefix = ""
54
+ #colors = null
55
+ #stackTrace = false
56
+ #name = ""
57
+ #vscodeError = null
58
+ #vscodeWarn = null
59
+ #vscodeInfo = null
60
+
61
+ constructor(options = {}) {
62
+ this.setOptions(options)
63
+
64
+ // VSCode integration if specified
65
+ if(options.env === "extension") {
66
+ try {
67
+ const vscode = require("vscode")
68
+
69
+ this.#vscodeError = vscode.window.showErrorMessage
70
+ this.#vscodeWarn = vscode.window.showWarningMessage
71
+ this.#vscodeInfo = vscode.window.showInformationMessage
72
+ } catch {
73
+ // VSCode not available, ignore
74
+ }
75
+ }
76
+ }
77
+
78
+ // === CONFIGURATION METHODS ===
79
+
80
+ setOptions(options) {
81
+ this.#name = options.name ?? this.#name
82
+ this.#logLevel = options.debugLevel ?? options.logLevel ?? this.#logLevel
83
+ this.#logPrefix = options.prefix ?? this.#logPrefix
84
+ this.#colors = options.colors ?? this.#colors
85
+ this.#stackTrace = options.stackTrace ?? this.#stackTrace
86
+
87
+ return this
88
+ }
89
+
90
+ // === STATIC CONFIGURATION (for global usage) ===
27
91
 
28
- /**
29
- * Sets the log prefix for all subsequent log messages.
30
- * The prefix helps identify the source of log messages in complex
31
- * applications with multiple components.
32
- *
33
- * @param {string} prefix - The prefix string to prepend to log messages
34
- * @returns {typeof Glog} Returns the Glog class for method chaining
35
- * @example
36
- * Glog.setLogPrefix('[Database]')
37
- * Glog('Connection established') // Output: [Database] Connection established
38
- */
39
92
  static setLogPrefix(prefix) {
40
93
  this.logPrefix = prefix
41
94
 
42
95
  return this
43
96
  }
44
97
 
45
- /**
46
- * Sets the minimum log level for messages to be displayed.
47
- * Messages with a level higher than this threshold will be filtered out.
48
- * Log levels range from 0 (critical) to 5 (verbose debug).
49
- *
50
- * @param {number} level - The minimum log level (0-5, clamped to range)
51
- * @returns {typeof Glog} Returns the Glog class for method chaining
52
- * @example
53
- * Glog.setLogLevel(2) // Only show messages with level 0, 1, or 2
54
- * Glog(1, 'Important') // Shown
55
- * Glog(3, 'Verbose') // Hidden
56
- */
57
98
  static setLogLevel(level) {
58
99
  this.logLevel = Data.clamp(level, 0, 5)
59
100
 
60
101
  return this
61
102
  }
62
103
 
63
- /**
64
- * Internal logging method that handles message formatting and level
65
- * filtering.
66
- *
67
- * Parses arguments to determine log level and message content, then outputs
68
- * the message if it meets the current log level threshold.
69
- *
70
- * @private
71
- * @param {...unknown} args - Variable arguments: either (level, ...messages) or (...messages)
72
- * @returns {void}
73
- */
74
- static #log(...args) {
104
+ static withName(name) {
105
+ this.name = name
106
+
107
+ return this
108
+ }
109
+
110
+ static withColors(colors = loggerColours) {
111
+ this.colors = colors
112
+
113
+ return this
114
+ }
115
+
116
+ static withStackTrace(enabled = true) {
117
+ this.stackTrace = enabled
118
+
119
+ return this
120
+ }
121
+
122
+ // === FLUENT INSTANCE CREATION ===
123
+
124
+ static create(options = {}) {
125
+ return new Glog(options)
126
+ }
127
+
128
+ withName(name) {
129
+ this.#name = name
130
+
131
+ return this
132
+ }
133
+
134
+ withLogLevel(level) {
135
+ this.#logLevel = level
136
+
137
+ return this
138
+ }
139
+
140
+ withPrefix(prefix) {
141
+ this.#logPrefix = prefix
142
+
143
+ return this
144
+ }
145
+
146
+ withColors(colors = loggerColours) {
147
+ this.#colors = colors
148
+
149
+ return this
150
+ }
151
+
152
+ withStackTrace(enabled = true) {
153
+ this.#stackTrace = enabled
154
+
155
+ return this
156
+ }
157
+
158
+ // === UTILITY METHODS ===
159
+
160
+ get name() {
161
+ return this.#name
162
+ }
163
+
164
+ get debugLevel() {
165
+ return this.#logLevel
166
+ }
167
+
168
+ get options() {
169
+ return {
170
+ name: this.#name,
171
+ debugLevel: this.#logLevel,
172
+ prefix: this.#logPrefix,
173
+ colors: this.#colors,
174
+ stackTrace: this.#stackTrace
175
+ }
176
+ }
177
+
178
+ #compose(level, message, debugLevel = 0) {
179
+ const colors = this.#colors || Glog.colors || loggerColours
180
+ const name = this.#name || Glog.name || "Log"
181
+ const tag = Util.capitalize(level)
182
+
183
+ if(!colors) {
184
+ return `[${name}] ${tag}: ${message}`
185
+ }
186
+
187
+ if(level === "debug") {
188
+ const colorCode = colors[level][debugLevel] || colors[level][0]
189
+
190
+ return c`[${name}] ${colorCode}${tag}{/}: ${message}`
191
+ }
192
+
193
+ return c`[${name}] ${colors[level]}${tag}{/}: ${message}`
194
+ }
195
+
196
+ // Stack trace functionality - simplified for now
197
+ extractFileFunction() {
198
+ // Simple fallback - just return a basic tag
199
+ return "caller"
200
+ }
201
+
202
+ newDebug(tag) {
203
+ return function(message, level, ...arg) {
204
+ if(this.#stackTrace || Glog.stackTrace) {
205
+ tag = this.extractFileFunction()
206
+ }
207
+
208
+ this.debug(`[${tag}] ${message}`, level, ...arg)
209
+ }.bind(this)
210
+ }
211
+
212
+ // === LOGGING METHODS ===
213
+
214
+ #log(...args) {
215
+ let level, rest
216
+
217
+ if(args.length === 0) {
218
+ [level = 0, rest = [""]] = []
219
+ } else if(args.length === 1) {
220
+ [rest, level = 0] = [args, 0]
221
+ } else {
222
+ [level, ...rest] = typeof args[0] === "number" ? args : [0, ...args]
223
+ }
224
+
225
+ const currentLevel = this.#logLevel || Glog.logLevel
226
+
227
+ if(level > currentLevel)
228
+ return
229
+
230
+ const prefix = this.#logPrefix || Glog.logPrefix
231
+
232
+ if(prefix) {
233
+ Term.log(prefix, ...rest)
234
+ } else {
235
+ Term.log(...rest)
236
+ }
237
+ }
238
+
239
+ // Traditional logger methods
240
+ debug(message, level = 0, ...arg) {
241
+ const currentLevel = this.#logLevel || Glog.logLevel
242
+
243
+ if(level <= currentLevel) {
244
+ Term.debug(this.#compose("debug", message, level), ...arg)
245
+ }
246
+ }
247
+
248
+ info(message, ...arg) {
249
+ Term.info(this.#compose("info", message), ...arg)
250
+ this.#vscodeInfo?.(JSON.stringify(message))
251
+ }
252
+
253
+ warn(message, ...arg) {
254
+ Term.warn(this.#compose("warn", message), ...arg)
255
+ this.#vscodeWarn?.(JSON.stringify(message))
256
+ }
257
+
258
+ error(message, ...arg) {
259
+ Term.error(this.#compose("error", message), ...arg)
260
+ this.#vscodeError?.(JSON.stringify(message))
261
+ }
262
+
263
+ // Core execute method for simple usage
264
+ static execute(...args) {
265
+ // Use static properties for global calls
75
266
  let level, rest
76
267
 
77
268
  if(args.length === 0) {
78
- ;[level=0, rest=[""]] = []
269
+ [level = 0, rest = [""]] = []
79
270
  } else if(args.length === 1) {
80
- ;[rest, level=0] = [args, 0]
271
+ [rest, level = 0] = [args, 0]
81
272
  } else {
82
- ;[level, ...rest] = typeof args[0] === "number" ? args : [0, ...args]
273
+ [level, ...rest] = typeof args[0] === "number" ? args : [0, ...args]
83
274
  }
84
275
 
85
276
  if(level > this.logLevel)
86
277
  return
87
278
 
88
- if(this.logPrefix)
89
- console.log(this.logPrefix, ...rest)
90
- else
91
- console.log(...rest)
279
+ if(this.logPrefix) {
280
+ Term.log(this.logPrefix, ...rest)
281
+ } else {
282
+ Term.log(...rest)
283
+ }
284
+ }
285
+
286
+ // Instance execute for configured loggers
287
+ execute(...args) {
288
+ this.#log(...args)
92
289
  }
93
290
 
291
+ // === ENHANCED METHODS WITH @gesslar/colours ===
292
+
94
293
  /**
95
- * Executes a log operation with the provided arguments.
96
- * This method serves as the entry point for all logging operations,
97
- * delegating to the private #log method for actual processing.
294
+ * Log a colorized message using template literals
98
295
  *
99
- * @param {...unknown} args - Log level (optional) followed by message arguments
100
- * @returns {void}
101
- * @example
102
- * Glog.execute(0, 'Error:', error.message)
103
- * Glog.execute('Simple message') // Level 0 assumed
296
+ * @param {Array<string>} strings - Template strings
297
+ * @param {...unknown} values - Template values
298
+ * @example logger.colorize`{success}Operation completed{/} in {bold}${time}ms{/}`
104
299
  */
105
- static execute(...args) {
106
- this.#log(...args)
300
+ colorize(strings, ...values) {
301
+ const message = c(strings, ...values)
302
+ const name = this.#name || Glog.name || "Log"
303
+
304
+ Term.log(`[${name}] ${message}`)
305
+ }
306
+
307
+ /**
308
+ * Static version of colorize for global usage
309
+ *
310
+ * @param {Array<string>} strings - Template strings
311
+ * @param {...unknown} values - Template values
312
+ */
313
+ static colorize(strings, ...values) {
314
+ const message = c(strings, ...values)
315
+ const name = this.name || "Log"
316
+
317
+ Term.log(`[${name}] ${message}`)
318
+ }
319
+
320
+ /**
321
+ * Log a success message with green color
322
+ *
323
+ * @param {string} message - Success message
324
+ * @param {...unknown} args - Additional arguments
325
+ */
326
+ success(message, ...args) {
327
+ Term.log(c`[${this.#name || Glog.name || "Log"}] {success}Success{/}: ${message}`, ...args)
328
+ }
329
+
330
+ /**
331
+ * Static success method
332
+ *
333
+ * @param {string} message - Success message to log
334
+ * @param {...unknown} args - Additional arguments to log
335
+ */
336
+ static success(message, ...args) {
337
+ Term.log(c`[${this.name || "Log"}] {success}Success{/}: ${message}`, ...args)
338
+ }
339
+
340
+ /**
341
+ * Set a color alias for convenient usage
342
+ *
343
+ * @param {string} alias - Alias name
344
+ * @param {string} colorCode - Color code (e.g., "{F196}" or "{<B}")
345
+ * @returns {Glog} The Glog class for chaining.
346
+ */
347
+ static setAlias(alias, colorCode) {
348
+ c.alias.set(alias, colorCode)
349
+
350
+ return this
351
+ }
352
+
353
+ /**
354
+ * Get access to the colours template function for instance usage
355
+ *
356
+ * @returns {import('@gesslar/colours')} The colours template function from \@gesslar/colours
357
+ */
358
+ get colours() {
359
+ return c
107
360
  }
108
361
  }
109
362
 
110
- /**
111
- * Global logging utility with proxy-based dual interface.
112
- * Can be used as both a class and a function for maximum flexibility.
113
- *
114
- * @class Glog
115
- * @example
116
- * // Use as function
117
- * Glog('Hello world')
118
- * Glog(2, 'Debug message')
119
- *
120
- * // Use class methods
121
- * Glog.setLogLevel(3).setLogPrefix('[App]')
122
- */
123
- // Wrap the class in a proxy
363
+ // Wrap in proxy for dual usage
124
364
  export default new Proxy(Glog, {
125
365
  apply(target, thisArg, argumentsList) {
126
- // When called as function: call execute method internally
127
366
  return target.execute(...argumentsList)
128
367
  },
129
368
  construct(target, argumentsList) {
130
369
  return new target(...argumentsList)
131
370
  },
132
371
  get(target, prop) {
133
- // Hide execute method from public API
134
372
  if(prop === "execute") {
135
373
  return undefined
136
374
  }
@@ -0,0 +1,182 @@
1
+ /*
2
+ For formatting console info, see:
3
+ https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args
4
+
5
+ * %s: String will be used to convert all values except BigInt, Object and -0.
6
+ BigInt values will be represented with an n and Objects that have no
7
+ user defined toString function are inspected using util.inspect() with
8
+ options { depth: 0, colors: false, compact: 3 }.
9
+ * %d: Number will be used to convert all values except BigInt and Symbol.
10
+ * %i: parseInt(value, 10) is used for all values except BigInt and Symbol.
11
+ * %f: parseFloat(value) is used for all values expect Symbol.
12
+ * %j: JSON. Replaced with the string '[Circular]' if the argument contains
13
+ circular references.
14
+ * %o: Object. A string representation of an object with generic JavaScript
15
+ object formatting. Similar to util.inspect() with options { showHidden:
16
+ true, showProxy: true }. This will show the full object including non-
17
+ enumerable properties and proxies.
18
+ * %O: Object. A string representation of an object with generic JavaScript
19
+ object formatting. Similar to util.inspect() without options. This will
20
+ show the full object not including non-enumerable properties and
21
+ proxies.
22
+ * %%: single percent sign ('%'). This does not consume an argument.
23
+
24
+ */
25
+
26
+ import ErrorStackParser from "error-stack-parser"
27
+ import console from "node:console"
28
+ import {Environment} from "./Core.js"
29
+ import {FileObject, Util} from "@gesslar/toolkit"
30
+
31
+ export const loggerColours = {
32
+ debug: [
33
+ "\x1b[38;5;19m", // Debug level 0: Dark blue
34
+ "\x1b[38;5;27m", // Debug level 1: Medium blue
35
+ "\x1b[38;5;33m", // Debug level 2: Light blue
36
+ "\x1b[38;5;39m", // Debug level 3: Teal
37
+ "\x1b[38;5;44m", // Debug level 4: Blue-tinted cyan
38
+ ],
39
+ info: "\x1b[38;5;36m", // Medium Spring Green
40
+ warn: "\x1b[38;5;214m", // Orange1
41
+ error: "\x1b[38;5;196m", // Red1
42
+ reset: "\x1b[0m", // Reset
43
+ }
44
+
45
+ /**
46
+ * Logger class
47
+ *
48
+ * Log levels:
49
+ * - debug: Debugging information
50
+ * - Debug levels
51
+ * - 0: No/critical debug information, not error level, but, should be
52
+ * logged
53
+ * - 1: Basic debug information, startup, shutdown, etc
54
+ * - 2: Intermediate debug information, discovery, starting to get more
55
+ * detailed
56
+ * - 3: Detailed debug information, parsing, processing, etc
57
+ * - 4: Very detailed debug information, nerd mode!
58
+ * - warn: Warning information
59
+ * - info: Informational information
60
+ * - error: Error information
61
+ */
62
+
63
+ export default class Logger {
64
+ #name = null
65
+ #debugLevel = 0
66
+
67
+ constructor(options) {
68
+ this.#name = "BeDoc"
69
+ if(options) {
70
+ this.setOptions(options)
71
+ if(options.env === Environment.EXTENSION) {
72
+ const vscode = import("vscode")
73
+
74
+ this.vscodeError = vscode.window.showErrorMessage
75
+ this.vscodeWarn = vscode.window.showWarningMessage
76
+ this.vscodeInfo = vscode.window.showInformationMessage
77
+ }
78
+ }
79
+ }
80
+
81
+ get name() {
82
+ return this.#name
83
+ }
84
+
85
+ get debugLevel() {
86
+ return this.#debugLevel
87
+ }
88
+
89
+ get options() {
90
+ return {
91
+ name: this.#name,
92
+ debugLevel: this.#debugLevel,
93
+ }
94
+ }
95
+
96
+ setOptions(options) {
97
+ this.#name = options.name ?? this.#name
98
+ this.#debugLevel = options.debugLevel
99
+ }
100
+
101
+ #compose(level, message, debugLevel = 0) {
102
+ const tag = Util.capitalize(level)
103
+
104
+ if(level === "debug")
105
+ return `[${this.#name}] ${loggerColours[level][debugLevel]}${tag}${loggerColours.reset}: ${message}`
106
+
107
+ return `[${this.#name}] ${loggerColours[level]}${tag}${loggerColours.reset}: ${message}`
108
+ }
109
+
110
+ lastStackLine(error = new Error(), stepsRemoved = 3) {
111
+ const stack = ErrorStackParser.parse(error)
112
+
113
+ return stack[stepsRemoved]
114
+ }
115
+
116
+ extractFileFunction(level = 0) {
117
+ const frame = this.lastStackLine()
118
+ const {
119
+ functionName: func,
120
+ fileName: file,
121
+ lineNumber: line,
122
+ columnNumber: col,
123
+ } = frame
124
+
125
+ const tempFile = new FileObject(file)
126
+ const {module, uri} = tempFile
127
+
128
+ let functionName = func ?? "anonymous"
129
+
130
+ if(functionName.startsWith("#"))
131
+ functionName = `${module}.${functionName}`
132
+
133
+ const methodName = /\[as \w+\]$/.test(functionName)
134
+ ? /\[as (\w+)\]/.exec(functionName)[1]
135
+ : null
136
+
137
+ if(methodName) {
138
+ functionName = functionName.replace(/\[as \w+\]$/, "")
139
+ functionName = `${functionName}{${methodName}}`
140
+ }
141
+
142
+ if(/^async /.test(functionName))
143
+ functionName = functionName.replace(/^async /, "(async)")
144
+
145
+ let result = functionName
146
+
147
+ if(level >= 2)
148
+ result = `${result}:${line}:${col}`
149
+
150
+ if(level >= 3)
151
+ result = `${uri} ${result}`
152
+
153
+ return result
154
+ }
155
+
156
+ newDebug(tag) {
157
+ return function(message, level, ...arg) {
158
+ tag = this.extractFileFunction(this.#debugLevel)
159
+ this.debug(`[${tag}] ${message}`, level, ...arg)
160
+ }.bind(this)
161
+ }
162
+
163
+ debug(message, level = 0, ...arg) {
164
+ if(level <= (this.debugLevel ?? 4))
165
+ console.debug(this.#compose("debug", message, level), ...arg)
166
+ }
167
+
168
+ warn(message, ...arg) {
169
+ console.warn(this.#compose("warn", message), ...arg)
170
+ this.vscodeWarn?.(JSON.stringify(message))
171
+ }
172
+
173
+ info(message, ...arg) {
174
+ console.info(this.#compose("info", message), ...arg)
175
+ this.vscodeInfo?.(JSON.stringify(message))
176
+ }
177
+
178
+ error(message, ...arg) {
179
+ console.error(this.#compose("error", message), ...arg)
180
+ this.vscodeError?.(JSON.stringify(message))
181
+ }
182
+ }
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Generic Pipeline - Process items through a series of steps with concurrency control
3
+ *
4
+ * This abstraction handles:
5
+ * - Concurrent processing with configurable limits
6
+ * - Pipeline of processing steps
7
+ * - Result categorization (success/warning/error)
8
+ * - Setup/cleanup lifecycle hooks
9
+ * - Error handling and reporting
10
+ */
11
+
12
+ export default class Piper {
13
+ #succeeded = []
14
+ #warned = []
15
+ #errored = []
16
+ #steps = []
17
+ #setupHooks = []
18
+ #cleanupHooks = []
19
+ #logger
20
+
21
+ constructor(options = {}) {
22
+ this.#logger = options.logger || {newDebug: () => () => {}}
23
+ }
24
+
25
+ /**
26
+ * Add a processing step to the pipeline
27
+ *
28
+ * @param {(context: object) => Promise<object>} stepFn - Function that processes an item: (context) => Promise<result>
29
+ * @param {object} options - Step options (name, required, etc.)
30
+ * @returns {Piper} The pipeline instance (for chaining)
31
+ */
32
+ addStep(stepFn, options = {}) {
33
+ this.#steps.push({
34
+ fn: stepFn,
35
+ name: options.name || `Step ${this.#steps.length + 1}`,
36
+ required: options.required !== false, // Default to required
37
+ ...options
38
+ })
39
+
40
+ return this
41
+ }
42
+
43
+ /**
44
+ * Add setup hook that runs before processing starts
45
+ *
46
+ * @param {() => Promise<void>} setupFn - Setup function: () => Promise<void>
47
+ * @returns {Piper} The pipeline instance (for chaining)
48
+ */
49
+ addSetup(setupFn) {
50
+ this.#setupHooks.push(setupFn)
51
+
52
+ return this
53
+ }
54
+
55
+ /**
56
+ * Add cleanup hook that runs after processing completes
57
+ *
58
+ * @param {() => Promise<void>} cleanupFn - Cleanup function: () => Promise<void>
59
+ * @returns {Piper} The pipeline instance (for chaining)
60
+ */
61
+ addCleanup(cleanupFn) {
62
+ this.#cleanupHooks.push(cleanupFn)
63
+
64
+ return this
65
+ }
66
+
67
+ /**
68
+ * Process items through the pipeline with concurrency control
69
+ *
70
+ * @param {Array} items - Items to process
71
+ * @param {number} maxConcurrent - Maximum concurrent items to process
72
+ * @returns {Promise<object>} - Results object with succeeded, warned, errored arrays
73
+ */
74
+ async pipe(items, maxConcurrent = 10) {
75
+ const itemQueue = [...items]
76
+ const activePromises = []
77
+
78
+ // Run setup hooks
79
+ await Promise.allSettled(this.#setupHooks.map(hook => hook()))
80
+
81
+ const processNextItem = item => {
82
+ return this.#processItem(item).then(result => {
83
+ // Categorize result
84
+ if(result.status === "success") {
85
+ this.#succeeded.push({input: item, ...result})
86
+ } else if(result.status === "warning") {
87
+ this.#warned.push({input: item, ...result})
88
+ } else {
89
+ this.#errored.push({input: item, ...result})
90
+ }
91
+
92
+ // Process next item if queue has items
93
+ if(itemQueue.length > 0) {
94
+ const nextItem = itemQueue.shift()
95
+
96
+ return processNextItem(nextItem)
97
+ }
98
+ })
99
+ }
100
+
101
+ // Fill initial concurrent slots
102
+ while(activePromises.length < maxConcurrent && itemQueue.length > 0) {
103
+ const item = itemQueue.shift()
104
+
105
+ activePromises.push(processNextItem(item))
106
+ }
107
+
108
+ // Wait for all processing to complete
109
+ await Promise.allSettled(activePromises)
110
+
111
+ // Run cleanup hooks
112
+ await Promise.allSettled(this.#cleanupHooks.map(hook => hook()))
113
+
114
+ return {
115
+ succeeded: this.#succeeded,
116
+ warned: this.#warned,
117
+ errored: this.#errored
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Process a single item through all pipeline steps
123
+ *
124
+ * @param {object} item - The item to process
125
+ * @returns {Promise<object>} Result object with status and data
126
+ * @private
127
+ */
128
+ async #processItem(item) {
129
+ const debug = this.#logger.newDebug()
130
+ const context = {item, data: {}}
131
+
132
+ try {
133
+ // Execute each step in sequence
134
+ for(const step of this.#steps) {
135
+ debug(`Executing step: ${step.name}`, 2)
136
+
137
+ const result = await step.fn(context)
138
+
139
+ // Handle step result
140
+ if(result && typeof result === "object") {
141
+ if(result.status === "error") {
142
+ return result
143
+ }
144
+
145
+ if(result.status === "warning" && step.required) {
146
+ return result
147
+ }
148
+
149
+ // Merge result data into context for next steps
150
+ context.data = {...context.data, ...result.data}
151
+ context.status = result.status || context.status
152
+ }
153
+ }
154
+
155
+ return {
156
+ status: context.status || "success",
157
+ ...context.data
158
+ }
159
+
160
+ } catch(error) {
161
+ return {
162
+ status: "error",
163
+ error,
164
+ item
165
+ }
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Clear results (useful for reusing pipeline instance)
171
+ *
172
+ * @returns {Piper} The pipeline instance (for chaining)
173
+ */
174
+ clearResults() {
175
+ this.#succeeded = []
176
+ this.#warned = []
177
+ this.#errored = []
178
+
179
+ return this
180
+ }
181
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * @file Tantrum.js
3
+ *
4
+ * Defines the Tantrum class, a custom AggregateError type for toolkit
5
+ * that collects multiple errors with Sass-style reporting.
6
+ *
7
+ * Auto-wraps plain Error objects in Sass instances while preserving
8
+ * existing Sass errors, providing consistent formatted output for
9
+ * multiple error scenarios.
10
+ */
11
+
12
+ import Sass from "./Sass.js"
13
+ import Term from "./Term.js"
14
+
15
+ /**
16
+ * Custom aggregate error class that extends AggregateError.
17
+ * Automatically wraps plain errors in Sass instances for consistent reporting.
18
+ */
19
+ export default class Tantrum extends AggregateError {
20
+ /**
21
+ * Creates a new Tantrum instance.
22
+ *
23
+ * @param {string} message - The aggregate error message
24
+ * @param {Array<Error|Sass>} errors - Array of errors to aggregate
25
+ */
26
+ constructor(message, errors = []) {
27
+ // Auto-wrap plain errors in Sass, keep existing Sass instances
28
+ const wrappedErrors = errors.map(error => {
29
+ if(error instanceof Sass) {
30
+ return error
31
+ }
32
+
33
+ if(!(error instanceof Error)) {
34
+ throw new TypeError(`All items in errors array must be Error instances, got: ${typeof error}`)
35
+ }
36
+
37
+ return Sass.new(error.message, error)
38
+ })
39
+
40
+ super(wrappedErrors, message)
41
+ this.name = "Tantrum"
42
+ }
43
+ /**
44
+ * Reports all aggregated errors to the terminal with formatted output.
45
+ *
46
+ * @param {boolean} [nerdMode] - Whether to include detailed stack traces
47
+ */
48
+ report(nerdMode = false) {
49
+ Term.error(
50
+ `${Term.terminalBracket(["error", "Tantrum Incoming"])} (${this.errors.length} errors)\n` +
51
+ this.message
52
+ )
53
+
54
+ this.errors.forEach(error => {
55
+ Term.error("\n")
56
+ error.report(nerdMode)
57
+ })
58
+ }
59
+
60
+ /**
61
+ * Factory method to create a Tantrum instance.
62
+ *
63
+ * @param {string} message - The aggregate error message
64
+ * @param {Array<Error|Sass>} errors - Array of errors to aggregate
65
+ * @returns {Tantrum} New Tantrum instance
66
+ */
67
+ static new(message, errors = []) {
68
+ return new Tantrum(message, errors)
69
+ }
70
+ }
@@ -326,4 +326,27 @@ export default class FileObject extends FS {
326
326
 
327
327
  /** Load an object from JSON5 or YAML file with type specification */
328
328
  loadData(type?: 'json' | 'json5' | 'yaml' | 'any', encoding?: string): Promise<unknown>
329
+
330
+ /**
331
+ * Dynamically import the file using the resolved file URI.
332
+ *
333
+ * Uses Node.js' native dynamic `import()` under the hood, allowing consumers to load
334
+ * ESM modules from disk with full path resolution handled by FileObject. The method
335
+ * verifies the file exists before attempting the import to provide clearer error
336
+ * messaging and prevent low-level loader failures.
337
+ *
338
+ * @typeParam TModule - Expected module shape. Defaults to a loose record to help with
339
+ * module namespace typing.
340
+ *
341
+ * @returns The imported module namespace object.
342
+ *
343
+ * @throws {Error} When the file does not exist or the path cannot be converted to a URI.
344
+ *
345
+ * @example
346
+ * ```typescript
347
+ * const configModule = await file.import<{ default: Config }>()
348
+ * const config = configModule.default
349
+ * ```
350
+ */
351
+ import<TModule = Record<string, unknown>>(): Promise<TModule>
329
352
  }
@@ -0,0 +1,81 @@
1
+ // Implementation: ../lib/Tantrum.js
2
+ // Type definitions for Tantrum aggregate error class
3
+
4
+ import Sass from './Sass'
5
+
6
+ /**
7
+ * Custom aggregate error class that extends AggregateError.
8
+ *
9
+ * Automatically wraps plain Error objects in Sass instances while preserving
10
+ * existing Sass errors, providing consistent formatted reporting for
11
+ * multiple error scenarios.
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * // Collect multiple errors and throw as a bundle
16
+ * const errors = [new Error("thing 1"), sassError, new Error("thing 3")]
17
+ * throw Tantrum.new("Multiple validation failures", errors)
18
+ *
19
+ * // Later, in error handling:
20
+ * catch (error) {
21
+ * if (error instanceof Tantrum) {
22
+ * error.report() // Reports all errors with Sass formatting
23
+ * }
24
+ * }
25
+ * ```
26
+ */
27
+ export default class Tantrum extends AggregateError {
28
+ /**
29
+ * Creates a new Tantrum instance.
30
+ * Plain Error objects are automatically wrapped in Sass instances.
31
+ *
32
+ * @param message - The aggregate error message describing the overall failure
33
+ * @param errors - Array of errors to aggregate (mix of Error and Sass instances allowed)
34
+ */
35
+ constructor(message: string, errors?: Array<Error | Sass>)
36
+
37
+ /** Name of the error class */
38
+ readonly name: 'Tantrum'
39
+
40
+ /** Array of aggregated errors (all wrapped as Sass instances) */
41
+ readonly errors: Array<Sass>
42
+
43
+ /**
44
+ * Reports all aggregated errors to the terminal with formatted output.
45
+ * Shows a header with error count, then delegates to each Sass instance
46
+ * for individual error reporting.
47
+ *
48
+ * @param nerdMode - Whether to include detailed stack traces in output
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * try {
53
+ * throw Tantrum.new("Batch failed", [error1, error2])
54
+ * } catch (tantrum) {
55
+ * tantrum.report() // User-friendly output
56
+ * tantrum.report(true) // Includes full stack traces
57
+ * }
58
+ * ```
59
+ */
60
+ report(nerdMode?: boolean): void
61
+
62
+ /**
63
+ * Factory method to create a Tantrum instance.
64
+ * Follows the same pattern as Sass.new() for consistency.
65
+ *
66
+ * @param message - The aggregate error message
67
+ * @param errors - Array of errors to aggregate
68
+ * @returns New Tantrum instance with all errors wrapped as Sass
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * // Typical usage pattern
73
+ * throw Tantrum.new("Someone ate all my Runts!", [
74
+ * emptyRuntsBoxError,
75
+ * emptyRuntsBoxError,
76
+ * emptyRuntsBoxError
77
+ * ])
78
+ * ```
79
+ */
80
+ static new(message: string, errors?: Array<Error | Sass>): Tantrum
81
+ }
@@ -10,6 +10,7 @@ export { default as Collection } from './Collection.js'
10
10
  export { default as Data } from './Data.js'
11
11
  export { default as Glog } from './Glog.js'
12
12
  export { default as Sass } from './Sass.js'
13
+ export { default as Tantrum } from './Tantrum.js'
13
14
  export { default as Term } from './Term.js'
14
15
  export { default as Type } from './Type.js'
15
16
  export { default as Util } from './Util.js'