@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,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
+ }
package/src/lib/Sass.js CHANGED
@@ -102,10 +102,8 @@ export default class Sass extends Error {
102
102
  * @returns {string|undefined} Formatted stack trace or undefined
103
103
  */
104
104
  #fullBodyMassage(stack) {
105
- // Remove the first line, it's already been reported
106
-
107
105
  stack = stack ?? ""
108
-
106
+ // Remove the first line, it's already been reported
109
107
  const {rest} = stack.match(/^.*?\n(?<rest>[\s\S]+)$/m)?.groups ?? {}
110
108
  const lines = []
111
109
 
@@ -114,7 +112,7 @@ export default class Sass extends Error {
114
112
  ...rest
115
113
  .split("\n")
116
114
  .map(line => {
117
- const at = line.match(/^\s{4}at\s(?<at>.*)$/)?.groups?.at ?? {}
115
+ const at = line.match(/^\s{4}at\s(?<at>.*)$/)?.groups?.at ?? ""
118
116
 
119
117
  return at
120
118
  ? `* ${at}`
@@ -0,0 +1,69 @@
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
+ if(!(error instanceof Error))
33
+ throw new TypeError(`All items in errors array must be Error instances, got: ${typeof error}`)
34
+
35
+ return Sass.new(error.message, error)
36
+ })
37
+
38
+ super(wrappedErrors, message)
39
+ this.name = "Tantrum"
40
+ }
41
+ /**
42
+ * Reports all aggregated errors to the terminal with formatted output.
43
+ *
44
+ * @param {boolean} [nerdMode] - Whether to include detailed stack traces
45
+ */
46
+ report(nerdMode = false) {
47
+ Term.error(
48
+ `${Term.terminalBracket(["error", "Tantrum Incoming"])} (${this.errors.length} errors)\n` +
49
+ this.message
50
+ )
51
+
52
+ Term.error()
53
+
54
+ this.errors.forEach(error => {
55
+ error.report(nerdMode)
56
+ })
57
+ }
58
+
59
+ /**
60
+ * Factory method to create a Tantrum instance.
61
+ *
62
+ * @param {string} message - The aggregate error message
63
+ * @param {Array<Error|Sass>} errors - Array of errors to aggregate
64
+ * @returns {Tantrum} New Tantrum instance
65
+ */
66
+ static new(message, errors = []) {
67
+ return new Tantrum(message, errors)
68
+ }
69
+ }
@@ -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'