@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
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import process from "node:process"
|
|
2
|
+
import {Environment} from "./Core.js"
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
ConfigurationParameters,
|
|
6
|
+
ConfigurationPriorityKeys,
|
|
7
|
+
} from "./ConfigurationParameters.js"
|
|
8
|
+
|
|
9
|
+
import * as ActionUtil from "./util/ActionUtil.js"
|
|
10
|
+
import * as DataUtil from "./util/DataUtil.js"
|
|
11
|
+
import * as FDUtil from "./util/FDUtil.js"
|
|
12
|
+
|
|
13
|
+
const {loadJson} = ActionUtil
|
|
14
|
+
const {isNothing, isType, mapObject} = DataUtil
|
|
15
|
+
const {getFiles, resolveDirectory, resolveFilename} = FDUtil
|
|
16
|
+
const {fdType, fdTypes} = FDUtil
|
|
17
|
+
|
|
18
|
+
export default class Configuration {
|
|
19
|
+
async validate({options, source}) {
|
|
20
|
+
const finalOptions = {}
|
|
21
|
+
|
|
22
|
+
this.#mapEntryOptions({options, source})
|
|
23
|
+
|
|
24
|
+
// While the entry points do wrap the entire process in a try/catch, we
|
|
25
|
+
// should also do this here, so we can trap everything and instead
|
|
26
|
+
// of throwing, return friendly messages back!
|
|
27
|
+
|
|
28
|
+
// If the configuration parameters are invalid, we can't proceed. No error
|
|
29
|
+
// collection is needed here, because the ConfigurationParameters object
|
|
30
|
+
// is a static object and should be correct. OUT WITH THE TRASH!!! (I mean
|
|
31
|
+
// the error collection, not the ConfigurationParameters object)
|
|
32
|
+
// (Edit: No, I mean the ConfigurationParameters object. It's trash. Fix it
|
|
33
|
+
// if you get this error.)
|
|
34
|
+
const configValidationErrors = this.#validateConfigurationParameters()
|
|
35
|
+
if(configValidationErrors.length > 0)
|
|
36
|
+
throw new AggregateError(
|
|
37
|
+
configValidationErrors,
|
|
38
|
+
`ConfigurationParameters validation errors: `+
|
|
39
|
+
`${configValidationErrors.join(", ")}`,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const allOptions = this.#findAllOptions(options)
|
|
43
|
+
Object.assign(finalOptions, await this.#mergeOptions(allOptions))
|
|
44
|
+
this.#fixOptionValues(finalOptions)
|
|
45
|
+
|
|
46
|
+
// Priority keys are those which must be processed first. They are
|
|
47
|
+
// specified in order of priority.
|
|
48
|
+
// Find them and add them to an array; the rest will be in pushed to the
|
|
49
|
+
// end of the priority array.
|
|
50
|
+
const orderedSections = []
|
|
51
|
+
ConfigurationPriorityKeys.forEach((key) => {
|
|
52
|
+
if(!ConfigurationParameters[key])
|
|
53
|
+
throw new Error(`Invalid priority key: ${key}`)
|
|
54
|
+
|
|
55
|
+
if(finalOptions[key])
|
|
56
|
+
orderedSections.push({key, value: finalOptions[key]})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const remainingSections = Object.keys(ConfigurationParameters).filter(
|
|
60
|
+
(key) => !ConfigurationPriorityKeys.includes(key),
|
|
61
|
+
)
|
|
62
|
+
orderedSections.push(
|
|
63
|
+
...remainingSections.map((key) => {
|
|
64
|
+
return {key, value: finalOptions[key]}
|
|
65
|
+
}),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
// Check exclusive options
|
|
69
|
+
for(const [key, param] of Object.entries(ConfigurationParameters)) {
|
|
70
|
+
if(
|
|
71
|
+
param.exclusiveOf &&
|
|
72
|
+
finalOptions[key] &&
|
|
73
|
+
finalOptions[param.exclusiveOf]
|
|
74
|
+
)
|
|
75
|
+
throw new SyntaxError(
|
|
76
|
+
`Options \`${key}\` and \`${param.exclusiveOf}\` are mutually exclusive`,
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for(const section of orderedSections) {
|
|
81
|
+
const {key} = section
|
|
82
|
+
|
|
83
|
+
// Skipping config, we've already handled it
|
|
84
|
+
if(key === "config")
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
let {value} = section
|
|
88
|
+
const nothing = isNothing(value)
|
|
89
|
+
const param = ConfigurationParameters[key]
|
|
90
|
+
const {required, path} = param
|
|
91
|
+
|
|
92
|
+
if(nothing) {
|
|
93
|
+
if(required === true)
|
|
94
|
+
throw new SyntaxError(`Option \`${key}\` is required`)
|
|
95
|
+
else
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Additional path validation if needed
|
|
100
|
+
if(path && !nothing) {
|
|
101
|
+
const {mustExist, type: pathType} = path
|
|
102
|
+
|
|
103
|
+
// Special for `input` and `exclude` because they can be a comma-
|
|
104
|
+
// separated list of glob patterns.
|
|
105
|
+
if(key === "input" || key === "exclude") {
|
|
106
|
+
if(isType(value, "array"))
|
|
107
|
+
value = await Promise.all(
|
|
108
|
+
value.map((pattern) => getFiles(pattern)),
|
|
109
|
+
)
|
|
110
|
+
else if(isType(value, "string"))
|
|
111
|
+
value = await getFiles(value)
|
|
112
|
+
else
|
|
113
|
+
throw new TypeError(
|
|
114
|
+
`Option \`${key}\` must be a string or an array of strings`,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
finalOptions[key] = value.flat()
|
|
118
|
+
|
|
119
|
+
continue
|
|
120
|
+
} else {
|
|
121
|
+
if(mustExist === true) {
|
|
122
|
+
finalOptions[key] =
|
|
123
|
+
pathType === fdType.FILE
|
|
124
|
+
? resolveFilename(value)
|
|
125
|
+
: resolveDirectory(value)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
status: "success",
|
|
133
|
+
validated: true,
|
|
134
|
+
...finalOptions,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
#mapEntryOptions({options, source}) {
|
|
139
|
+
// CLI already has done all the work via commander
|
|
140
|
+
if(source === Environment.CLI)
|
|
141
|
+
return options
|
|
142
|
+
|
|
143
|
+
for(const [key, value] of Object.entries(options)) {
|
|
144
|
+
options[key] = {value, source}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// We will need to inject some options if they are not available
|
|
148
|
+
const cwd = process.cwd()
|
|
149
|
+
const dir = resolveDirectory(cwd)
|
|
150
|
+
|
|
151
|
+
// Inject basePath if not available
|
|
152
|
+
if(!options.basePath)
|
|
153
|
+
options.basePath = {value: dir, source}
|
|
154
|
+
|
|
155
|
+
// Inject packageJson if not available
|
|
156
|
+
if(!options.packageJson) {
|
|
157
|
+
const jsonFile = resolveFilename("package.json", dir)
|
|
158
|
+
const jsonObj = loadJson(jsonFile)
|
|
159
|
+
options.packageJson = {value: jsonObj, source}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return options
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Validate the ConfigurationParameters object. This is a sanity check to
|
|
167
|
+
* ensure that the ConfigurationParameters object is valid.
|
|
168
|
+
*
|
|
169
|
+
* @returns {string[]} Errors
|
|
170
|
+
*/
|
|
171
|
+
#validateConfigurationParameters() {
|
|
172
|
+
const errors = []
|
|
173
|
+
|
|
174
|
+
for(const [key, param] of Object.entries(ConfigurationParameters)) {
|
|
175
|
+
// Type
|
|
176
|
+
if(!param.type) {
|
|
177
|
+
errors.push(`Option \`${key}\` has no type`)
|
|
178
|
+
continue
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Paths
|
|
182
|
+
if(param.subtype?.path) {
|
|
183
|
+
const pathType = param.subtype.path?.type
|
|
184
|
+
|
|
185
|
+
// Check if pathType is defined
|
|
186
|
+
if(!pathType)
|
|
187
|
+
errors.push(`Option \`${key}\` has no path type`)
|
|
188
|
+
|
|
189
|
+
// Check if pathType is a valid key in FdTypes
|
|
190
|
+
if(!fdTypes.includes(pathType))
|
|
191
|
+
errors.push(`Option \`${key}\` has invalid path type: ${pathType}`)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return errors
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Find all options from all sources
|
|
200
|
+
*
|
|
201
|
+
* @param {object} entryOptions - The command line options.
|
|
202
|
+
* @returns {Promise<object[]>} All options from all sources.
|
|
203
|
+
*/
|
|
204
|
+
#findAllOptions(entryOptions) {
|
|
205
|
+
const allOptions = []
|
|
206
|
+
|
|
207
|
+
const environmentVariables = this.#getEnvironmentVariables()
|
|
208
|
+
if(environmentVariables)
|
|
209
|
+
allOptions.push({source: "environment", options: environmentVariables})
|
|
210
|
+
|
|
211
|
+
const packageJson = entryOptions.packageJson
|
|
212
|
+
if(packageJson?.bedoc)
|
|
213
|
+
allOptions.push({source: "packageJson", options: packageJson.bedoc})
|
|
214
|
+
|
|
215
|
+
// Then the config file, if the options specified a config file
|
|
216
|
+
const useConfig =
|
|
217
|
+
entryOptions?.config ||
|
|
218
|
+
packageJson?.bedoc?.config ||
|
|
219
|
+
environmentVariables?.config
|
|
220
|
+
|
|
221
|
+
if(useConfig) {
|
|
222
|
+
const configFilename = packageJson?.bedoc?.config || entryOptions.config
|
|
223
|
+
|
|
224
|
+
if(!configFilename)
|
|
225
|
+
throw new Error("No config file specified")
|
|
226
|
+
|
|
227
|
+
const configFile = resolveFilename(configFilename)
|
|
228
|
+
const config = loadJson(configFile)
|
|
229
|
+
|
|
230
|
+
allOptions.push({source: "config", options: config})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
allOptions.push({source: "entry", options: entryOptions})
|
|
234
|
+
|
|
235
|
+
return allOptions
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get environment variables
|
|
240
|
+
*
|
|
241
|
+
* @returns {object} Environment variables
|
|
242
|
+
*/
|
|
243
|
+
#getEnvironmentVariables() {
|
|
244
|
+
const environmentVariables = {}
|
|
245
|
+
const params = Object.keys(ConfigurationParameters).map((param) => {
|
|
246
|
+
return {
|
|
247
|
+
param,
|
|
248
|
+
env: `bedoc_${param}`.toUpperCase(),
|
|
249
|
+
}
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
for(const param of params) {
|
|
253
|
+
if(process.env[param.env])
|
|
254
|
+
environmentVariables[param.param] = process.env[param.env]
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return environmentVariables
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Merge all options into one object
|
|
262
|
+
*
|
|
263
|
+
* @param {object[]} allOptions - All options from all sources.
|
|
264
|
+
* @returns {Promise<object>} The merged options.
|
|
265
|
+
*/
|
|
266
|
+
async #mergeOptions(allOptions) {
|
|
267
|
+
const entryIndex = allOptions.findIndex(
|
|
268
|
+
option => option.source && option.source === "entry",
|
|
269
|
+
)
|
|
270
|
+
const entryOptions = allOptions[entryIndex].options
|
|
271
|
+
const nonEntryOptions = allOptions.filter(
|
|
272
|
+
option => option.source && option.source !== "entry",
|
|
273
|
+
)
|
|
274
|
+
const optionsOnly = nonEntryOptions.map((option) => option.options)
|
|
275
|
+
const mergedOptions = optionsOnly.reduce((acc, options) => {
|
|
276
|
+
for(const [key, value] of Object.entries(options)) acc[key] = value
|
|
277
|
+
|
|
278
|
+
return acc
|
|
279
|
+
}, {})
|
|
280
|
+
|
|
281
|
+
const mappedOptions = await mapObject(mergedOptions, (option, value) => {
|
|
282
|
+
const {value: entryValue, source: entrySource} = entryOptions[option] ?? {
|
|
283
|
+
value: undefined,
|
|
284
|
+
source: undefined,
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const entryDefaulted = entrySource === "default"
|
|
288
|
+
|
|
289
|
+
if(entryValue && value !== entryValue)
|
|
290
|
+
return entryDefaulted ? value : entryValue
|
|
291
|
+
|
|
292
|
+
return value
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
// Last, but not least, add any defaulted options that are not in the
|
|
296
|
+
// mapped options
|
|
297
|
+
for(const [key, value] of Object.entries(entryOptions)) {
|
|
298
|
+
if(!mappedOptions[key]) {
|
|
299
|
+
if(value.source)
|
|
300
|
+
mappedOptions[key] = value.value
|
|
301
|
+
} else {
|
|
302
|
+
if(value.source !== "default")
|
|
303
|
+
mappedOptions[key] = value.value
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return mappedOptions
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Fix option values. This operation is performed in place.
|
|
312
|
+
*
|
|
313
|
+
* @param {object} options - The options to fix.
|
|
314
|
+
*/
|
|
315
|
+
#fixOptionValues(options) {
|
|
316
|
+
for(const [key, param] of Object.entries(ConfigurationParameters)) {
|
|
317
|
+
// If the options passed includes this configuration parameter
|
|
318
|
+
if(options[key]) {
|
|
319
|
+
if(typeof options[key] === "string" && param.type !== "string") {
|
|
320
|
+
switch(param.type.toString()) {
|
|
321
|
+
case "boolean":
|
|
322
|
+
case "number":
|
|
323
|
+
options[key] = JSON.parse(options[key])
|
|
324
|
+
break
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import * as DataUtil from "./util/DataUtil.js"
|
|
2
|
+
|
|
3
|
+
const {newTypeSpec} = DataUtil
|
|
4
|
+
|
|
5
|
+
const ConfigurationParameters = Object.freeze({
|
|
6
|
+
input: {
|
|
7
|
+
short: "i",
|
|
8
|
+
param: "file",
|
|
9
|
+
description: "Comma-separated glob patterns to match files",
|
|
10
|
+
type: newTypeSpec("string|string[]"),
|
|
11
|
+
required: true,
|
|
12
|
+
path: {
|
|
13
|
+
type: "file",
|
|
14
|
+
mustExist: true,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
exclude: {
|
|
18
|
+
short: "x",
|
|
19
|
+
param: "file",
|
|
20
|
+
description: "Comma-separated glob patterns to exclude files",
|
|
21
|
+
type: newTypeSpec("string|string[]"),
|
|
22
|
+
required: false,
|
|
23
|
+
},
|
|
24
|
+
language: {
|
|
25
|
+
short: "l",
|
|
26
|
+
param: "lang",
|
|
27
|
+
description: "Language parser to use",
|
|
28
|
+
type: newTypeSpec("string"),
|
|
29
|
+
required: false,
|
|
30
|
+
exclusiveOf: "parser",
|
|
31
|
+
},
|
|
32
|
+
format: {
|
|
33
|
+
short: "f",
|
|
34
|
+
description: "Output format",
|
|
35
|
+
type: newTypeSpec("string"),
|
|
36
|
+
required: false,
|
|
37
|
+
exclusiveOf: "printer",
|
|
38
|
+
},
|
|
39
|
+
maxConcurrent: {
|
|
40
|
+
short: "C",
|
|
41
|
+
param: "num",
|
|
42
|
+
description: "Maximum number of concurrent tasks",
|
|
43
|
+
type: newTypeSpec("number"),
|
|
44
|
+
required: false,
|
|
45
|
+
default: 10,
|
|
46
|
+
},
|
|
47
|
+
hooks: {
|
|
48
|
+
short: "k",
|
|
49
|
+
param: "file",
|
|
50
|
+
description: "Custom hooks JS file",
|
|
51
|
+
type: newTypeSpec("string"),
|
|
52
|
+
required: false,
|
|
53
|
+
path: {
|
|
54
|
+
type: "file",
|
|
55
|
+
mustExist: true,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
output: {
|
|
59
|
+
short: "o",
|
|
60
|
+
param: "dir",
|
|
61
|
+
description: "Output directory",
|
|
62
|
+
type: newTypeSpec("string"),
|
|
63
|
+
required: false,
|
|
64
|
+
path: {
|
|
65
|
+
type: "directory",
|
|
66
|
+
mustExist: true,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
parser: {
|
|
70
|
+
short: "p",
|
|
71
|
+
param: "file",
|
|
72
|
+
description: "Custom parser JS file",
|
|
73
|
+
type: newTypeSpec("string"),
|
|
74
|
+
required: false,
|
|
75
|
+
exclusiveOf: "language",
|
|
76
|
+
path: {
|
|
77
|
+
type: "file",
|
|
78
|
+
mustExist: true,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
printer: {
|
|
82
|
+
short: "P",
|
|
83
|
+
param: "file",
|
|
84
|
+
description: "Custom printer JS file",
|
|
85
|
+
type: newTypeSpec("string"),
|
|
86
|
+
required: false,
|
|
87
|
+
exclusiveOf: "format",
|
|
88
|
+
path: {
|
|
89
|
+
type: "file",
|
|
90
|
+
mustExist: true,
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
hookTimeout: {
|
|
94
|
+
short: "T",
|
|
95
|
+
param: "ms",
|
|
96
|
+
description: "Timeout in milliseconds for hook execution",
|
|
97
|
+
type: newTypeSpec("number"),
|
|
98
|
+
required: false,
|
|
99
|
+
default: 5000,
|
|
100
|
+
},
|
|
101
|
+
mock: {
|
|
102
|
+
short: "m",
|
|
103
|
+
param: "dir",
|
|
104
|
+
description: "Path to mock parsers and printers",
|
|
105
|
+
type: newTypeSpec("string"),
|
|
106
|
+
required: false,
|
|
107
|
+
path: {
|
|
108
|
+
type: "directory",
|
|
109
|
+
mustExist: true,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
config: {
|
|
113
|
+
short: "c",
|
|
114
|
+
param: "file",
|
|
115
|
+
description: "Use JSON config file",
|
|
116
|
+
type: newTypeSpec("string"),
|
|
117
|
+
required: false,
|
|
118
|
+
path: {
|
|
119
|
+
type: "file",
|
|
120
|
+
mustExist: true,
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
debug: {
|
|
124
|
+
short: "d",
|
|
125
|
+
description: "Enable debug mode",
|
|
126
|
+
type: newTypeSpec("boolean"),
|
|
127
|
+
required: false,
|
|
128
|
+
default: false,
|
|
129
|
+
},
|
|
130
|
+
debugLevel: {
|
|
131
|
+
short: "D",
|
|
132
|
+
param: "level",
|
|
133
|
+
description: "Debug level",
|
|
134
|
+
type: newTypeSpec("number"),
|
|
135
|
+
required: false,
|
|
136
|
+
default: 0,
|
|
137
|
+
},
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
const ConfigurationPriorityKeys = Object.freeze(["exclude", "input"])
|
|
141
|
+
|
|
142
|
+
export {ConfigurationParameters, ConfigurationPriorityKeys}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as FDUtil from "./util/FDUtil.js"
|
|
2
|
+
|
|
3
|
+
const {readFile, writeFile, composeFilename} = FDUtil
|
|
4
|
+
|
|
5
|
+
export default class Conveyor {
|
|
6
|
+
#succeeded = []
|
|
7
|
+
#errored = []
|
|
8
|
+
|
|
9
|
+
constructor(parser, printer, logger, output) {
|
|
10
|
+
this.parser = parser
|
|
11
|
+
this.printer = printer
|
|
12
|
+
this.logger = logger
|
|
13
|
+
this.output = output
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Processes files with a concurrency limit.
|
|
18
|
+
*
|
|
19
|
+
* @param {Array} files - List of files to process.
|
|
20
|
+
* @param {number} maxConcurrent - Maximum number of concurrent tasks.
|
|
21
|
+
* @returns {Promise<object>} - Resolves when all files are processed.
|
|
22
|
+
*/
|
|
23
|
+
async convey(files, maxConcurrent = 10) {
|
|
24
|
+
const semaphore = Array(maxConcurrent).fill(Promise.resolve())
|
|
25
|
+
|
|
26
|
+
for(const file of files) {
|
|
27
|
+
const slot = Promise.race(semaphore) // Wait for an available slot
|
|
28
|
+
semaphore.push(slot.then(async() => {
|
|
29
|
+
const result = await this.#processFile(file)
|
|
30
|
+
|
|
31
|
+
if(result.status === "success")
|
|
32
|
+
this.#succeeded.push({input: file, output: result.file})
|
|
33
|
+
else
|
|
34
|
+
this.#errored.push({input: file, error: result.error})
|
|
35
|
+
}))
|
|
36
|
+
semaphore.shift() // Remove the oldest promise
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Wait for all tasks to complete
|
|
40
|
+
await Promise.all(semaphore)
|
|
41
|
+
|
|
42
|
+
return {succeeded: this.#succeeded, errored: this.#errored}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Processes a single file.
|
|
47
|
+
*
|
|
48
|
+
* @param {object} file - FileMap object representing a file.
|
|
49
|
+
* @returns {Promise<object>} - Resolves when the file is processed
|
|
50
|
+
*/
|
|
51
|
+
async #processFile(file) {
|
|
52
|
+
const debug = this.logger.newDebug()
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
debug("Processing file: `%s`", 2, file.path)
|
|
56
|
+
|
|
57
|
+
// Step 1: Read file
|
|
58
|
+
const fileContent = await readFile(file)
|
|
59
|
+
debug("Read file content `%s` (%d bytes)", 2, file.path, fileContent.length)
|
|
60
|
+
|
|
61
|
+
// Step 2: Parse file
|
|
62
|
+
const parseResult = await this.parser.parse(file, fileContent)
|
|
63
|
+
if(parseResult.status === "error")
|
|
64
|
+
return parseResult
|
|
65
|
+
|
|
66
|
+
debug("Parsed file successfully: `%s`", 2, file.path)
|
|
67
|
+
|
|
68
|
+
// Step 3: Print file
|
|
69
|
+
const printResult = await this.printer.print(
|
|
70
|
+
file,
|
|
71
|
+
parseResult.result,
|
|
72
|
+
)
|
|
73
|
+
if(printResult.status === "error")
|
|
74
|
+
return printResult
|
|
75
|
+
|
|
76
|
+
debug("Printed file successfully: `%s`", 2, file.path)
|
|
77
|
+
|
|
78
|
+
// Step 4: Write output
|
|
79
|
+
const {destFile, content} = printResult
|
|
80
|
+
if(!destFile || !content)
|
|
81
|
+
return {status: "error", message: "Invalid print result"}
|
|
82
|
+
|
|
83
|
+
const writeResult = await this.#writeOutput(destFile, content)
|
|
84
|
+
|
|
85
|
+
if(writeResult.status === "success")
|
|
86
|
+
debug("Wrote output for: `%s` (%d bytes)", 2, file.path, content.length)
|
|
87
|
+
|
|
88
|
+
return writeResult
|
|
89
|
+
} 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}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Writes the output to the destination.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} destFile - Destination file path.
|
|
100
|
+
* @param {string} content - File content.
|
|
101
|
+
* @returns {Promise<object>} - Resolves when the file is written.
|
|
102
|
+
*/
|
|
103
|
+
async #writeOutput(destFile, content) {
|
|
104
|
+
const destFileMap = composeFilename(this.output.path, destFile)
|
|
105
|
+
try {
|
|
106
|
+
writeFile(destFileMap, content)
|
|
107
|
+
|
|
108
|
+
return {status: "success", file: destFileMap}
|
|
109
|
+
} catch(error) {
|
|
110
|
+
return {status: "error", output: destFileMap, error}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|