@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 +2 -1
- package/src/index.js +1 -0
- package/src/lib/ActionBuilder.js +144 -0
- package/src/lib/ActionRunner.js +109 -0
- package/src/lib/FileObject.js +20 -3
- package/src/lib/Glog.js +324 -86
- package/src/lib/Logger.js +182 -0
- package/src/lib/Piper.js +181 -0
- package/src/lib/Tantrum.js +70 -0
- package/src/types/FileObject.d.ts +23 -0
- package/src/types/Tantrum.d.ts +81 -0
- package/src/types/index.d.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gesslar/toolkit",
|
|
3
|
-
"version": "0.2.
|
|
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
|
+
}
|
package/src/lib/FileObject.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
269
|
+
[level = 0, rest = [""]] = []
|
|
79
270
|
} else if(args.length === 1) {
|
|
80
|
-
|
|
271
|
+
[rest, level = 0] = [args, 0]
|
|
81
272
|
} else {
|
|
82
|
-
|
|
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
|
-
|
|
90
|
-
else
|
|
91
|
-
|
|
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
|
-
*
|
|
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 {
|
|
100
|
-
* @
|
|
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
|
-
|
|
106
|
-
|
|
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
|
+
}
|
package/src/lib/Piper.js
ADDED
|
@@ -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
|
+
}
|
package/src/types/index.d.ts
CHANGED
|
@@ -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'
|