@gesslar/bedoc 1.0.2 → 1.2.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/README.md CHANGED
@@ -1,13 +1,13 @@
1
- # BeDoc
2
-
3
1
  [![CodeQL Advanced](https://github.com/gesslar/BeDoc/actions/workflows/codeql.yml/badge.svg)](https://github.com/gesslar/BeDoc/actions/workflows/codeql.yml)
4
2
  [![Dependabot Updates](https://github.com/gesslar/BeDoc/actions/workflows/dependabot/dependabot-updates/badge.svg)](https://github.com/gesslar/BeDoc/actions/workflows/dependabot/dependabot-updates)
5
3
  [![Auto PR and Merge - dev 🤗](https://github.com/gesslar/BeDoc/actions/workflows/autopr-dev.yml/badge.svg?branch=dev)](https://github.com/gesslar/BeDoc/actions/workflows/autopr-dev.yml)
6
4
 
7
-
8
5
  # BeDoc
9
6
 
10
- **BeDoc** is a powerful, pluggable documentation generator designed to handle any programming language and output format. With its extensible framework, you can easily create custom parsers and printers to generate structured documentation for your projects.
7
+ **BeDoc** is a powerful, pluggable documentation generator designed to handle
8
+ any programming language and output format. With its extensible framework, you
9
+ can easily create custom parsers and printers to generate structured
10
+ documentation for your projects.
11
11
 
12
12
  ---
13
13
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gesslar/bedoc",
3
- "version": "1.0.2",
3
+ "version": "1.2.0",
4
4
  "description": "Pluggable documentation engine for any language and format",
5
5
  "publisher": "gesslar",
6
6
  "main": "./src/core/Core.js",
@@ -27,7 +27,6 @@
27
27
  "globby": "^14.0.2",
28
28
  "micromatch": "^4.0.8",
29
29
  "node-fetch": "^3.3.2",
30
- "vscode-uri": "^3.0.8",
31
30
  "yaml": "^2.7.0"
32
31
  },
33
32
  "devDependencies": {
package/src/cli.js CHANGED
@@ -63,7 +63,7 @@ const {resolveDirectory} = FDUtil
63
63
  }
64
64
 
65
65
  // Create core instance with validated config
66
- BeDoc
66
+ const bedoc = await BeDoc
67
67
  .new({
68
68
  options: {
69
69
  ...optionsWithSources,
@@ -72,19 +72,20 @@ const {resolveDirectory} = FDUtil
72
72
  },
73
73
  source: Environment.CLI
74
74
  })
75
- } catch(e) {
76
- if(e instanceof Error) {
77
- if(e instanceof AggregateError) {
78
- for(const error of e.errors) {
79
- console.error(`Error: ${error.message}`)
80
- }
81
- } else if(e.stack) {
82
- console.error(e.stack)
75
+ const filesToProcess = bedoc.options.input.map(f => f.absolutePath)
76
+ const result = await bedoc.processFiles(filesToProcess)
77
+ const errored = result.errored
78
+ if(errored.length > 0)
79
+ throw new AggregateError(errored.map(e => e.error), "Error processing files")
80
+ } catch(error) {
81
+ if(error instanceof Error) {
82
+ if(error instanceof AggregateError) {
83
+ error.errors.forEach(e => console.error(e))
83
84
  } else {
84
- console.error(`Error: ${e.message}`)
85
+ console.error(error.message, error.stack)
85
86
  }
86
87
  } else {
87
- console.error(`Error: ${e}`)
88
+ console.error("Error: %o", error)
88
89
  }
89
90
 
90
91
  process.exit(1)
@@ -1,27 +1,26 @@
1
- import {hookPoints} from "./HooksManager.js"
1
+ import {hookPoints} from "./HookManager.js"
2
2
 
3
3
  export default class ActionManager {
4
4
  #action = null
5
- #meta = {}
6
- #hooks = null
5
+ #hookManager = null
7
6
  #contract
8
- #module
9
7
  #log
10
8
  #debug
9
+ #file
11
10
 
12
11
  constructor(actionDefinition, logger) {
13
12
  this.#log = logger
14
13
  this.#debug = this.#log.newDebug()
15
14
 
16
- this.#setupAction(actionDefinition)
15
+ this.#initialize(actionDefinition)
17
16
  }
18
17
 
19
- #setupAction(actionDefinition) {
18
+ #initialize(actionDefinition) {
20
19
  const debug = this.#debug
21
20
 
22
21
  debug("Setting up action", 2)
23
22
 
24
- const {action, contract, module, meta} = actionDefinition
23
+ const {action, file, contract} = actionDefinition
25
24
 
26
25
  if(!action)
27
26
  throw new Error("Action is required")
@@ -29,13 +28,9 @@ export default class ActionManager {
29
28
  if(!contract)
30
29
  throw new Error("Contract is required")
31
30
 
32
- if(!module)
33
- throw new Error("Module is required")
34
-
35
- this.#module = module
36
31
  this.#action = action
37
32
  this.#contract = contract
38
- this.#meta = meta
33
+ this.#file = file
39
34
 
40
35
  debug("Action setup complete", 2)
41
36
  }
@@ -44,33 +39,101 @@ export default class ActionManager {
44
39
  return this.#action
45
40
  }
46
41
 
47
- get hooks() {
48
- return this.#hooks
42
+ get hookManager() {
43
+ return this.#hookManager
49
44
  }
50
45
 
51
- set hooks(hookManager) {
52
- if(this.hooks)
46
+ set hookManager(hookManager) {
47
+ if(this.hookManager)
53
48
  throw new Error("Hooks already set")
54
49
 
55
50
  this.action.hook = hookManager.on.bind(this.action)
56
51
  this.action.HOOKS = hookPoints
52
+ this.#hookManager = hookManager
57
53
  this.action.hooks = hookManager.hooks
58
- this.#hooks = hookManager
59
54
  }
60
55
 
61
56
  get contract() {
62
57
  return this.#contract
63
58
  }
64
59
 
65
- get module() {
66
- return this.#module
67
- }
68
-
69
60
  get meta() {
70
- return this.#meta
61
+ return this.#action.meta
71
62
  }
72
63
 
73
64
  get log() {
74
65
  return this.#log
75
66
  }
67
+
68
+ async #setupAction() {
69
+ const setup = this.action?.setup
70
+
71
+ if(!setup)
72
+ return
73
+
74
+ await this.action.setup.call(
75
+ this.action, {parent: this, log: this.#log}
76
+ )
77
+ }
78
+
79
+ async #cleanupAction() {
80
+ const cleanup = this.action?.cleanup
81
+
82
+ if(!cleanup)
83
+ return
84
+
85
+ await this.action.cleanup.call(this.action)
86
+ }
87
+
88
+ async #setupHooks() {
89
+ const setup = this.hookManager?.setup
90
+
91
+ if(!setup)
92
+ return
93
+
94
+ await this.hookManager.setup.call(
95
+ this.hookManager.hooks, {parent: this.action, log: this.#log}
96
+ )
97
+ }
98
+
99
+ async #cleanupHooks() {
100
+ const cleanup = this.hookManager?.cleanup
101
+
102
+ if(!cleanup)
103
+ return
104
+
105
+ await this.hookManager.cleanup.call(this.hookManager.hooks)
106
+ }
107
+
108
+ async setupAction() {
109
+ this.#debug("Setting up action for %s", 2, this.meta.action)
110
+
111
+ await this.#setupHooks()
112
+ await this.#setupAction()
113
+ }
114
+
115
+ async runAction({file,content}) {
116
+ const func = this.action.run
117
+
118
+ if(!func)
119
+ throw new Error(`No \`run\` function found for action \`${this.meta.action}\``)
120
+
121
+ const actionResult = await func.call(
122
+ this.action, {module: file.module, content}
123
+ )
124
+
125
+ return actionResult
126
+ }
127
+
128
+ async cleanupAction() {
129
+ this.#debug("Post action", 2)
130
+ this.#debug("Cleaning up action for %s", 2, this.meta.action)
131
+
132
+ await this.#cleanupHooks()
133
+ await this.#cleanupAction()
134
+ }
135
+
136
+ toString() {
137
+ return `${this.#file?.module || "UNDEFINED"} (${this.meta?.action || "UNDEFINED"})`
138
+ }
76
139
  }
@@ -159,6 +159,12 @@ export default class Configuration {
159
159
  options.packageJson = {value: jsonObj, source}
160
160
  }
161
161
 
162
+ // Add defaults which are missing
163
+ for(const [key, param] of Object.entries(ConfigurationParameters)) {
164
+ if(options[key] === undefined && param.default !== undefined)
165
+ options[key] = {value: param.default, source: "default"}
166
+ }
167
+
162
168
  return options
163
169
  }
164
170
 
@@ -6,9 +6,9 @@ export default class Conveyor {
6
6
  #succeeded = []
7
7
  #errored = []
8
8
 
9
- constructor(parser, printer, logger, output) {
10
- this.parser = parser
11
- this.printer = printer
9
+ constructor(parse, print, logger, output) {
10
+ this.parse = parse
11
+ this.print = print
12
12
  this.logger = logger
13
13
  this.output = output
14
14
  }
@@ -23,11 +23,14 @@ export default class Conveyor {
23
23
  async convey(files, maxConcurrent = 10) {
24
24
  const semaphore = Array(maxConcurrent).fill(Promise.resolve())
25
25
 
26
+ // Set up the actions
27
+ await this.parse.setupAction()
28
+ await this.print.setupAction()
29
+
26
30
  for(const file of files) {
27
31
  const slot = Promise.race(semaphore) // Wait for an available slot
28
32
  semaphore.push(slot.then(async() => {
29
33
  const result = await this.#processFile(file)
30
-
31
34
  if(result.status === "success")
32
35
  this.#succeeded.push({input: file, output: result.file})
33
36
  else
@@ -39,6 +42,10 @@ export default class Conveyor {
39
42
  // Wait for all tasks to complete
40
43
  await Promise.all(semaphore)
41
44
 
45
+ // Clean up actions
46
+ await this.parse.cleanupAction()
47
+ await this.print.cleanupAction()
48
+
42
49
  return {succeeded: this.#succeeded, errored: this.#errored}
43
50
  }
44
51
 
@@ -50,6 +57,7 @@ export default class Conveyor {
50
57
  */
51
58
  async #processFile(file) {
52
59
  const debug = this.logger.newDebug()
60
+ const {parse, print} = this
53
61
 
54
62
  try {
55
63
  debug("Processing file: `%s`", 2, file.path)
@@ -59,17 +67,20 @@ export default class Conveyor {
59
67
  debug("Read file content `%s` (%d bytes)", 2, file.path, fileContent.length)
60
68
 
61
69
  // Step 2: Parse file
62
- const parseResult = await this.parser.parse(file, fileContent)
70
+ const parseResult = await parse.runAction({
71
+ file,
72
+ content: fileContent
73
+ })
63
74
  if(parseResult.status === "error")
64
75
  return parseResult
65
76
 
66
77
  debug("Parsed file successfully: `%s`", 2, file.path)
67
78
 
68
79
  // Step 3: Print file
69
- const printResult = await this.printer.print(
80
+ const printResult = await print.runAction({
70
81
  file,
71
- parseResult.result,
72
- )
82
+ content: parseResult.result,
83
+ })
73
84
  if(printResult.status === "error")
74
85
  return printResult
75
86
 
@@ -87,9 +98,7 @@ export default class Conveyor {
87
98
 
88
99
  return writeResult
89
100
  } catch(error) {
90
- const mess = `Error processing file ${file.path}: ${error.message}\n${error.stack}`
91
- this.logger.error(mess)
92
- return {status: "error", error}
101
+ return {status: "error", file, error}
93
102
  }
94
103
  }
95
104
 
package/src/core/Core.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import process from "node:process"
2
2
 
3
3
  import Discovery from "./Discovery.js"
4
- import {HooksManager} from "./HooksManager.js"
4
+ import HookManager from "./HookManager.js"
5
5
  import Logger from "./Logger.js"
6
6
  import ParseManager from "./action/ParseManager.js"
7
7
  import PrintManager from "./action/PrintManager.js"
@@ -33,100 +33,85 @@ export default class Core {
33
33
  }
34
34
 
35
35
  static async new({options, source}) {
36
- const configuration = new Configuration()
36
+ const config = new Configuration()
37
37
 
38
- const validatedConfig = await configuration.validate({options, source})
39
- if(validatedConfig.status === "error")
40
- throw new AggregateError(validatedConfig.errors, "BeDoc configuration failed")
38
+ const validConfig = await config.validate({options, source})
39
+ if(validConfig.status === "error")
40
+ throw new AggregateError(validConfig.errors,"BeDoc configuration failed")
41
41
 
42
- const instance = new Core({...validatedConfig, name: "BeDoc"})
42
+ const instance = new Core({...validConfig, name: "BeDoc"})
43
43
  const debug = instance.logger.newDebug()
44
44
 
45
- debug("Creating new BeDoc instance with options: `%o`", 2, validatedConfig)
45
+ debug("Creating new BeDoc instance with options: `%o`", 2, validConfig)
46
46
 
47
47
  const discovery = new Discovery(instance)
48
- const actionDefinitions = await discovery.discoverActions()
48
+ const {printer: validPrint, parser: validParse} = validConfig
49
49
 
50
- const filteredActions = {
51
- parse: [],
52
- print: [],
53
- }
50
+ const actionDefs = await discovery.discoverActions({
51
+ print: validPrint,
52
+ parse: validParse
53
+ })
54
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
- }
55
+ const validCrit = discovery.satisfyCriteria(actionDefs, validConfig)
56
+
57
+ debug("Actions that met criteria: `%o`", 2, validCrit)
62
58
 
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)
59
+ if(Object.values(validCrit).some(arr => arr.length === 0))
60
+ throw new Error("No found matching parser and printer")
68
61
 
69
- if(satisfied.status === "success")
70
- matches.push({parse: parser, print: printer})
62
+ const validSchemas = {print: [], parse: []}
63
+ let printers = validCrit.print.length
64
+ while(printers--) {
65
+ const printer = validCrit.print[printers]
66
+ const printerSchema = printer.contract
67
+ const satisfied = []
68
+ for(const parser of validCrit.parse) {
69
+ const parserSchema = parser.contract
70
+ const result = schemaCompare(parserSchema, printerSchema)
71
+ if(result.status === "success")
72
+ satisfied.push(parser)
71
73
  }
72
- }
73
74
 
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)
75
+ if(satisfied.length > 0) {
76
+ validSchemas.print.push(printer)
77
+ validSchemas.parse.push(...satisfied)
78
+ }
80
79
  }
81
80
 
82
- debug("Found matching actions: `%o`", 3, matches)
83
-
84
- const chosenActions = matches[0]
81
+ const finalActions = {}
82
+ for(const [key, value] of Object.entries(validSchemas)) {
83
+ if(value.length === 0)
84
+ throw new Error(`No matching ${key} found`)
85
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
- )
86
+ if(value.length > 1)
87
+ throw new Error(`Multiple matching ${key} found`)
93
88
 
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
- )
89
+ finalActions[key] = validSchemas[key][0]
103
90
  }
104
91
 
105
92
  debug("Contracts satisfied between parser and printer", 2)
106
93
 
107
94
  // 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,
95
+ instance.actions = {}
96
+ const managers = {print: PrintManager, parse: ParseManager}
97
+ for(const [, value] of Object.entries(finalActions)) {
98
+ const {action: actionType} = value.action.meta
99
+
100
+ debug("Attaching `%o` action to instance", 2, actionType)
101
+ instance.actions[actionType] = new managers[actionType](
102
+ value, instance.logger
103
+ )
104
+
105
+ if(validConfig.hooks) {
106
+ const hookManager = await HookManager.new({
107
+ action: actionType,
108
+ hooksFile: validConfig.hooks,
124
109
  logger: new Logger(instance.debugOptions),
125
- timeout: validatedConfig.hooksTimeout,
110
+ timeout: validConfig.hooksTimeout,
126
111
  })
127
112
 
128
- if(hooks)
129
- manager.hooks = hooks
113
+ if(hookManager)
114
+ instance.actions[actionType].hookManager = hookManager
130
115
  }
131
116
  }
132
117
 
@@ -135,6 +120,7 @@ export default class Core {
135
120
 
136
121
  async processFiles(glob, startTime = process.hrtime()) {
137
122
  const debug = this.logger.newDebug()
123
+
138
124
  debug("Starting file processing with conveyor", 1)
139
125
 
140
126
  const {output} = this.options
@@ -145,8 +131,8 @@ export default class Core {
145
131
 
146
132
  // Instantiate the conveyor
147
133
  const conveyor = new Conveyor(
148
- this.parser,
149
- this.printer,
134
+ this.actions.parse,
135
+ this.actions.print,
150
136
  this.logger,
151
137
  output,
152
138
  )
@@ -171,12 +157,16 @@ export default class Core {
171
157
 
172
158
  this.logger.debug(message, 1)
173
159
 
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("")
160
+ if(errored.length > 0) {
161
+ // const failureRate = ((errored.length / totalFiles) * 100).toFixed(2)
162
+ // const errorMessage =
163
+ // `Errors processing ${errored.length} files [${failureRate}%]`
164
+ // const errorLines = errored.map(r => {
165
+ // const stackLine = log.lastStackLine(r.error, 0)
166
+ // return `\n- ${r.input.module}: ${stackLine} - ${r.error.message}`
167
+ // }).join("")
178
168
 
179
- this.logger.error(errorMessage)
169
+ // this.logger(errorMessage+errorLines)
180
170
  }
181
171
 
182
172
  debug("File processing complete", 1)
@@ -5,30 +5,32 @@ import {execSync} from "child_process"
5
5
  import * as FDUtil from "./util/FDUtil.js"
6
6
  import * as ActionUtil from "./util/ActionUtil.js"
7
7
  import * as DataUtil from "./util/DataUtil.js"
8
- import * as ValidUtil from "./util/ValidUtil.js"
9
8
 
10
9
  const {ls, resolveDirectory, resolveFilename, getFiles} = FDUtil
11
10
  const {actionTypes, actionMetaRequirements, loadJson} = ActionUtil
12
11
  const {isType} = DataUtil
13
- const {assert} = ValidUtil
14
-
15
- let debug
16
12
 
17
13
  export default class Discovery {
18
14
  #logger
15
+ #debug
19
16
 
20
17
  constructor(core) {
21
18
  this.core = core
22
19
  this.#logger = core.logger
23
- debug = this.#logger.newDebug()
20
+ this.#debug = this.#logger.newDebug()
24
21
  }
25
22
 
26
23
  /**
27
24
  * Discover actions from local or global node_modules
28
25
  *
26
+ * @param {object[]} specified The specified actions to discover
29
27
  * @returns {Promise<object>} A map of discovered modules
30
28
  */
31
- async discoverActions() {
29
+ async discoverActions({print, parse} = {}) {
30
+ const debug = this.#debug
31
+
32
+ debug("Discovering actions", 2)
33
+
32
34
  const bucket = []
33
35
  const options = this.core.options ?? {}
34
36
 
@@ -42,35 +44,55 @@ export default class Discovery {
42
44
  ])),
43
45
  )
44
46
  } else {
45
- debug("Discovering actions", 2)
47
+ debug("Mock path not set, discovering actions in node_modules", 1)
46
48
 
47
- for(const actionType of actionTypes) {
48
- if(this.core.packageJson[actionType]) {
49
- const action = this.core.packageJson[actionType]
49
+ debug("Looking for actions in project's package.json", 2)
50
+ if(this.core.packageJson?.bedoc?.modules) {
51
+ const actions = this.core.packageJson?.bedoc?.modules
50
52
 
51
- debug("Found action in package.json: %o", 3, action)
53
+ debug("Found %d actions in package.json: %d", 3, actions)
54
+ debug("Actions found in package.json action in package.json: %o", 3, actions)
52
55
 
53
- bucket.push(action)
54
- }
56
+ if(actions && typeof(actions) === "object")
57
+ bucket.push(...actions)
58
+ else
59
+ debug("No actions found in package.json", 3)
60
+ } else {
61
+ debug("No actions found in project's package.json", 2)
55
62
  }
56
63
 
64
+ debug("Looking for actions in node_modules (global and locally installed", 2)
57
65
  const directories = [
58
- // "c:/temp",
59
66
  "./node_modules",
60
67
  execSync("npm root -g").toString().trim(),
61
68
  ]
62
69
 
70
+ debug("Found %d directories to search for actions", 2, directories.length)
71
+ debug("Directories to search for actions: %o", 3, directories)
72
+
63
73
  const moduleDirectories = directories.map(resolveDirectory)
64
74
  for(const moduleDirectory of moduleDirectories) {
65
75
  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))
76
+
77
+ debug("Found %d directories in `%s`", 2,
78
+ dirs.length, moduleDirectory.absolutePath
79
+ )
80
+
81
+ const bedocDirs = dirs.filter(d => d.name.startsWith("bedoc-"))
82
+ debug("Found %d bedoc directories under %s", 2, bedocDirs.length, moduleDirectory.absolutePath)
83
+
84
+ const exports = bedocDirs.map(d => this.#getModuleExports(d))
85
+ debug("Found %d module exports under %s", 2, exports.length, moduleDirectory.absolutePath)
86
+
69
87
  bucket.push(...exports.flat())
70
88
  }
71
89
  }
72
90
 
73
- return await this.#loadActionsAndContracts(bucket)
91
+ debug("Discovered %d actions", 2, bucket.length)
92
+
93
+ return await this.#loadActionsAndContracts(
94
+ bucket, {print: print, parse: parse}
95
+ )
74
96
  }
75
97
 
76
98
  /**
@@ -80,11 +102,18 @@ export default class Discovery {
80
102
  * @returns {object[]} The discovered module exports
81
103
  */
82
104
  #getModuleExports(dirMap) {
105
+ const debug = this.#debug
106
+ debug("Getting module exports from `%s`", 3, dirMap.absolutePath)
107
+
83
108
  const packageJsonFile = resolveFilename("package.json", dirMap)
109
+ debug("Loading package.json from `%s`", 3, packageJsonFile.absolutePath)
110
+
84
111
  const packageJson = loadJson(packageJsonFile)
112
+ debug("Loaded package.json from `%s`", 3, packageJsonFile.absolutePath)
113
+
85
114
  const bedocPackageJsonModules = packageJson.bedoc?.modules ?? []
86
- const bedocModuleFiles = bedocPackageJsonModules.map((file) =>
87
- resolveFilename(file, dirMap),
115
+ const bedocModuleFiles = bedocPackageJsonModules.map(file =>
116
+ resolveFilename(file, dirMap)
88
117
  )
89
118
 
90
119
  return bedocModuleFiles
@@ -95,68 +124,114 @@ export default class Discovery {
95
124
  * respective contracts.
96
125
  *
97
126
  * @param {object[]} moduleFiles The module file objects to process
127
+ * @param {object} specific The specific actions to load
98
128
  * @returns {Promise<object>} The discovered action
99
129
  */
100
- async #loadActionsAndContracts(moduleFiles) {
130
+ async #loadActionsAndContracts(moduleFiles, specific) {
131
+ const debug = this.#debug
132
+
133
+ debug("Loading actions and contracts", 2)
134
+ debug("Loading %d module files", 2, moduleFiles.length)
135
+ debug("Specific actions to load: %o", 2, specific)
136
+
101
137
  const resultActions = {}
138
+ actionTypes.forEach(actionType => (resultActions[actionType] = []))
139
+
140
+ // Tag the specific actions to load, so we can filter them later
141
+ for(const [type, file] of Object.entries(specific)) {
142
+ if(file) {
143
+ debug("Tagging specific action `%s` as `%s`", 3, file.absolutePath, type)
144
+ file.specificType = type
145
+ }
146
+ }
147
+
148
+ const toLoad = [
149
+ ...moduleFiles,
150
+ ...Object.values(specific).filter(Boolean),
151
+ ]
102
152
 
103
- actionTypes.forEach((actionType) => (resultActions[actionType] = []))
153
+ debug("Loading %d combined actions", 2, toLoad.length)
154
+ debug("Actions to load: %o", 3, toLoad)
104
155
 
105
- for(const moduleFile of moduleFiles) {
106
- const result = {total: 0, accepted: 0}
107
- const {actions, contracts} = await import(moduleFile.absoluteUri)
156
+ const loadedActions = []
157
+ for(const file of toLoad) {
158
+ debug("Loading module `%s`", 2, file.absolutePath)
108
159
 
109
- debug("Loaded actions from `%s`", 2, moduleFile.absoluteUri)
110
- debug("Found %d actions and %d contracts", 3, actions.length, contracts.length)
160
+ const loading = await this.#loadModule(file)
161
+ const loaded = loading.actions.map((action, index) => {
162
+ const contract = yaml.parse(loading.contracts[index])
163
+
164
+ return {file, action, contract}
165
+ })
166
+ loadedActions.push(...loaded)
167
+ }
111
168
 
112
- assert(
113
- actions.length === contracts.length,
114
- "Actions and contracts must be the same length",
115
- 1,
169
+ debug("Loaded %d actions", 2, loadedActions.length)
170
+
171
+ const filtered = []
172
+ for(const actionType of actionTypes) {
173
+ const file = specific[actionType]
174
+ const matchingActions = []
175
+ if(file) {
176
+ debug("Filtering actions for specific `%s`", 2, actionType)
177
+ const found = loadedActions.find(
178
+ e => e.file.absolutePath === file.absolutePath
179
+ )
180
+
181
+ if(!found)
182
+ throw new Error(`Could not find specific action: ${file.absolutePath}`)
183
+
184
+ matchingActions.push(found)
185
+ } else {
186
+ debug("No specific action required for `%s`", 2, actionType)
187
+
188
+ const found = loadedActions.filter(
189
+ e => e.action.meta.action === actionType
190
+ )
191
+ matchingActions.push(...found)
192
+ }
193
+
194
+ debug("Filtered %d actions for `%s`", 2,
195
+ matchingActions.length, actionType
116
196
  )
117
197
 
118
- result.total = actions.length
198
+ filtered.push(...matchingActions)
199
+ }
119
200
 
120
- for(let i = actions.length; i--; ) {
121
- const tempContract = contracts[i]
201
+ debug("Filtered %d actions", 2, filtered.length)
122
202
 
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}`)
203
+ // Now check the metas for validity
204
+ for(const e of filtered) {
205
+ const {action, contract, file: moduleFile} = e
206
+ const meta = action.meta
207
+ if(!meta)
208
+ throw new TypeError("Action has no meta object:\n" +
209
+ JSON.stringify(moduleFile, null, 2) + "\n" +
210
+ JSON.stringify(action, null, 2))
129
211
 
130
- const curr = {
131
- module: moduleFile.module,
132
- action: actions[i],
133
- contract: contracts[i],
134
- }
212
+ const metaAction = meta.action
213
+ if(!metaAction)
214
+ throw new TypeError("Action has no meta action:\n" +
215
+ JSON.stringify(moduleFile, null, 2) + "\n" +
216
+ JSON.stringify(action, null, 2))
135
217
 
136
- const meta = curr.action.meta
137
- const metaAction = meta?.action
218
+ debug("Checking action `%s`", 2, metaAction)
138
219
 
139
- debug("Checking action `%s`", 2, metaAction)
220
+ const isValid = this.#validMeta(metaAction, {action, contract})
140
221
 
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")
222
+ debug("Action `%o` in `%s` is %s", 3,
223
+ metaAction, moduleFile.module, isValid ? "valid" : "invalid"
224
+ )
144
225
 
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
- }
226
+ if(isValid) {
227
+ debug("Action is a valid `%s` action", 3, metaAction)
154
228
 
155
- debug("Processed action `%s`", 2, metaAction)
156
- debug("Result: %d/%d actions accepted", 3, result.accepted, result.total)
229
+ resultActions[metaAction].push({file: moduleFile, action, contract})
230
+ } else {
231
+ debug("Action is not a valid `%s` action", 3, metaAction)
157
232
  }
158
233
 
159
- debug("Processed %d actions from `%s`", 2, result.total, moduleFile.module)
234
+ debug("Processed action `%s`", 2, metaAction)
160
235
  }
161
236
 
162
237
  for(const actionType of actionTypes) {
@@ -168,13 +243,92 @@ export default class Discovery {
168
243
  return acc + resultActions[curr].length
169
244
  }, 0)
170
245
 
171
- debug("Loaded %d action definitions from %d modules", 2, total, moduleFiles.length)
246
+ debug("Loaded %d action definitions from %d modules", 2,
247
+ total, moduleFiles.length
248
+ )
172
249
 
173
250
  return resultActions
174
251
  }
175
252
 
176
- validMeta(actionType, toValidate) {
253
+ satisfyCriteria(actions, validatedConfig) {
254
+ const debug = this.#debug
255
+ const satisfied = {parse: [], print: []}
256
+ const toMatch = {
257
+ parse: {criterion: "language", config: "parser"},
258
+ print: {criterion: "format", config: "printer"}
259
+ }
260
+
261
+ debug("Satisfying criteria for actions", 2)
262
+ for(const [actionType, search] of Object.entries(toMatch)) {
263
+ debug("Satisfying criteria for `%s` actions", 2, actionType)
264
+
265
+ const {criterion, config} = search
266
+ debug("Criterion: %s, Config: %s", 3, criterion, config)
267
+
268
+ // First let's check if we wanted something specific
269
+ if(validatedConfig[config]) {
270
+ debug("Checking for specific `%s` action", 3, actionType)
271
+ const found = actions[actionType].find(
272
+ a => a.file.specificType === actionType
273
+ )
274
+ if(found) {
275
+ debug("Found specific `%s` action", 3, actionType)
276
+ satisfied[actionType].push(found)
277
+ continue
278
+ }
279
+
280
+ debug("No specific `%s` action found", 3, actionType)
281
+ }
282
+
283
+ // Hmm! We didn't find anything specific. Let's check the criterion
284
+ debug("Checking for `%s` actions with criterion `%s`", 3, actionType, criterion)
285
+ const found = actions[actionType].filter(a => {
286
+ debug("Meta criterion value: %o", 4, a.action.meta[criterion])
287
+ debug("Config criterion value: %o", 4, validatedConfig[criterion])
288
+ return a.action.meta[criterion] === validatedConfig[criterion]
289
+ })
290
+
291
+ debug("Found %d `%s` actions with criterion `%s`", 3,
292
+ found.length, actionType, criterion
293
+ )
294
+
295
+ // Shove them into the result!
296
+ satisfied[actionType].push(...found)
297
+
298
+ // That should about cover it!
299
+ }
300
+
301
+ return satisfied
302
+ }
303
+
304
+ /**
305
+ * Load a module and return its exports
306
+ *
307
+ * @param {object} module The module object to load
308
+ * @returns {Promise<object>} The module exports {actions, contracts}
309
+ */
310
+ async #loadModule(module) {
311
+ const debug = this.#debug
312
+
313
+ debug("Loading module `%j`", 2, module)
314
+
315
+ const {absoluteUri} = module
316
+ const moduleExports = await import(absoluteUri)
317
+
318
+ return {file: module, ...moduleExports}
319
+ }
320
+
321
+ /**
322
+ * Validates the meta requirements for an action
323
+ *
324
+ * @param {string} actionType The action type to validate
325
+ * @param {object} toValidate - The action object to validate
326
+ * @returns {boolean} Whether the action object meets the meta requirements
327
+ */
328
+ #validMeta(actionType, toValidate) {
329
+ const debug = this.#debug
177
330
  debug("Checking meta requirements for `%s`", 3, actionType)
331
+
178
332
  const requirements = actionMetaRequirements[actionType]
179
333
  if(!requirements)
180
334
  throw new Error(
@@ -8,17 +8,17 @@ const {assert} = ValidUtil
8
8
  const freeze = Object.freeze
9
9
 
10
10
  const hookEvents = freeze(["start", "section_load", "enter", "exit", "end"])
11
- const hookPoints = freeze(
11
+ export const hookPoints = freeze(
12
12
  await allocateObject(
13
13
  hookEvents.map((event) => event.toUpperCase()),
14
14
  hookEvents,
15
15
  ),
16
16
  )
17
17
 
18
- class HooksManager {
18
+ export default class HookManager {
19
19
  #hooksFile = null
20
20
  #log = null
21
- #hooks = {}
21
+ #hooks = null
22
22
  #action = null
23
23
  #timeout = 1
24
24
 
@@ -49,11 +49,19 @@ class HooksManager {
49
49
  return this.#timeout
50
50
  }
51
51
 
52
+ get setup() {
53
+ return this.hooks?.setup || null
54
+ }
55
+
56
+ get cleanup() {
57
+ return this.hooks?.cleanup || null
58
+ }
59
+
52
60
  static async new(arg) {
53
- const instance = new HooksManager(arg)
61
+ const instance = new HookManager(arg)
54
62
  const debug = instance.log.newDebug()
55
63
 
56
- debug("Creating new HooksManager instance with args: `%o`", 2, arg)
64
+ debug("Creating new HookManager instance with args: `%o`", 2, arg)
57
65
 
58
66
  const hooksFile = instance.hooksFile
59
67
 
@@ -116,7 +124,7 @@ class HooksManager {
116
124
  1,
117
125
  )
118
126
 
119
- const hookExecution = await hook.call(this, ...args)
127
+ const hookExecution = await hook.call(this.hooks, ...args)
120
128
  const hookTimeout = this.parent.timeout
121
129
  const expireAsync = () =>
122
130
  timeoutPromise(
@@ -134,10 +142,3 @@ class HooksManager {
134
142
  }
135
143
  }
136
144
  }
137
-
138
- export {
139
- // Class
140
- HooksManager,
141
- // Constants
142
- hookPoints,
143
- }
@@ -119,8 +119,8 @@ export default class Logger {
119
119
  return `[${this.#name}] ${loggerColours[level]}${tag}${loggerColours.reset}: ${message}`
120
120
  }
121
121
 
122
- lastStackLine(stepsRemoved = 3) {
123
- const stack = ErrorStackParser.parse(new Error())
122
+ lastStackLine(error = new Error(), stepsRemoved = 3) {
123
+ const stack = ErrorStackParser.parse(error)
124
124
  return stack[stepsRemoved]
125
125
  }
126
126
 
@@ -4,23 +4,4 @@ export default class ParseManager extends ActionManager {
4
4
  constructor(actionDefinition, logger) {
5
5
  super(actionDefinition, logger)
6
6
  }
7
-
8
- async parse(fileMap, content) {
9
- const log = this.log
10
- const debug = log.newDebug()
11
-
12
- debug("Parsing file `%j`", 3, fileMap)
13
-
14
- if(this.action.init)
15
- this.action.init({parent: this, log})
16
-
17
- if(!this.action.parse)
18
- throw new Error(`No parse function found for action: ${this.module}`)
19
-
20
- const result = await this.action.parse(fileMap.path, content)
21
-
22
- debug("Parse complete of file `%j`", 3, fileMap)
23
-
24
- return result
25
- }
26
7
  }
@@ -4,23 +4,4 @@ export default class PrintManager extends ActionManager {
4
4
  constructor(actionDefinition, logger) {
5
5
  super(actionDefinition, logger)
6
6
  }
7
-
8
- async print(fileMap, content) {
9
- const log = this.log
10
- const debug = log.newDebug()
11
-
12
- debug("Printing data for `%s`", 3, fileMap.module)
13
-
14
- if(this.action.init)
15
- this.action.init({parent: this, log})
16
-
17
- if(!this.action.print)
18
- throw new Error(`No print function found for action: ${this.module}`)
19
-
20
- const result = await this.action.print(fileMap.module, content)
21
-
22
- debug("Print complete for `%s`", 3, fileMap.module)
23
-
24
- return result
25
- }
26
7
  }