@gesslar/bedoc 1.0.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/CONTRIBUTING.md +125 -0
- package/LICENSE +24 -0
- package/README.md +93 -0
- package/package.json +64 -0
- package/src/cli.js +92 -0
- package/src/core/ActionManager.js +76 -0
- package/src/core/Configuration.js +330 -0
- package/src/core/ConfigurationParameters.js +142 -0
- package/src/core/Conveyor.js +113 -0
- package/src/core/Core.js +186 -0
- package/src/core/Discovery.js +208 -0
- package/src/core/HooksManager.js +143 -0
- package/src/core/Logger.js +191 -0
- package/src/core/action/ParseManager.js +26 -0
- package/src/core/action/PrintManager.js +26 -0
- package/src/core/util/ActionUtil.js +47 -0
- package/src/core/util/DataUtil.js +479 -0
- package/src/core/util/FDUtil.js +322 -0
- package/src/core/util/ModuleUtil.js +39 -0
- package/src/core/util/StringUtil.js +11 -0
- package/src/core/util/TypeSpec.js +114 -0
- package/src/core/util/ValidUtil.js +50 -0
package/src/core/Core.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import process from "node:process"
|
|
2
|
+
|
|
3
|
+
import Discovery from "./Discovery.js"
|
|
4
|
+
import {HooksManager} from "./HooksManager.js"
|
|
5
|
+
import Logger from "./Logger.js"
|
|
6
|
+
import ParseManager from "./action/ParseManager.js"
|
|
7
|
+
import PrintManager from "./action/PrintManager.js"
|
|
8
|
+
import Conveyor from "./Conveyor.js"
|
|
9
|
+
import Configuration from "./Configuration.js"
|
|
10
|
+
|
|
11
|
+
import * as ActionUtil from "./util/ActionUtil.js"
|
|
12
|
+
import * as DataUtil from "./util/DataUtil.js"
|
|
13
|
+
import * as FDUtil from "./util/FDUtil.js"
|
|
14
|
+
|
|
15
|
+
const {loadPackageJson} = ActionUtil
|
|
16
|
+
const {schemaCompare} = DataUtil
|
|
17
|
+
const {getFiles} = FDUtil
|
|
18
|
+
|
|
19
|
+
export const Environment = Object.freeze({
|
|
20
|
+
EXTENSION: "extension",
|
|
21
|
+
NPM: "npm",
|
|
22
|
+
ACTION: "action",
|
|
23
|
+
CLI: "cli",
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export default class Core {
|
|
27
|
+
constructor(options) {
|
|
28
|
+
this.options = options
|
|
29
|
+
const {debug: debugMode, debugLevel} = options
|
|
30
|
+
this.logger = new Logger({name: "BeDoc", debugMode, debugLevel})
|
|
31
|
+
this.packageJson = loadPackageJson()?.bedoc ?? {}
|
|
32
|
+
this.debugOptions = this.logger.options
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static async new({options, source}) {
|
|
36
|
+
const configuration = new Configuration()
|
|
37
|
+
|
|
38
|
+
const validatedConfig = await configuration.validate({options, source})
|
|
39
|
+
if(validatedConfig.status === "error")
|
|
40
|
+
throw new AggregateError(validatedConfig.errors, "BeDoc configuration failed")
|
|
41
|
+
|
|
42
|
+
const instance = new Core({...validatedConfig, name: "BeDoc"})
|
|
43
|
+
const debug = instance.logger.newDebug()
|
|
44
|
+
|
|
45
|
+
debug("Creating new BeDoc instance with options: `%o`", 2, validatedConfig)
|
|
46
|
+
|
|
47
|
+
const discovery = new Discovery(instance)
|
|
48
|
+
const actionDefinitions = await discovery.discoverActions()
|
|
49
|
+
|
|
50
|
+
const filteredActions = {
|
|
51
|
+
parse: [],
|
|
52
|
+
print: [],
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for(const search of [{parse: "language", print: "format"}]) {
|
|
56
|
+
for(const [actionType, criterion] of Object.entries(search)) {
|
|
57
|
+
filteredActions[actionType] = actionDefinitions[actionType].filter(
|
|
58
|
+
(a) => a.action.meta[criterion] === validatedConfig[criterion],
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const matches = []
|
|
64
|
+
// Now let us find the ones that agree to a contract
|
|
65
|
+
for(const printer of filteredActions.print) {
|
|
66
|
+
for(const parser of filteredActions.parse) {
|
|
67
|
+
const satisfied = schemaCompare(parser.contract, printer.contract)
|
|
68
|
+
|
|
69
|
+
if(satisfied.status === "success")
|
|
70
|
+
matches.push({parse: parser, print: printer})
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// We only want one!
|
|
75
|
+
if(matches.length > 1) {
|
|
76
|
+
const message =
|
|
77
|
+
`Multiple matching actions found: ` +
|
|
78
|
+
`${matches.map((m) => m.print.name).join(", ")}`
|
|
79
|
+
throw new Error(message)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
debug("Found matching actions: `%o`", 3, matches)
|
|
83
|
+
|
|
84
|
+
const chosenActions = matches[0]
|
|
85
|
+
|
|
86
|
+
if(Object.values(chosenActions).some(a => !a))
|
|
87
|
+
throw new Error("No found matching parser and printer")
|
|
88
|
+
|
|
89
|
+
const satisfied = schemaCompare(
|
|
90
|
+
chosenActions.parse.contract,
|
|
91
|
+
chosenActions.print.contract,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if(satisfied.status === "error") {
|
|
95
|
+
instance.logger.error(
|
|
96
|
+
`[Core.new] action contract failed: ${satisfied.errors}`,
|
|
97
|
+
)
|
|
98
|
+
throw new AggregateError(satisfied.errors, "Action contract failed")
|
|
99
|
+
} else if(satisfied.status !== "success") {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`[Core.new] Action contract failed: ${satisfied.message}`,
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
debug("Contracts satisfied between parser and printer", 2)
|
|
106
|
+
|
|
107
|
+
// Adding to instance
|
|
108
|
+
debug("Attaching parse action to instance: `%o`", 2, chosenActions.parse.module)
|
|
109
|
+
instance.parser = new ParseManager(chosenActions.parse, instance.logger)
|
|
110
|
+
|
|
111
|
+
debug("Attaching print action to instance: `%o`", 2, chosenActions.print.module)
|
|
112
|
+
instance.printer = new PrintManager(chosenActions.print, instance.logger)
|
|
113
|
+
|
|
114
|
+
// Setup and attach hooks
|
|
115
|
+
for(const target of [
|
|
116
|
+
{manager: instance.parser, action: "parse"},
|
|
117
|
+
{manager: instance.printer, action: "print"},
|
|
118
|
+
]) {
|
|
119
|
+
if(validatedConfig.hooks) {
|
|
120
|
+
const {manager, action} = target
|
|
121
|
+
const hooks = await HooksManager.new({
|
|
122
|
+
action: action,
|
|
123
|
+
hooksFile: validatedConfig.hooks,
|
|
124
|
+
logger: new Logger(instance.debugOptions),
|
|
125
|
+
timeout: validatedConfig.hooksTimeout,
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
if(hooks)
|
|
129
|
+
manager.hooks = hooks
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return instance
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async processFiles(glob, startTime = process.hrtime()) {
|
|
137
|
+
const debug = this.logger.newDebug()
|
|
138
|
+
debug("Starting file processing with conveyor", 1)
|
|
139
|
+
|
|
140
|
+
const {output} = this.options
|
|
141
|
+
|
|
142
|
+
const input = await getFiles(glob)
|
|
143
|
+
if(!input?.length)
|
|
144
|
+
throw new Error("No input files specified")
|
|
145
|
+
|
|
146
|
+
// Instantiate the conveyor
|
|
147
|
+
const conveyor = new Conveyor(
|
|
148
|
+
this.parser,
|
|
149
|
+
this.printer,
|
|
150
|
+
this.logger,
|
|
151
|
+
output,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
const processStart = process.hrtime()
|
|
155
|
+
|
|
156
|
+
// Initiate the conveyor
|
|
157
|
+
const result = await conveyor.convey(input, this.options.maxConcurrent)
|
|
158
|
+
|
|
159
|
+
debug("Conveyor complete", 1)
|
|
160
|
+
|
|
161
|
+
const endTime = (process.hrtime(startTime)[1] / 1_000_000).toFixed(2)
|
|
162
|
+
const processEnd = (process.hrtime(processStart)[1] / 1_000_000).toFixed(2)
|
|
163
|
+
|
|
164
|
+
// Grab the results
|
|
165
|
+
const totalFiles = input.length
|
|
166
|
+
const errored = result.errored
|
|
167
|
+
const succeeded = result.succeeded
|
|
168
|
+
|
|
169
|
+
const message = `Processed ${totalFiles} files: ${succeeded.length} succeeded, ${errored.length} errored ` +
|
|
170
|
+
`in ${processEnd}ms [total: ${endTime}ms]`
|
|
171
|
+
|
|
172
|
+
this.logger.debug(message, 1)
|
|
173
|
+
|
|
174
|
+
if(errored. length > 0) {
|
|
175
|
+
const failureRate = ((errored.length / totalFiles) * 100).toFixed(2)
|
|
176
|
+
const errorMessage = `Errors processing ${errored.length} files [${failureRate}%]` +
|
|
177
|
+
errored.map(r => `\n- ${r.file.module}: ${r.result.message}`).join("")
|
|
178
|
+
|
|
179
|
+
this.logger.error(errorMessage)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
debug("File processing complete", 1)
|
|
183
|
+
|
|
184
|
+
return result
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// import {process} from "node:process"
|
|
2
|
+
import yaml from "yaml"
|
|
3
|
+
import {execSync} from "child_process"
|
|
4
|
+
|
|
5
|
+
import * as FDUtil from "./util/FDUtil.js"
|
|
6
|
+
import * as ActionUtil from "./util/ActionUtil.js"
|
|
7
|
+
import * as DataUtil from "./util/DataUtil.js"
|
|
8
|
+
import * as ValidUtil from "./util/ValidUtil.js"
|
|
9
|
+
|
|
10
|
+
const {ls, resolveDirectory, resolveFilename, getFiles} = FDUtil
|
|
11
|
+
const {actionTypes, actionMetaRequirements, loadJson} = ActionUtil
|
|
12
|
+
const {isType} = DataUtil
|
|
13
|
+
const {assert} = ValidUtil
|
|
14
|
+
|
|
15
|
+
let debug
|
|
16
|
+
|
|
17
|
+
export default class Discovery {
|
|
18
|
+
#logger
|
|
19
|
+
|
|
20
|
+
constructor(core) {
|
|
21
|
+
this.core = core
|
|
22
|
+
this.#logger = core.logger
|
|
23
|
+
debug = this.#logger.newDebug()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Discover actions from local or global node_modules
|
|
28
|
+
*
|
|
29
|
+
* @returns {Promise<object>} A map of discovered modules
|
|
30
|
+
*/
|
|
31
|
+
async discoverActions() {
|
|
32
|
+
const bucket = []
|
|
33
|
+
const options = this.core.options ?? {}
|
|
34
|
+
|
|
35
|
+
if(options?.mockPath) {
|
|
36
|
+
debug("Discovering mock actions in `%s`", 1, options.mockPath)
|
|
37
|
+
|
|
38
|
+
bucket.push(
|
|
39
|
+
...(await getFiles([
|
|
40
|
+
`${options.mockPath}/bedoc-*-printer.js`,
|
|
41
|
+
`${options.mockPath}/bedoc-*-parser.js`,
|
|
42
|
+
])),
|
|
43
|
+
)
|
|
44
|
+
} else {
|
|
45
|
+
debug("Discovering actions", 2)
|
|
46
|
+
|
|
47
|
+
for(const actionType of actionTypes) {
|
|
48
|
+
if(this.core.packageJson[actionType]) {
|
|
49
|
+
const action = this.core.packageJson[actionType]
|
|
50
|
+
|
|
51
|
+
debug("Found action in package.json: %o", 3, action)
|
|
52
|
+
|
|
53
|
+
bucket.push(action)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const directories = [
|
|
58
|
+
// "c:/temp",
|
|
59
|
+
"./node_modules",
|
|
60
|
+
execSync("npm root -g").toString().trim(),
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
const moduleDirectories = directories.map(resolveDirectory)
|
|
64
|
+
for(const moduleDirectory of moduleDirectories) {
|
|
65
|
+
const {directories: dirs} = await ls(moduleDirectory.absolutePath)
|
|
66
|
+
debug("Found %d directories in `%s`", 2, dirs.length, moduleDirectory.absolutePath)
|
|
67
|
+
const bedocDirs = dirs.filter((d) => d.name.startsWith("bedoc-"))
|
|
68
|
+
const exports = bedocDirs.map((d) => this.#getModuleExports(d))
|
|
69
|
+
bucket.push(...exports.flat())
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return await this.#loadActionsAndContracts(bucket)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get the exports from a module's package.json file, resolved to file paths
|
|
78
|
+
*
|
|
79
|
+
* @param {object} dirMap The directory map object
|
|
80
|
+
* @returns {object[]} The discovered module exports
|
|
81
|
+
*/
|
|
82
|
+
#getModuleExports(dirMap) {
|
|
83
|
+
const packageJsonFile = resolveFilename("package.json", dirMap)
|
|
84
|
+
const packageJson = loadJson(packageJsonFile)
|
|
85
|
+
const bedocPackageJsonModules = packageJson.bedoc?.modules ?? []
|
|
86
|
+
const bedocModuleFiles = bedocPackageJsonModules.map((file) =>
|
|
87
|
+
resolveFilename(file, dirMap),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return bedocModuleFiles
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Process the discovered file objects and return the action and their
|
|
95
|
+
* respective contracts.
|
|
96
|
+
*
|
|
97
|
+
* @param {object[]} moduleFiles The module file objects to process
|
|
98
|
+
* @returns {Promise<object>} The discovered action
|
|
99
|
+
*/
|
|
100
|
+
async #loadActionsAndContracts(moduleFiles) {
|
|
101
|
+
const resultActions = {}
|
|
102
|
+
|
|
103
|
+
actionTypes.forEach((actionType) => (resultActions[actionType] = []))
|
|
104
|
+
|
|
105
|
+
for(const moduleFile of moduleFiles) {
|
|
106
|
+
const result = {total: 0, accepted: 0}
|
|
107
|
+
const {actions, contracts} = await import(moduleFile.absoluteUri)
|
|
108
|
+
|
|
109
|
+
debug("Loaded actions from `%s`", 2, moduleFile.absoluteUri)
|
|
110
|
+
debug("Found %d actions and %d contracts", 3, actions.length, contracts.length)
|
|
111
|
+
|
|
112
|
+
assert(
|
|
113
|
+
actions.length === contracts.length,
|
|
114
|
+
"Actions and contracts must be the same length",
|
|
115
|
+
1,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
result.total = actions.length
|
|
119
|
+
|
|
120
|
+
for(let i = actions.length; i--; ) {
|
|
121
|
+
const tempContract = contracts[i]
|
|
122
|
+
|
|
123
|
+
if(isType(tempContract, "string"))
|
|
124
|
+
contracts[i] = yaml.parse(tempContract)
|
|
125
|
+
else if(isType(tempContract, "object"))
|
|
126
|
+
contracts[i] = tempContract
|
|
127
|
+
else
|
|
128
|
+
throw new Error(`Invalid contract type: ${typeof tempContract}`)
|
|
129
|
+
|
|
130
|
+
const curr = {
|
|
131
|
+
module: moduleFile.module,
|
|
132
|
+
action: actions[i],
|
|
133
|
+
contract: contracts[i],
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const meta = curr.action.meta
|
|
137
|
+
const metaAction = meta?.action
|
|
138
|
+
|
|
139
|
+
debug("Checking action `%s`", 2, metaAction)
|
|
140
|
+
|
|
141
|
+
for(const actionType of actionTypes) {
|
|
142
|
+
const isValid = this.validMeta(actionType, curr)
|
|
143
|
+
debug("Action `%o` in `%s` is %s", 3, metaAction, moduleFile.module, isValid ? "valid" : "invalid")
|
|
144
|
+
|
|
145
|
+
if(isValid && metaAction === actionType) {
|
|
146
|
+
debug("Action is a valid `%s` action", 3, actionType)
|
|
147
|
+
result.accepted++
|
|
148
|
+
resultActions[actionType].push(curr)
|
|
149
|
+
continue
|
|
150
|
+
} else {
|
|
151
|
+
debug("Action is not a valid `%s` action", 3, actionType)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
debug("Processed action `%s`", 2, metaAction)
|
|
156
|
+
debug("Result: %d/%d actions accepted", 3, result.accepted, result.total)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
debug("Processed %d actions from `%s`", 2, result.total, moduleFile.module)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
for(const actionType of actionTypes) {
|
|
163
|
+
const total = resultActions[actionType].length
|
|
164
|
+
debug("Found %d `%s` actions", 2, total, actionType)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const total = Object.keys(resultActions).reduce((acc, curr) => {
|
|
168
|
+
return acc + resultActions[curr].length
|
|
169
|
+
}, 0)
|
|
170
|
+
|
|
171
|
+
debug("Loaded %d action definitions from %d modules", 2, total, moduleFiles.length)
|
|
172
|
+
|
|
173
|
+
return resultActions
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
validMeta(actionType, toValidate) {
|
|
177
|
+
debug("Checking meta requirements for `%s`", 3, actionType)
|
|
178
|
+
const requirements = actionMetaRequirements[actionType]
|
|
179
|
+
if(!requirements)
|
|
180
|
+
throw new Error(
|
|
181
|
+
`No meta requirements found for action type \`${actionType}\``,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
for(const requirement of requirements) {
|
|
185
|
+
debug("Checking requirement %o", 4, requirement)
|
|
186
|
+
|
|
187
|
+
if(isType(requirement, "object")) {
|
|
188
|
+
for(const [key, value] of Object.entries(requirement)) {
|
|
189
|
+
debug("Checking object requirement %o", 4, {key, value})
|
|
190
|
+
|
|
191
|
+
if(toValidate.action.meta[key] !== value)
|
|
192
|
+
return false
|
|
193
|
+
|
|
194
|
+
debug("Requirement met: %o", 4, {key, value})
|
|
195
|
+
}
|
|
196
|
+
} else if(isType(requirement, "string")) {
|
|
197
|
+
debug("Checking string requirement: %s", 4, requirement)
|
|
198
|
+
|
|
199
|
+
if(!toValidate.action.meta[requirement])
|
|
200
|
+
return false
|
|
201
|
+
|
|
202
|
+
debug("Requirement met: %s", 4, requirement)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return true
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import {setTimeout as timeoutPromise} from "timers/promises"
|
|
2
|
+
import * as DataUtil from "./util/DataUtil.js"
|
|
3
|
+
import * as ValidUtil from "./util/ValidUtil.js"
|
|
4
|
+
|
|
5
|
+
const {isEmpty, isType, allocateObject} = DataUtil
|
|
6
|
+
const {assert} = ValidUtil
|
|
7
|
+
|
|
8
|
+
const freeze = Object.freeze
|
|
9
|
+
|
|
10
|
+
const hookEvents = freeze(["start", "section_load", "enter", "exit", "end"])
|
|
11
|
+
const hookPoints = freeze(
|
|
12
|
+
await allocateObject(
|
|
13
|
+
hookEvents.map((event) => event.toUpperCase()),
|
|
14
|
+
hookEvents,
|
|
15
|
+
),
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
class HooksManager {
|
|
19
|
+
#hooksFile = null
|
|
20
|
+
#log = null
|
|
21
|
+
#hooks = {}
|
|
22
|
+
#action = null
|
|
23
|
+
#timeout = 1
|
|
24
|
+
|
|
25
|
+
constructor({action, hooksFile, logger, timeOut: timeout}) {
|
|
26
|
+
this.#action = action
|
|
27
|
+
this.#hooksFile = hooksFile
|
|
28
|
+
this.#log = logger
|
|
29
|
+
this.#timeout = timeout
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get action() {
|
|
33
|
+
return this.#action
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get hooksFile() {
|
|
37
|
+
return this.#hooksFile
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get hooks() {
|
|
41
|
+
return this.#hooks
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get log() {
|
|
45
|
+
return this.#log
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get timeout() {
|
|
49
|
+
return this.#timeout
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static async new(arg) {
|
|
53
|
+
const instance = new HooksManager(arg)
|
|
54
|
+
const debug = instance.log.newDebug()
|
|
55
|
+
|
|
56
|
+
debug("Creating new HooksManager instance with args: `%o`", 2, arg)
|
|
57
|
+
|
|
58
|
+
const hooksFile = instance.hooksFile
|
|
59
|
+
|
|
60
|
+
debug("Loading hooks from `%s", 2, hooksFile.absoluteUri)
|
|
61
|
+
|
|
62
|
+
debug("Checking hooks file exists: %j", 2, hooksFile)
|
|
63
|
+
const hooksFileContent = await import(hooksFile.absoluteUri)
|
|
64
|
+
|
|
65
|
+
debug("Hooks file loaded successfully", 2)
|
|
66
|
+
|
|
67
|
+
if(!hooksFileContent)
|
|
68
|
+
throw new Error(`Hooks file is empty: ${hooksFile.absoluteUri}`)
|
|
69
|
+
|
|
70
|
+
const hooks = hooksFileContent.default || hooksFileContent.Hooks
|
|
71
|
+
|
|
72
|
+
if(!hooks)
|
|
73
|
+
throw new Error(`\`${hooksFile.absoluteUri}\` contains no hooks.`)
|
|
74
|
+
|
|
75
|
+
const hooksObj = hooks[instance.action]
|
|
76
|
+
if(isEmpty(hooksObj))
|
|
77
|
+
return null
|
|
78
|
+
|
|
79
|
+
debug("Hooks found for action: `%s`", 2, instance.action)
|
|
80
|
+
|
|
81
|
+
if(!hooksObj)
|
|
82
|
+
return null
|
|
83
|
+
|
|
84
|
+
hooksObj.log = instance.log
|
|
85
|
+
instance.#hooks = hooksObj
|
|
86
|
+
|
|
87
|
+
debug("Hooks loaded successfully for `%s`", 2, instance.action)
|
|
88
|
+
|
|
89
|
+
return instance
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Trigger a hook
|
|
94
|
+
*
|
|
95
|
+
* @param {string} event - The type of hook to trigger
|
|
96
|
+
* @param {...any} args - The hook arguments
|
|
97
|
+
* @returns {Promise<any>} The result of the hook
|
|
98
|
+
*/
|
|
99
|
+
async on(event, ...args) {
|
|
100
|
+
const debug = this.log.newDebug()
|
|
101
|
+
|
|
102
|
+
debug("Triggering hook for event `%s`", 4, event)
|
|
103
|
+
|
|
104
|
+
if(!event)
|
|
105
|
+
throw new Error("Event type is required for hook invocation")
|
|
106
|
+
|
|
107
|
+
if(!hookEvents.includes(event))
|
|
108
|
+
throw new Error(`[HookManager.on] Invalid event type: ${event}`)
|
|
109
|
+
|
|
110
|
+
const hook = this.hooks[event]
|
|
111
|
+
|
|
112
|
+
if(hook) {
|
|
113
|
+
assert(
|
|
114
|
+
isType(hook, "function"),
|
|
115
|
+
`[HookManager.on] Hook "${event}" is not a function`,
|
|
116
|
+
1,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
const hookExecution = await hook.call(this, ...args)
|
|
120
|
+
const hookTimeout = this.parent.timeout
|
|
121
|
+
const expireAsync = () =>
|
|
122
|
+
timeoutPromise(
|
|
123
|
+
hookTimeout,
|
|
124
|
+
new Error(`Hook execution exceeded timeout of ${hookTimeout}ms`),
|
|
125
|
+
)
|
|
126
|
+
const result = await Promise.race([hookExecution, expireAsync()])
|
|
127
|
+
|
|
128
|
+
if(result?.status === "error")
|
|
129
|
+
throw result.error
|
|
130
|
+
|
|
131
|
+
debug("Hook executed successfully for event: `%s`", 4, event)
|
|
132
|
+
|
|
133
|
+
return result
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export {
|
|
139
|
+
// Class
|
|
140
|
+
HooksManager,
|
|
141
|
+
// Constants
|
|
142
|
+
hookPoints,
|
|
143
|
+
}
|