@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.
@@ -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
+ }