@gesslar/bedoc 1.11.0 → 2.1.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 +304 -0
- package/src/CLIOutput.js +198 -0
- package/src/{core/Configuration.js → Configuration.js} +73 -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 +86 -38
- 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 -62
- 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/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 {loadDataFile} = 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
|
}
|
|
@@ -141,6 +142,7 @@ export default class Configuration {
|
|
|
141
142
|
return {
|
|
142
143
|
status: "success",
|
|
143
144
|
validated: true,
|
|
145
|
+
basePath: base,
|
|
144
146
|
...finalOptions,
|
|
145
147
|
}
|
|
146
148
|
}
|
|
@@ -150,17 +152,22 @@ export default class Configuration {
|
|
|
150
152
|
if(source === Environment.CLI)
|
|
151
153
|
return options
|
|
152
154
|
|
|
155
|
+
// `basePath` and `project` are consumed downstream as raw values (matching
|
|
156
|
+
// the shape the CLI provides); everything else is wrapped as {value, source}.
|
|
153
157
|
for(const [key, value] of Object.entries(options)) {
|
|
158
|
+
if(key === "basePath" || key === "project")
|
|
159
|
+
continue
|
|
160
|
+
|
|
154
161
|
options[key] = {value, source}
|
|
155
162
|
}
|
|
156
163
|
|
|
157
164
|
// We will need to inject some options if they are not available
|
|
158
165
|
const cwd = process.cwd()
|
|
159
|
-
const dir =
|
|
166
|
+
const dir = new DirectoryObject(cwd)
|
|
160
167
|
|
|
161
168
|
// Inject basePath if not available
|
|
162
169
|
if(!options.basePath)
|
|
163
|
-
options.basePath =
|
|
170
|
+
options.basePath = dir
|
|
164
171
|
|
|
165
172
|
// Add defaults which are missing
|
|
166
173
|
for(const [key, param] of Object.entries(ConfigurationParameters)) {
|
|
@@ -196,7 +203,7 @@ export default class Configuration {
|
|
|
196
203
|
errors.push(`Option \`${key}\` has no path type`)
|
|
197
204
|
|
|
198
205
|
// Check if pathType is a valid key in FdTypes
|
|
199
|
-
if(!fdTypes.includes(pathType))
|
|
206
|
+
if(!FS.fdTypes.includes(pathType))
|
|
200
207
|
errors.push(`Option \`${key}\` has invalid path type: ${pathType}`)
|
|
201
208
|
}
|
|
202
209
|
}
|
|
@@ -210,19 +217,23 @@ export default class Configuration {
|
|
|
210
217
|
* @param {object} entryOptions - The command line options.
|
|
211
218
|
* @returns {Promise<object[]>} All options from all sources.
|
|
212
219
|
*/
|
|
213
|
-
#findAllOptions(entryOptions) {
|
|
220
|
+
async #findAllOptions(entryOptions) {
|
|
221
|
+
const {basePath} = entryOptions
|
|
214
222
|
const allOptions = []
|
|
215
223
|
const environmentVariables = this.#getEnvironmentVariables()
|
|
224
|
+
|
|
216
225
|
if(environmentVariables)
|
|
217
226
|
allOptions.push({source: "environment", options: environmentVariables})
|
|
218
227
|
|
|
219
|
-
const packageJson = entryOptions?.
|
|
228
|
+
const packageJson = entryOptions?.project
|
|
229
|
+
|
|
220
230
|
if(packageJson) {
|
|
221
231
|
allOptions.push({source: "packageJson", options: packageJson})
|
|
222
232
|
} else {
|
|
223
|
-
const packageJsonFile =
|
|
224
|
-
|
|
225
|
-
|
|
233
|
+
const packageJsonFile = basePath.getFile("package.json")
|
|
234
|
+
|
|
235
|
+
if(await packageJsonFile.exists) {
|
|
236
|
+
const packageJson = await packageJsonFile.loadData()
|
|
226
237
|
|
|
227
238
|
if(packageJson.bedoc)
|
|
228
239
|
allOptions.push({source: "packageJson", options: packageJson.bedoc})
|
|
@@ -238,15 +249,17 @@ export default class Configuration {
|
|
|
238
249
|
if(useConfig) {
|
|
239
250
|
const configFile =
|
|
240
251
|
packageJson?.config
|
|
241
|
-
?
|
|
252
|
+
? new FileObject(packageJson.config)
|
|
242
253
|
: entryOptions.config?.value
|
|
243
|
-
?
|
|
244
|
-
:
|
|
254
|
+
? new FileObject(entryOptions.config.value)
|
|
255
|
+
: environmentVariables?.config
|
|
256
|
+
? new FileObject(environmentVariables.config)
|
|
257
|
+
: null
|
|
245
258
|
|
|
246
259
|
if(!configFile)
|
|
247
|
-
throw new
|
|
260
|
+
throw Sass.new("No config file specified")
|
|
248
261
|
|
|
249
|
-
const configObject =
|
|
262
|
+
const configObject = await configFile.loadData()
|
|
250
263
|
const subConfigName =
|
|
251
264
|
entryOptions?.sub ||
|
|
252
265
|
packageJson?.sub ||
|
|
@@ -273,7 +286,7 @@ export default class Configuration {
|
|
|
273
286
|
const subConfig = configObject.sub?.find(sub => sub.name === subConfigName)
|
|
274
287
|
|
|
275
288
|
if(!subConfig)
|
|
276
|
-
throw new
|
|
289
|
+
throw Sass.new(`No such subconfiguration \`${subConfigName}\``)
|
|
277
290
|
|
|
278
291
|
// We don't need this anymore
|
|
279
292
|
delete subConfig.name
|
|
@@ -328,19 +341,21 @@ export default class Configuration {
|
|
|
328
341
|
return acc
|
|
329
342
|
}, {})
|
|
330
343
|
|
|
331
|
-
const mappedOptions = await mapObject(mergedOptions,
|
|
332
|
-
|
|
333
|
-
value:
|
|
334
|
-
|
|
335
|
-
|
|
344
|
+
const mappedOptions = await Collection.mapObject(mergedOptions,
|
|
345
|
+
(option, value) => {
|
|
346
|
+
const {value: entryValue, source: entrySource} =
|
|
347
|
+
entryOptions[option] ?? {
|
|
348
|
+
value: undefined,
|
|
349
|
+
source: undefined,
|
|
350
|
+
}
|
|
336
351
|
|
|
337
|
-
|
|
352
|
+
const entryDefaulted = entrySource === "default"
|
|
338
353
|
|
|
339
|
-
|
|
340
|
-
|
|
354
|
+
if(entryValue && value !== entryValue)
|
|
355
|
+
return entryDefaulted ? value : entryValue
|
|
341
356
|
|
|
342
|
-
|
|
343
|
-
|
|
357
|
+
return value
|
|
358
|
+
})
|
|
344
359
|
|
|
345
360
|
// Last, but not least, add any defaulted options that are not in the
|
|
346
361
|
// 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
|
+
}
|