@gesslar/toolkit 0.2.8 → 0.3.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.
@@ -0,0 +1,206 @@
1
+ import { setTimeout as timeoutPromise } from "timers/promises"
2
+ import Collection from "./Collection.js"
3
+ import Data from "./Data.js"
4
+ import Sass from "./Sass.js"
5
+ import Valid from "./Valid.js"
6
+
7
+ /**
8
+ * Generic base class for managing hooks with configurable event types.
9
+ * Provides common functionality for hook registration, execution, and lifecycle management.
10
+ * Designed to be extended by specific implementations.
11
+ */
12
+ export default class BaseHookManager {
13
+ #hooksFile = null
14
+ #log = null
15
+ #hooks = null
16
+ #action = null
17
+ #timeout = 1000 // Default 1 second timeout
18
+ #allowedEvents = []
19
+
20
+ /**
21
+ * @param {object} config - Configuration object
22
+ * @param {string|object} config.action - Action identifier or instance
23
+ * @param {object} config.hooksFile - File object containing hooks
24
+ * @param {object} config.logger - Logger instance
25
+ * @param {number} [config.timeOut=1000] - Hook execution timeout in milliseconds
26
+ * @param {string[]} [config.allowedEvents=[]] - Array of allowed event types for validation
27
+ */
28
+ constructor({action, hooksFile, logger, timeOut = 1000, allowedEvents = []}) {
29
+ this.#action = action
30
+ this.#hooksFile = hooksFile
31
+ this.#log = logger
32
+ this.#timeout = timeOut
33
+ this.#allowedEvents = allowedEvents
34
+ }
35
+
36
+ get action() {
37
+ return this.#action
38
+ }
39
+
40
+ get hooksFile() {
41
+ return this.#hooksFile
42
+ }
43
+
44
+ get hooks() {
45
+ return this.#hooks
46
+ }
47
+
48
+ get log() {
49
+ return this.#log
50
+ }
51
+
52
+ get timeout() {
53
+ return this.#timeout
54
+ }
55
+
56
+ get allowedEvents() {
57
+ return this.#allowedEvents
58
+ }
59
+
60
+ get setup() {
61
+ return this.hooks?.setup || null
62
+ }
63
+
64
+ get cleanup() {
65
+ return this.hooks?.cleanup || null
66
+ }
67
+
68
+ /**
69
+ * Static factory method to create and initialize a hook manager.
70
+ * Override loadHooks() in subclasses to customize hook loading logic.
71
+ *
72
+ * @param {object} config - Same as constructor config
73
+ * @returns {Promise<BaseHookManager|null>} Initialized hook manager or null if no hooks found
74
+ */
75
+ static async new(config) {
76
+ const instance = new this(config)
77
+ const debug = instance.log.newDebug()
78
+
79
+ debug("Creating new HookManager instance with args: `%o`", 2, config)
80
+
81
+ const hooksFile = instance.hooksFile
82
+
83
+ debug("Loading hooks from `%s`", 2, hooksFile.uri)
84
+
85
+ debug("Checking hooks file exists: %j", 2, hooksFile)
86
+
87
+ try {
88
+ const hooksFileContent = await import(hooksFile.uri)
89
+ debug("Hooks file loaded successfully", 2)
90
+
91
+ if (!hooksFileContent)
92
+ throw new Error(`Hooks file is empty: ${hooksFile.uri}`)
93
+
94
+ const hooks = await instance.loadHooks(hooksFileContent)
95
+
96
+ if (Data.isEmpty(hooks))
97
+ return null
98
+
99
+ debug("Hooks found for action: `%s`", 2, instance.action)
100
+
101
+ if (!hooks)
102
+ return null
103
+
104
+ // Attach common properties to hooks
105
+ hooks.log = instance.log
106
+ hooks.timeout = instance.timeout
107
+ instance.#hooks = hooks
108
+
109
+ debug("Hooks loaded successfully for `%s`", 2, instance.action)
110
+
111
+ return instance
112
+ } catch (error) {
113
+ debug("Failed to load hooks: %s", 1, error.message)
114
+ return null
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Load hooks from the imported hooks file content.
120
+ * Override in subclasses to customize hook loading logic.
121
+ *
122
+ * @param {object} hooksFileContent - Imported hooks file content
123
+ * @returns {Promise<object|null>} Loaded hooks object or null if no hooks found
124
+ * @protected
125
+ */
126
+ async loadHooks(hooksFileContent) {
127
+ const hooks = hooksFileContent.default || hooksFileContent.Hooks
128
+
129
+ if (!hooks)
130
+ throw new Error(`\`${this.hooksFile.uri}\` contains no hooks.`)
131
+
132
+ // Default implementation: look for hooks by action name
133
+ const hooksObj = hooks[this.action]
134
+
135
+ return hooksObj || null
136
+ }
137
+
138
+ /**
139
+ * Trigger a hook by event name.
140
+ *
141
+ * @param {string} event - The type of hook to trigger
142
+ * @param {object} args - The hook arguments as an object
143
+ * @returns {Promise<unknown>} The result of the hook
144
+ */
145
+ async on(event, args) {
146
+ const debug = this.log.newDebug()
147
+
148
+ debug("Triggering hook for event `%s`", 4, event)
149
+
150
+ if (!event)
151
+ throw new Error("Event type is required for hook invocation")
152
+
153
+ // Validate event type if allowed events are configured
154
+ if (this.#allowedEvents.length > 0 && !this.#allowedEvents.includes(event))
155
+ throw new Error(`Invalid event type: ${event}. Allowed events: ${this.#allowedEvents.join(", ")}`)
156
+
157
+ const hook = this.hooks?.[event]
158
+
159
+ if (hook) {
160
+ Valid.type(hook, "function", `Hook "${event}" is not a function`)
161
+
162
+ const hookExecution = hook.call(this.hooks, args)
163
+ const hookTimeout = this.timeout
164
+
165
+ const expireAsync = () =>
166
+ timeoutPromise(
167
+ hookTimeout,
168
+ new Error(`Hook execution exceeded timeout of ${hookTimeout}ms`)
169
+ )
170
+
171
+ const result = await Promise.race([hookExecution, expireAsync()])
172
+
173
+ if (result?.status === "error")
174
+ throw Sass.new(result.error)
175
+
176
+ debug("Hook executed successfully for event: `%s`", 4, event)
177
+
178
+ return result
179
+ } else {
180
+ debug("No hook found for event: `%s`", 4, event)
181
+ return null
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Check if a hook exists for the given event.
187
+ *
188
+ * @param {string} event - Event name to check
189
+ * @returns {boolean} True if hook exists
190
+ */
191
+ hasHook(event) {
192
+ return !!(this.hooks?.[event])
193
+ }
194
+
195
+ /**
196
+ * Get all available hook events.
197
+ *
198
+ * @returns {string[]} Array of available hook event names
199
+ */
200
+ getAvailableEvents() {
201
+ return this.hooks ? Object.keys(this.hooks).filter(key =>
202
+ typeof this.hooks[key] === 'function' &&
203
+ !['setup', 'cleanup', 'log', 'timeout'].includes(key)
204
+ ) : []
205
+ }
206
+ }
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
  }