@gesslar/bedoc 1.10.0 → 2.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/LICENSE.txt +12 -0
- package/README.md +15 -3
- package/dist/schema/bedoc.action.json +42 -0
- package/dist/types/Action.d.ts +3 -0
- package/dist/types/Action.d.ts.map +1 -0
- package/dist/types/BeDoc.d.ts +208 -0
- package/dist/types/BeDoc.d.ts.map +1 -0
- package/dist/types/Configuration.d.ts +11 -0
- package/dist/types/Configuration.d.ts.map +1 -0
- package/dist/types/ConfigurationParameters.d.ts +3 -0
- package/dist/types/ConfigurationParameters.d.ts.map +1 -0
- package/dist/types/Conveyor.d.ts +27 -0
- package/dist/types/Conveyor.d.ts.map +1 -0
- package/dist/types/Discovery.d.ts +215 -0
- package/dist/types/Discovery.d.ts.map +1 -0
- package/dist/types/Environment.d.ts +3 -0
- package/dist/types/Environment.d.ts.map +1 -0
- package/dist/types/Logger.d.ts +47 -0
- package/dist/types/Logger.d.ts.map +1 -0
- package/dist/types/Schema.d.ts +3 -0
- package/dist/types/Schema.d.ts.map +1 -0
- package/dist/types/cli.d.ts +2 -2
- package/dist/types/cli.d.ts.map +1 -10
- package/package.json +24 -23
- package/src/Action.js +9 -0
- package/src/BeDoc.js +276 -0
- package/src/CLIOutput.js +198 -0
- package/src/{core/Configuration.js → Configuration.js} +72 -58
- package/src/{core/ConfigurationParameters.js → ConfigurationParameters.js} +35 -27
- package/src/Conveyor.js +256 -0
- package/src/Discovery.js +442 -0
- package/src/Environment.js +8 -0
- package/src/{core/Logger.js → Logger.js} +30 -18
- package/src/Schema.js +6 -0
- package/src/cli.js +77 -34
- package/tsconfig.types.json +42 -0
- package/LICENSE +0 -24
- package/dist/types/core/ActionManager.d.ts +0 -58
- package/dist/types/core/ActionManager.d.ts.map +0 -10
- package/dist/types/core/Configuration.d.ts +0 -27
- package/dist/types/core/Configuration.d.ts.map +0 -10
- package/dist/types/core/ConfigurationParameters.d.ts +0 -38
- package/dist/types/core/ConfigurationParameters.d.ts.map +0 -10
- package/dist/types/core/Conveyor.d.ts +0 -49
- package/dist/types/core/Conveyor.d.ts.map +0 -10
- package/dist/types/core/Core.d.ts +0 -48
- package/dist/types/core/Core.d.ts.map +0 -10
- package/dist/types/core/Discovery.d.ts +0 -73
- package/dist/types/core/Discovery.d.ts.map +0 -10
- package/dist/types/core/HookManager.d.ts +0 -60
- package/dist/types/core/HookManager.d.ts.map +0 -10
- package/dist/types/core/Logger.d.ts +0 -63
- package/dist/types/core/Logger.d.ts.map +0 -10
- package/dist/types/core/action/ParseManager.d.ts +0 -8
- package/dist/types/core/action/ParseManager.d.ts.map +0 -10
- package/dist/types/core/action/PrintManager.d.ts +0 -8
- package/dist/types/core/action/PrintManager.d.ts.map +0 -10
- package/dist/types/core/util/ActionUtil.d.ts +0 -35
- package/dist/types/core/util/ActionUtil.d.ts.map +0 -10
- package/dist/types/core/util/DataUtil.d.ts +0 -52
- package/dist/types/core/util/DataUtil.d.ts.map +0 -10
- package/dist/types/core/util/FDUtil.d.ts +0 -171
- package/dist/types/core/util/FDUtil.d.ts.map +0 -10
- package/dist/types/core/util/ModuleUtil.d.ts +0 -27
- package/dist/types/core/util/ModuleUtil.d.ts.map +0 -10
- package/dist/types/core/util/StringUtil.d.ts +0 -5
- package/dist/types/core/util/StringUtil.d.ts.map +0 -10
- package/dist/types/core/util/TypeSpec.d.ts +0 -42
- package/dist/types/core/util/TypeSpec.d.ts.map +0 -10
- package/dist/types/core/util/ValidUtil.d.ts +0 -29
- package/dist/types/core/util/ValidUtil.d.ts.map +0 -10
- package/src/core/ActionManager.js +0 -147
- package/src/core/ContractManager.js +0 -112
- package/src/core/Conveyor.js +0 -185
- package/src/core/Core.js +0 -166
- package/src/core/Discovery.js +0 -403
- package/src/core/HookManager.js +0 -143
- package/src/core/action/ParseManager.js +0 -7
- package/src/core/action/PrintManager.js +0 -7
- package/src/core/contract/ParseContract.js +0 -7
- package/src/core/contract/PrintContract.js +0 -7
- package/src/core/util/ActionUtil.js +0 -53
- package/src/core/util/ContractUtil.js +0 -63
- package/src/core/util/DataUtil.js +0 -540
- package/src/core/util/FDUtil.js +0 -388
- package/src/core/util/ModuleUtil.js +0 -40
- package/src/core/util/StringUtil.js +0 -11
- package/src/core/util/TypeSpec.js +0 -114
- package/src/core/util/ValidUtil.js +0 -50
|
@@ -1,24 +1,16 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {Environment} from "./Core.js"
|
|
1
|
+
import {Collection, Data, DirectoryObject, FileObject, FileSystem as FS, Promised, Sass, Tantrum} from "@gesslar/toolkit"
|
|
3
2
|
import JSON5 from "json5"
|
|
3
|
+
import process from "node:process"
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
ConfigurationParameters,
|
|
7
7
|
ConfigurationPriorityKeys,
|
|
8
8
|
} from "./ConfigurationParameters.js"
|
|
9
|
-
|
|
10
|
-
import * as ActionUtil from "./util/ActionUtil.js"
|
|
11
|
-
import * as DataUtil from "./util/DataUtil.js"
|
|
12
|
-
import * as FDUtil from "./util/FDUtil.js"
|
|
13
|
-
|
|
14
|
-
const {loadJson} = ActionUtil
|
|
15
|
-
const {isNothing, isType, mapObject} = DataUtil
|
|
16
|
-
const {getFiles, composeFilename, fileExists} = FDUtil
|
|
17
|
-
const {resolveDirectory, resolveFilename} = FDUtil
|
|
18
|
-
const {fdType, fdTypes} = FDUtil
|
|
9
|
+
import Environment from "./Environment.js"
|
|
19
10
|
|
|
20
11
|
export default class Configuration {
|
|
21
12
|
async validate({options, source}) {
|
|
13
|
+
const {basePath: base} = options
|
|
22
14
|
const finalOptions = {}
|
|
23
15
|
|
|
24
16
|
this.#mapEntryOptions({options, source})
|
|
@@ -34,14 +26,15 @@ export default class Configuration {
|
|
|
34
26
|
// (Edit: No, I mean the ConfigurationParameters object. It's trash. Fix it
|
|
35
27
|
// if you get this error.)
|
|
36
28
|
const configValidationErrors = this.#validateConfigurationParameters()
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
`ConfigurationParameters validation errors
|
|
41
|
-
|
|
29
|
+
|
|
30
|
+
if(configValidationErrors.length > 0) {
|
|
31
|
+
throw Tantrum.new(
|
|
32
|
+
`ConfigurationParameters validation errors`,
|
|
33
|
+
configValidationErrors.map(e => new Sass(e))
|
|
42
34
|
)
|
|
35
|
+
}
|
|
43
36
|
|
|
44
|
-
const allOptions = this.#findAllOptions(options)
|
|
37
|
+
const allOptions = await this.#findAllOptions(options)
|
|
45
38
|
|
|
46
39
|
Object.assign(finalOptions, await this.#mergeOptions(allOptions))
|
|
47
40
|
|
|
@@ -52,9 +45,10 @@ export default class Configuration {
|
|
|
52
45
|
// Find them and add them to an array; the rest will be in pushed to the
|
|
53
46
|
// end of the priority array.
|
|
54
47
|
const orderedSections = []
|
|
48
|
+
|
|
55
49
|
ConfigurationPriorityKeys.forEach(key => {
|
|
56
50
|
if(!ConfigurationParameters[key])
|
|
57
|
-
throw new
|
|
51
|
+
throw Sass.new(`Invalid priority key: ${key}`)
|
|
58
52
|
|
|
59
53
|
if(finalOptions[key])
|
|
60
54
|
orderedSections.push({key, value: finalOptions[key]})
|
|
@@ -63,6 +57,7 @@ export default class Configuration {
|
|
|
63
57
|
const remainingSections = Object.keys(ConfigurationParameters).filter(
|
|
64
58
|
key => !ConfigurationPriorityKeys.includes(key),
|
|
65
59
|
)
|
|
60
|
+
|
|
66
61
|
orderedSections.push(
|
|
67
62
|
...remainingSections.map(key => {
|
|
68
63
|
return {key, value: finalOptions[key]}
|
|
@@ -95,7 +90,7 @@ export default class Configuration {
|
|
|
95
90
|
continue
|
|
96
91
|
|
|
97
92
|
let {value} = section
|
|
98
|
-
const nothing = isNothing(value)
|
|
93
|
+
const nothing = Data.isNothing(value)
|
|
99
94
|
const param = ConfigurationParameters[key]
|
|
100
95
|
const {required, path} = param
|
|
101
96
|
|
|
@@ -110,29 +105,35 @@ export default class Configuration {
|
|
|
110
105
|
if(path && !nothing) {
|
|
111
106
|
const {mustExist, type: pathType} = path
|
|
112
107
|
|
|
113
|
-
//
|
|
114
|
-
//
|
|
108
|
+
// `input` and `exclude` are glob patterns (or arrays of them). Each
|
|
109
|
+
// is resolved against the base directory via DirectoryObject's glob.
|
|
115
110
|
if(key === "input" || key === "exclude") {
|
|
116
|
-
if(isType(value, "
|
|
117
|
-
value = await Promise.all(
|
|
118
|
-
value.map(pattern => getFiles(pattern)),
|
|
119
|
-
)
|
|
120
|
-
else if(isType(value, "string"))
|
|
121
|
-
value = await getFiles(value)
|
|
122
|
-
else
|
|
111
|
+
if(!Data.isType(value, "Array") && !Data.isType(value, "String")) {
|
|
123
112
|
throw new TypeError(
|
|
124
113
|
`Option \`${key}\` must be a string or an array of strings`,
|
|
125
114
|
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const patterns = Data.isType(value, "Array") ? value : [value]
|
|
126
118
|
|
|
127
|
-
|
|
119
|
+
const settled = await Promised.settle(
|
|
120
|
+
patterns.map(async pat => {
|
|
121
|
+
const {files} = await base.glob(pat)
|
|
122
|
+
|
|
123
|
+
return files
|
|
124
|
+
})
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
value = Promised.values(settled).flat()
|
|
128
|
+
|
|
129
|
+
finalOptions[key] = value
|
|
128
130
|
|
|
129
131
|
continue
|
|
130
132
|
} else {
|
|
131
133
|
if(mustExist === true) {
|
|
132
|
-
finalOptions[key] =
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
: resolveDirectory(value)
|
|
134
|
+
finalOptions[key] = pathType === FS.fdType.FILE
|
|
135
|
+
? new FileObject(value)
|
|
136
|
+
: new DirectoryObject(value)
|
|
136
137
|
}
|
|
137
138
|
}
|
|
138
139
|
}
|
|
@@ -150,17 +151,22 @@ export default class Configuration {
|
|
|
150
151
|
if(source === Environment.CLI)
|
|
151
152
|
return options
|
|
152
153
|
|
|
154
|
+
// `basePath` and `project` are consumed downstream as raw values (matching
|
|
155
|
+
// the shape the CLI provides); everything else is wrapped as {value, source}.
|
|
153
156
|
for(const [key, value] of Object.entries(options)) {
|
|
157
|
+
if(key === "basePath" || key === "project")
|
|
158
|
+
continue
|
|
159
|
+
|
|
154
160
|
options[key] = {value, source}
|
|
155
161
|
}
|
|
156
162
|
|
|
157
163
|
// We will need to inject some options if they are not available
|
|
158
164
|
const cwd = process.cwd()
|
|
159
|
-
const dir =
|
|
165
|
+
const dir = new DirectoryObject(cwd)
|
|
160
166
|
|
|
161
167
|
// Inject basePath if not available
|
|
162
168
|
if(!options.basePath)
|
|
163
|
-
options.basePath =
|
|
169
|
+
options.basePath = dir
|
|
164
170
|
|
|
165
171
|
// Add defaults which are missing
|
|
166
172
|
for(const [key, param] of Object.entries(ConfigurationParameters)) {
|
|
@@ -196,7 +202,7 @@ export default class Configuration {
|
|
|
196
202
|
errors.push(`Option \`${key}\` has no path type`)
|
|
197
203
|
|
|
198
204
|
// Check if pathType is a valid key in FdTypes
|
|
199
|
-
if(!fdTypes.includes(pathType))
|
|
205
|
+
if(!FS.fdTypes.includes(pathType))
|
|
200
206
|
errors.push(`Option \`${key}\` has invalid path type: ${pathType}`)
|
|
201
207
|
}
|
|
202
208
|
}
|
|
@@ -210,19 +216,23 @@ export default class Configuration {
|
|
|
210
216
|
* @param {object} entryOptions - The command line options.
|
|
211
217
|
* @returns {Promise<object[]>} All options from all sources.
|
|
212
218
|
*/
|
|
213
|
-
#findAllOptions(entryOptions) {
|
|
219
|
+
async #findAllOptions(entryOptions) {
|
|
220
|
+
const {basePath} = entryOptions
|
|
214
221
|
const allOptions = []
|
|
215
222
|
const environmentVariables = this.#getEnvironmentVariables()
|
|
223
|
+
|
|
216
224
|
if(environmentVariables)
|
|
217
225
|
allOptions.push({source: "environment", options: environmentVariables})
|
|
218
226
|
|
|
219
|
-
const packageJson = entryOptions?.
|
|
227
|
+
const packageJson = entryOptions?.project
|
|
228
|
+
|
|
220
229
|
if(packageJson) {
|
|
221
230
|
allOptions.push({source: "packageJson", options: packageJson})
|
|
222
231
|
} else {
|
|
223
|
-
const packageJsonFile =
|
|
224
|
-
|
|
225
|
-
|
|
232
|
+
const packageJsonFile = basePath.getFile("package.json")
|
|
233
|
+
|
|
234
|
+
if(await packageJsonFile.exists) {
|
|
235
|
+
const packageJson = await packageJsonFile.loadData()
|
|
226
236
|
|
|
227
237
|
if(packageJson.bedoc)
|
|
228
238
|
allOptions.push({source: "packageJson", options: packageJson.bedoc})
|
|
@@ -238,15 +248,17 @@ export default class Configuration {
|
|
|
238
248
|
if(useConfig) {
|
|
239
249
|
const configFile =
|
|
240
250
|
packageJson?.config
|
|
241
|
-
?
|
|
251
|
+
? new FileObject(packageJson.config)
|
|
242
252
|
: entryOptions.config?.value
|
|
243
|
-
?
|
|
244
|
-
:
|
|
253
|
+
? new FileObject(entryOptions.config.value)
|
|
254
|
+
: environmentVariables?.config
|
|
255
|
+
? new FileObject(environmentVariables.config)
|
|
256
|
+
: null
|
|
245
257
|
|
|
246
258
|
if(!configFile)
|
|
247
|
-
throw new
|
|
259
|
+
throw Sass.new("No config file specified")
|
|
248
260
|
|
|
249
|
-
const configObject =
|
|
261
|
+
const configObject = await configFile.loadData()
|
|
250
262
|
const subConfigName =
|
|
251
263
|
entryOptions?.sub ||
|
|
252
264
|
packageJson?.sub ||
|
|
@@ -273,7 +285,7 @@ export default class Configuration {
|
|
|
273
285
|
const subConfig = configObject.sub?.find(sub => sub.name === subConfigName)
|
|
274
286
|
|
|
275
287
|
if(!subConfig)
|
|
276
|
-
throw new
|
|
288
|
+
throw Sass.new(`No such subconfiguration \`${subConfigName}\``)
|
|
277
289
|
|
|
278
290
|
// We don't need this anymore
|
|
279
291
|
delete subConfig.name
|
|
@@ -328,19 +340,21 @@ export default class Configuration {
|
|
|
328
340
|
return acc
|
|
329
341
|
}, {})
|
|
330
342
|
|
|
331
|
-
const mappedOptions = await mapObject(mergedOptions,
|
|
332
|
-
|
|
333
|
-
value:
|
|
334
|
-
|
|
335
|
-
|
|
343
|
+
const mappedOptions = await Collection.mapObject(mergedOptions,
|
|
344
|
+
(option, value) => {
|
|
345
|
+
const {value: entryValue, source: entrySource} =
|
|
346
|
+
entryOptions[option] ?? {
|
|
347
|
+
value: undefined,
|
|
348
|
+
source: undefined,
|
|
349
|
+
}
|
|
336
350
|
|
|
337
|
-
|
|
351
|
+
const entryDefaulted = entrySource === "default"
|
|
338
352
|
|
|
339
|
-
|
|
340
|
-
|
|
353
|
+
if(entryValue && value !== entryValue)
|
|
354
|
+
return entryDefaulted ? value : entryValue
|
|
341
355
|
|
|
342
|
-
|
|
343
|
-
|
|
356
|
+
return value
|
|
357
|
+
})
|
|
344
358
|
|
|
345
359
|
// Last, but not least, add any defaulted options that are not in the
|
|
346
360
|
// mapped options
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {Data} from "@gesslar/toolkit"
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
const ConfigurationParameters = Object.freeze({
|
|
3
|
+
const ConfigurationParameters = Data.deepFreezeObject({
|
|
6
4
|
input: {
|
|
7
5
|
short: "i",
|
|
8
6
|
param: "file",
|
|
9
|
-
description: "
|
|
10
|
-
type: newTypeSpec("string|string[]"),
|
|
7
|
+
description: "Glob pattern (or array of patterns) to match files",
|
|
8
|
+
type: Data.newTypeSpec("string|string[]"),
|
|
11
9
|
required: true,
|
|
12
10
|
path: {
|
|
13
11
|
type: "file",
|
|
@@ -17,30 +15,30 @@ const ConfigurationParameters = Object.freeze({
|
|
|
17
15
|
exclude: {
|
|
18
16
|
short: "x",
|
|
19
17
|
param: "file",
|
|
20
|
-
description: "
|
|
21
|
-
type: newTypeSpec("string|string[]"),
|
|
18
|
+
description: "Glob pattern (or array of patterns) to exclude files",
|
|
19
|
+
type: Data.newTypeSpec("string|string[]"),
|
|
22
20
|
required: false,
|
|
23
21
|
},
|
|
24
22
|
language: {
|
|
25
23
|
short: "l",
|
|
26
24
|
param: "lang",
|
|
27
25
|
description: "Language parser to use",
|
|
28
|
-
type: newTypeSpec("string"),
|
|
26
|
+
type: Data.newTypeSpec("string"),
|
|
29
27
|
required: false,
|
|
30
28
|
exclusiveOf: "parser",
|
|
31
29
|
},
|
|
32
30
|
format: {
|
|
33
31
|
short: "f",
|
|
34
32
|
description: "Output format",
|
|
35
|
-
type: newTypeSpec("string"),
|
|
33
|
+
type: Data.newTypeSpec("string"),
|
|
36
34
|
required: false,
|
|
37
|
-
exclusiveOf: "
|
|
35
|
+
exclusiveOf: "formatter",
|
|
38
36
|
},
|
|
39
37
|
maxConcurrent: {
|
|
40
38
|
short: "C",
|
|
41
39
|
param: "num",
|
|
42
40
|
description: "Maximum number of concurrent tasks",
|
|
43
|
-
type: newTypeSpec("number"),
|
|
41
|
+
type: Data.newTypeSpec("number"),
|
|
44
42
|
required: false,
|
|
45
43
|
default: 10,
|
|
46
44
|
},
|
|
@@ -48,7 +46,7 @@ const ConfigurationParameters = Object.freeze({
|
|
|
48
46
|
short: "k",
|
|
49
47
|
param: "file",
|
|
50
48
|
description: "Custom hooks JS file",
|
|
51
|
-
type: newTypeSpec("string"),
|
|
49
|
+
type: Data.newTypeSpec("string"),
|
|
52
50
|
required: false,
|
|
53
51
|
path: {
|
|
54
52
|
type: "file",
|
|
@@ -59,7 +57,7 @@ const ConfigurationParameters = Object.freeze({
|
|
|
59
57
|
short: "o",
|
|
60
58
|
param: "dir",
|
|
61
59
|
description: "Output directory",
|
|
62
|
-
type: newTypeSpec("string"),
|
|
60
|
+
type: Data.newTypeSpec("string"),
|
|
63
61
|
required: false,
|
|
64
62
|
path: {
|
|
65
63
|
type: "directory",
|
|
@@ -70,7 +68,7 @@ const ConfigurationParameters = Object.freeze({
|
|
|
70
68
|
short: "p",
|
|
71
69
|
param: "file",
|
|
72
70
|
description: "Custom parser JS file",
|
|
73
|
-
type: newTypeSpec("string"),
|
|
71
|
+
type: Data.newTypeSpec("string"),
|
|
74
72
|
required: false,
|
|
75
73
|
exclusiveOf: "language",
|
|
76
74
|
path: {
|
|
@@ -78,11 +76,11 @@ const ConfigurationParameters = Object.freeze({
|
|
|
78
76
|
mustExist: true,
|
|
79
77
|
},
|
|
80
78
|
},
|
|
81
|
-
|
|
79
|
+
formatter: {
|
|
82
80
|
short: "P",
|
|
83
81
|
param: "file",
|
|
84
|
-
description: "Custom
|
|
85
|
-
type: newTypeSpec("string"),
|
|
82
|
+
description: "Custom formatter JS file",
|
|
83
|
+
type: Data.newTypeSpec("string"),
|
|
86
84
|
required: false,
|
|
87
85
|
exclusiveOf: "format",
|
|
88
86
|
path: {
|
|
@@ -94,15 +92,15 @@ const ConfigurationParameters = Object.freeze({
|
|
|
94
92
|
short: "T",
|
|
95
93
|
param: "ms",
|
|
96
94
|
description: "Timeout in milliseconds for hook execution",
|
|
97
|
-
type: newTypeSpec("number"),
|
|
95
|
+
type: Data.newTypeSpec("number"),
|
|
98
96
|
required: false,
|
|
99
97
|
default: 5000,
|
|
100
98
|
},
|
|
101
99
|
mock: {
|
|
102
100
|
short: "m",
|
|
103
101
|
param: "dir",
|
|
104
|
-
description: "Path to mock parsers and
|
|
105
|
-
type: newTypeSpec("string"),
|
|
102
|
+
description: "Path to mock parsers and formatters",
|
|
103
|
+
type: Data.newTypeSpec("string"),
|
|
106
104
|
required: false,
|
|
107
105
|
path: {
|
|
108
106
|
type: "directory",
|
|
@@ -113,7 +111,7 @@ const ConfigurationParameters = Object.freeze({
|
|
|
113
111
|
short: "c",
|
|
114
112
|
param: "file",
|
|
115
113
|
description: "Use JSON config file",
|
|
116
|
-
type: newTypeSpec("string"),
|
|
114
|
+
type: Data.newTypeSpec("string"),
|
|
117
115
|
required: false,
|
|
118
116
|
path: {
|
|
119
117
|
type: "file",
|
|
@@ -124,14 +122,14 @@ const ConfigurationParameters = Object.freeze({
|
|
|
124
122
|
short: "s",
|
|
125
123
|
param: "name",
|
|
126
124
|
description: "Specify a subconfiguration",
|
|
127
|
-
type: newTypeSpec("string"),
|
|
125
|
+
type: Data.newTypeSpec("string"),
|
|
128
126
|
required: false,
|
|
129
127
|
dependent: "config",
|
|
130
128
|
},
|
|
131
129
|
debug: {
|
|
132
130
|
short: "d",
|
|
133
131
|
description: "Enable debug mode",
|
|
134
|
-
type: newTypeSpec("boolean"),
|
|
132
|
+
type: Data.newTypeSpec("boolean"),
|
|
135
133
|
required: false,
|
|
136
134
|
default: false,
|
|
137
135
|
},
|
|
@@ -139,12 +137,22 @@ const ConfigurationParameters = Object.freeze({
|
|
|
139
137
|
short: "D",
|
|
140
138
|
param: "level",
|
|
141
139
|
description: "Debug level",
|
|
142
|
-
type: newTypeSpec("number"),
|
|
140
|
+
type: Data.newTypeSpec("number"),
|
|
143
141
|
required: false,
|
|
144
142
|
default: 0,
|
|
145
143
|
},
|
|
144
|
+
terse: {
|
|
145
|
+
short: "t",
|
|
146
|
+
description: "Terse output (hide per-stage progress lines)",
|
|
147
|
+
type: Data.newTypeSpec("boolean"),
|
|
148
|
+
required: false,
|
|
149
|
+
default: false,
|
|
150
|
+
},
|
|
146
151
|
})
|
|
147
152
|
|
|
148
|
-
const ConfigurationPriorityKeys =
|
|
153
|
+
const ConfigurationPriorityKeys = Data.deepFreezeObject(["exclude", "input"])
|
|
149
154
|
|
|
150
|
-
export {
|
|
155
|
+
export {
|
|
156
|
+
ConfigurationParameters,
|
|
157
|
+
ConfigurationPriorityKeys
|
|
158
|
+
}
|
package/src/Conveyor.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import {ActionBuilder, ActionRunner, ACTIVITY} from "@gesslar/actioneer"
|
|
2
|
+
import {DirectoryObject, FileObject, Notify, Sass} from "@gesslar/toolkit"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @import {CLIOutput} from "./CLIOutput.js"
|
|
6
|
+
* @import {Contract} from "@gesslar/negotiator"
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const {IF} = ACTIVITY
|
|
10
|
+
|
|
11
|
+
export default class Conveyor {
|
|
12
|
+
#parser
|
|
13
|
+
#formatter
|
|
14
|
+
/** An instance of CLIOutput @type {CLIOutput} */
|
|
15
|
+
#cli
|
|
16
|
+
|
|
17
|
+
/** @type {DirectoryObject} */
|
|
18
|
+
#output
|
|
19
|
+
|
|
20
|
+
/** @type {Contract} */
|
|
21
|
+
#contract
|
|
22
|
+
|
|
23
|
+
#hooks
|
|
24
|
+
#basePath
|
|
25
|
+
|
|
26
|
+
constructor({
|
|
27
|
+
basePath,
|
|
28
|
+
parser,
|
|
29
|
+
formatter,
|
|
30
|
+
hooks,
|
|
31
|
+
contract,
|
|
32
|
+
output,
|
|
33
|
+
cli
|
|
34
|
+
}) {
|
|
35
|
+
this.#basePath = basePath
|
|
36
|
+
this.#parser = parser
|
|
37
|
+
this.#formatter = formatter
|
|
38
|
+
this.#hooks = hooks
|
|
39
|
+
this.#contract = contract
|
|
40
|
+
this.#output = output
|
|
41
|
+
this.#cli = cli
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Emits a pipeline stage transition for a file.
|
|
46
|
+
*
|
|
47
|
+
* @param {FileObject} file - The file the stage pertains to.
|
|
48
|
+
* @param {string} stage - The stage name (read|parse|validate|format|write).
|
|
49
|
+
* @param {string} state - The new state (active|done|warning|error).
|
|
50
|
+
*/
|
|
51
|
+
#emitStage = (file, stage, state) =>
|
|
52
|
+
Notify.emit("update-data", {file, message: {kind: "stage", stage, state}})
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Defines the per-file processing pipeline.
|
|
56
|
+
*
|
|
57
|
+
* @param {ActionBuilder} builder - The Actioneer builder instance.
|
|
58
|
+
*/
|
|
59
|
+
setup(builder) {
|
|
60
|
+
builder
|
|
61
|
+
.do("read", this.#readFile)
|
|
62
|
+
.do("parse", this.#parseFile)
|
|
63
|
+
.do("validate", this.#validateContracts)
|
|
64
|
+
.do("format", this.#formatFile)
|
|
65
|
+
.do("write", IF, this.#shouldWrite, this.#writeOutput)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Processes files through the parser→formatter pipeline with concurrency.
|
|
70
|
+
*
|
|
71
|
+
* @param {Array<FileObject>} files - List of files to process.
|
|
72
|
+
* @param {number} [maxConcurrent] - Maximum number of files to process at a time.
|
|
73
|
+
* @returns {Promise<object>} - Resolves with {succeeded, errored, warned}.
|
|
74
|
+
*/
|
|
75
|
+
async convey(files, maxConcurrent = 10) {
|
|
76
|
+
const builder = new ActionBuilder(this)
|
|
77
|
+
const runner = new ActionRunner(builder)
|
|
78
|
+
.addSetup(async() => {
|
|
79
|
+
if(this.#output && !await this.#output.exists)
|
|
80
|
+
await this.#output.assureExists({recursive: true})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const destExtension = this.#formatter.meta.extension ?? "txt"
|
|
84
|
+
const contexts = files.map(file => ({
|
|
85
|
+
file,
|
|
86
|
+
output: new FileObject(`${file.module}.${destExtension}`, this.#output)
|
|
87
|
+
}))
|
|
88
|
+
|
|
89
|
+
Notify.emit("conveyor-start", contexts)
|
|
90
|
+
|
|
91
|
+
const settled = await runner.pipe(contexts, maxConcurrent)
|
|
92
|
+
|
|
93
|
+
return this.#categorize(settled, files)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// -- Pipeline activities --------------------------------------------------
|
|
97
|
+
|
|
98
|
+
#readFile = async ctx => {
|
|
99
|
+
try {
|
|
100
|
+
this.#emitStage(ctx.file, "read", "active")
|
|
101
|
+
|
|
102
|
+
const content = await ctx.file.read()
|
|
103
|
+
|
|
104
|
+
Notify.emit("update-data", {file: ctx.file, message: {kind: "input-size", value: Buffer.byteLength(content)}})
|
|
105
|
+
this.#emitStage(ctx.file, "read", "done")
|
|
106
|
+
|
|
107
|
+
return {...ctx, content}
|
|
108
|
+
} catch(error) {
|
|
109
|
+
this.#emitStage(ctx.file, "read", "error")
|
|
110
|
+
|
|
111
|
+
return {...ctx, status: "error", error: Sass.new(`Reading file ${ctx.file}`, error)}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
#parseFile = async ctx => {
|
|
116
|
+
if(ctx.error)
|
|
117
|
+
return ctx
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
this.#emitStage(ctx.file, "parse", "active")
|
|
121
|
+
|
|
122
|
+
const {content} = ctx
|
|
123
|
+
const builder = new ActionBuilder(new this.#parser())
|
|
124
|
+
|
|
125
|
+
if(this.#hooks?.Parse)
|
|
126
|
+
builder.withHooks(new this.#hooks.Parse())
|
|
127
|
+
|
|
128
|
+
const runner = new ActionRunner(builder)
|
|
129
|
+
const result = await runner.run(content)
|
|
130
|
+
|
|
131
|
+
this.#emitStage(ctx.file, "parse", "done")
|
|
132
|
+
|
|
133
|
+
return Object.assign(ctx, {...result})
|
|
134
|
+
} catch(error) {
|
|
135
|
+
this.#emitStage(ctx.file, "parse", "error")
|
|
136
|
+
|
|
137
|
+
return {...ctx, status: "error", error: Sass.new(`Parsing file ${ctx.file}`, error)}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#validateContracts = ctx => {
|
|
142
|
+
if(ctx.error)
|
|
143
|
+
return ctx
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
this.#emitStage(ctx.file, "validate", "active")
|
|
147
|
+
|
|
148
|
+
this.#contract.validate(ctx)
|
|
149
|
+
|
|
150
|
+
this.#emitStage(ctx.file, "validate", "done")
|
|
151
|
+
} catch(err) {
|
|
152
|
+
if(err) {
|
|
153
|
+
this.#emitStage(ctx.file, "validate", "error")
|
|
154
|
+
|
|
155
|
+
throw Sass.new(`Parser validation for ${ctx.file.path}`, err)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return ctx
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
#formatFile = async ctx => {
|
|
163
|
+
if(ctx.error)
|
|
164
|
+
return ctx
|
|
165
|
+
|
|
166
|
+
this.#emitStage(ctx.file, "format", "active")
|
|
167
|
+
|
|
168
|
+
const {functions} = ctx
|
|
169
|
+
const builder = new ActionBuilder(new this.#formatter())
|
|
170
|
+
|
|
171
|
+
if(this.#hooks?.Format)
|
|
172
|
+
builder.withHooks(new this.#hooks.Format())
|
|
173
|
+
|
|
174
|
+
const runner = new ActionRunner(builder)
|
|
175
|
+
const formatResult = await runner.run(functions)
|
|
176
|
+
|
|
177
|
+
this.#emitStage(ctx.file, "format", "done")
|
|
178
|
+
|
|
179
|
+
return Object.assign(ctx, {formatResult})
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#shouldWrite = ctx => {
|
|
183
|
+
if(ctx.error)
|
|
184
|
+
return ctx
|
|
185
|
+
|
|
186
|
+
const result = this.#output != null && ctx?.formatResult
|
|
187
|
+
|
|
188
|
+
if(result)
|
|
189
|
+
return result
|
|
190
|
+
|
|
191
|
+
Object.assign(ctx, {status: "warning", warning: `No output content for ${ctx.file.path}`})
|
|
192
|
+
|
|
193
|
+
Notify.emit("update-data", {file: ctx.file, message: {kind: "output-size", value: 0}})
|
|
194
|
+
this.#emitStage(ctx.file, "write", "warning")
|
|
195
|
+
|
|
196
|
+
return false
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
#writeOutput = async ctx => {
|
|
200
|
+
if(ctx.error)
|
|
201
|
+
return ctx
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
this.#emitStage(ctx.file, "write", "active")
|
|
205
|
+
|
|
206
|
+
const {formatResult: content, output} = ctx
|
|
207
|
+
|
|
208
|
+
await output.write(content)
|
|
209
|
+
|
|
210
|
+
Notify.emit("update-data", {file: ctx.file, message: {kind: "output-size", value: Buffer.byteLength(content)}})
|
|
211
|
+
this.#emitStage(ctx.file, "write", "done")
|
|
212
|
+
|
|
213
|
+
return {...ctx, status: "success", output}
|
|
214
|
+
} catch(error) {
|
|
215
|
+
this.#emitStage(ctx.file, "write", "error")
|
|
216
|
+
|
|
217
|
+
return {...ctx, status: "error", error: Sass.new(`Writing file ${ctx.file}`, error)}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// -- Result categorization ------------------------------------------------
|
|
222
|
+
|
|
223
|
+
#categorize(settled, files) {
|
|
224
|
+
const succeeded = []
|
|
225
|
+
const warned = []
|
|
226
|
+
const errored = []
|
|
227
|
+
|
|
228
|
+
for(let i = 0; i < settled.length; i++) {
|
|
229
|
+
const entry = settled[i]
|
|
230
|
+
const file = files[i]
|
|
231
|
+
|
|
232
|
+
if(entry.status === "rejected") {
|
|
233
|
+
errored.push({input: file, error: entry.reason})
|
|
234
|
+
continue
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const val = entry.value
|
|
238
|
+
|
|
239
|
+
switch(val?.status) {
|
|
240
|
+
case "success":
|
|
241
|
+
succeeded.push({input: file, output: val.output})
|
|
242
|
+
break
|
|
243
|
+
case "warning":
|
|
244
|
+
warned.push({input: file, warning: val.warning})
|
|
245
|
+
break
|
|
246
|
+
case "error":
|
|
247
|
+
errored.push({input: file, error: val.error})
|
|
248
|
+
break
|
|
249
|
+
default:
|
|
250
|
+
errored.push({input: file, error: new Error(`Unknown status: ${val?.status}`)})
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return {succeeded, errored, warned}
|
|
255
|
+
}
|
|
256
|
+
}
|