@gesslar/toolkit 0.2.9 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gesslar/toolkit",
3
- "version": "0.2.9",
3
+ "version": "0.3.0",
4
4
  "description": "Get in, bitches, we're going toolkitting.",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
@@ -25,7 +25,8 @@
25
25
  "submit": "npm publish --access public",
26
26
  "update": "npx npm-check-updates -u && npm install",
27
27
  "test": "node --test tests/unit/*.test.js",
28
- "test:unit": "node --test tests/unit/*.test.js"
28
+ "test:unit": "node --test tests/unit/*.test.js",
29
+ "pr": "gt submit --cli --publish --restack --ai --merge-when-ready"
29
30
  },
30
31
  "repository": {
31
32
  "type": "git",
@@ -19,7 +19,7 @@ export default class ActionRunner {
19
19
  constructor({action, build, logger}) {
20
20
  this.#action = action
21
21
  this.#build = build
22
- this.#logger = logger ?? Glog.newDebug()
22
+ this.#logger = logger ?? {newDebug: () => () => {}}
23
23
  }
24
24
 
25
25
  /**
@@ -0,0 +1,246 @@
1
+ import Data from "./Data.js"
2
+ import Sass from "./Sass.js"
3
+ import ActionBuilder from "./ActionBuilder.js"
4
+ import ActionRunner from "./ActionRunner.js"
5
+
6
+ /**
7
+ * Generic base class for managing actions with lifecycle hooks.
8
+ * Provides common functionality for action setup, execution, and cleanup.
9
+ * Designed to be extended by specific implementations.
10
+ */
11
+ export default class BaseActionManager {
12
+ #action = null
13
+ #hookManager = null
14
+ #contract = null
15
+ #log = null
16
+ #debug = null
17
+ #file = null
18
+ #variables = null
19
+ #runner = null
20
+ #id = null
21
+
22
+ /**
23
+ * @param {object} config - Configuration object
24
+ * @param {object} config.actionDefinition - Action definition with action, file, and contract
25
+ * @param {object} config.logger - Logger instance
26
+ * @param {object} [config.variables] - Variables to pass to action
27
+ */
28
+ constructor({actionDefinition, logger, variables}) {
29
+ this.#id = Symbol(performance.now())
30
+ this.#log = logger
31
+ this.#debug = this.#log.newDebug()
32
+ this.#variables = variables || {}
33
+
34
+ this.#initialize(actionDefinition)
35
+ }
36
+
37
+ get id() {
38
+ return this.#id
39
+ }
40
+
41
+ get action() {
42
+ return this.#action
43
+ }
44
+
45
+ get hookManager() {
46
+ return this.#hookManager
47
+ }
48
+
49
+ set hookManager(hookManager) {
50
+ if (this.hookManager)
51
+ throw new Error("Hook manager already set")
52
+
53
+ this.#hookManager = hookManager
54
+ this.#attachHooksToAction(hookManager)
55
+ }
56
+
57
+ get contract() {
58
+ return this.#contract
59
+ }
60
+
61
+ get meta() {
62
+ return this.#action?.meta
63
+ }
64
+
65
+ get log() {
66
+ return this.#log
67
+ }
68
+
69
+ get variables() {
70
+ return this.#variables
71
+ }
72
+
73
+ get runner() {
74
+ return this.#runner
75
+ }
76
+
77
+ get file() {
78
+ return this.#file
79
+ }
80
+
81
+ /**
82
+ * Initialize the action manager with the provided definition.
83
+ * Override in subclasses to add specific validation or setup.
84
+ *
85
+ * @param {object} actionDefinition - Action definition
86
+ * @protected
87
+ */
88
+ #initialize(actionDefinition) {
89
+ const debug = this.#debug
90
+
91
+ debug("Setting up action", 2)
92
+
93
+ const {action, file, contract} = actionDefinition
94
+
95
+ if (!action)
96
+ throw new Error("Action is required")
97
+
98
+ if (!contract)
99
+ throw new Error("Contract is required")
100
+
101
+ this.#action = action
102
+ this.#contract = contract
103
+ this.#file = file
104
+
105
+ debug("Action initialization complete", 2)
106
+ }
107
+
108
+ /**
109
+ * Attach hooks to the action instance.
110
+ * Override in subclasses to customize hook attachment.
111
+ *
112
+ * @param {object} hookManager - Hook manager instance
113
+ * @protected
114
+ */
115
+ #attachHooksToAction(hookManager) {
116
+ // Basic hook attachment - can be overridden by subclasses
117
+ this.action.hook = hookManager.on?.bind(hookManager)
118
+ this.action.hooks = hookManager.hooks
119
+ }
120
+
121
+ /**
122
+ * Setup the action by creating and configuring the runner.
123
+ * Override setupActionInstance() in subclasses for custom setup logic.
124
+ *
125
+ * @returns {Promise<void>}
126
+ */
127
+ async setupAction() {
128
+ this.#debug("Setting up action for %s on %s", 2, this.action.meta?.kind, this.id)
129
+
130
+ await this.#setupHooks()
131
+ await this.#setupActionInstance()
132
+ }
133
+
134
+ /**
135
+ * Setup the action instance and create the runner.
136
+ * Override in subclasses to customize action setup.
137
+ *
138
+ * @protected
139
+ */
140
+ async #setupActionInstance() {
141
+ const actionInstance = new this.action()
142
+ const setup = actionInstance?.setup
143
+
144
+ // Setup is required for actions.
145
+ if (Data.typeOf(setup) === "Function") {
146
+ const builder = new ActionBuilder(actionInstance)
147
+ const configuredBuilder = setup(builder)
148
+ const buildResult = configuredBuilder.build()
149
+ const runner = new ActionRunner({
150
+ action: buildResult.action,
151
+ build: buildResult.build,
152
+ logger: this.#log
153
+ })
154
+
155
+ this.#runner = runner
156
+ } else {
157
+ throw Sass.new("Action setup must be a function.")
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Run the action with the provided input.
163
+ *
164
+ * @param {unknown} result - Input to pass to the action
165
+ * @returns {Promise<unknown>} Action result
166
+ */
167
+ async runAction(result) {
168
+ if (!this.#runner)
169
+ throw new Error("Action not set up. Call setupAction() first.")
170
+
171
+ return await this.#runner.run(result)
172
+ }
173
+
174
+ /**
175
+ * Cleanup the action and hooks.
176
+ *
177
+ * @returns {Promise<void>}
178
+ */
179
+ async cleanupAction() {
180
+ this.#debug("Cleaning up action for %s on %s", 2, this.action.meta?.kind, this.id)
181
+
182
+ await this.#cleanupHooks()
183
+ await this.#cleanupActionInstance()
184
+ }
185
+
186
+ /**
187
+ * Setup hooks if hook manager is present.
188
+ * Override in subclasses to customize hook setup.
189
+ *
190
+ * @protected
191
+ */
192
+ async #setupHooks() {
193
+ const setup = this.#hookManager?.setup
194
+
195
+ const type = Data.typeOf(setup)
196
+
197
+ // No hooks attached.
198
+ if (type === "Null" || type === "Undefined")
199
+ return
200
+
201
+ if (type !== "Function")
202
+ throw Sass.new("Hook setup must be a function.")
203
+
204
+ await setup.call(
205
+ this.hookManager.hooks, {
206
+ action: this.action,
207
+ variables: this.#variables,
208
+ log: this.#log
209
+ }
210
+ )
211
+ }
212
+
213
+ /**
214
+ * Cleanup hooks if hook manager is present.
215
+ * Override in subclasses to customize hook cleanup.
216
+ *
217
+ * @protected
218
+ */
219
+ async #cleanupHooks() {
220
+ const cleanup = this.hookManager?.cleanup
221
+
222
+ if (!cleanup)
223
+ return
224
+
225
+ await cleanup.call(this.hookManager.hooks)
226
+ }
227
+
228
+ /**
229
+ * Cleanup the action instance.
230
+ * Override in subclasses to add custom cleanup logic.
231
+ *
232
+ * @protected
233
+ */
234
+ async #cleanupActionInstance() {
235
+ const cleanup = this.action?.cleanup
236
+
237
+ if (!cleanup)
238
+ return
239
+
240
+ await cleanup.call(this.action)
241
+ }
242
+
243
+ toString() {
244
+ return `${this.#file?.module || "UNDEFINED"} (${this.meta?.action || "UNDEFINED"})`
245
+ }
246
+ }
@@ -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/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}`
@@ -26,13 +26,11 @@ export default class Tantrum extends AggregateError {
26
26
  constructor(message, errors = []) {
27
27
  // Auto-wrap plain errors in Sass, keep existing Sass instances
28
28
  const wrappedErrors = errors.map(error => {
29
- if(error instanceof Sass) {
29
+ if(error instanceof Sass)
30
30
  return error
31
- }
32
31
 
33
- if(!(error instanceof Error)) {
32
+ if(!(error instanceof Error))
34
33
  throw new TypeError(`All items in errors array must be Error instances, got: ${typeof error}`)
35
- }
36
34
 
37
35
  return Sass.new(error.message, error)
38
36
  })
@@ -51,8 +49,9 @@ export default class Tantrum extends AggregateError {
51
49
  this.message
52
50
  )
53
51
 
52
+ Term.error()
53
+
54
54
  this.errors.forEach(error => {
55
- Term.error("\n")
56
55
  error.report(nerdMode)
57
56
  })
58
57
  }