@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.
Files changed (89) hide show
  1. package/LICENSE.txt +12 -0
  2. package/README.md +15 -3
  3. package/dist/schema/bedoc.action.json +42 -0
  4. package/dist/types/Action.d.ts +3 -0
  5. package/dist/types/Action.d.ts.map +1 -0
  6. package/dist/types/BeDoc.d.ts +208 -0
  7. package/dist/types/BeDoc.d.ts.map +1 -0
  8. package/dist/types/Configuration.d.ts +11 -0
  9. package/dist/types/Configuration.d.ts.map +1 -0
  10. package/dist/types/ConfigurationParameters.d.ts +3 -0
  11. package/dist/types/ConfigurationParameters.d.ts.map +1 -0
  12. package/dist/types/Conveyor.d.ts +27 -0
  13. package/dist/types/Conveyor.d.ts.map +1 -0
  14. package/dist/types/Discovery.d.ts +215 -0
  15. package/dist/types/Discovery.d.ts.map +1 -0
  16. package/dist/types/Environment.d.ts +3 -0
  17. package/dist/types/Environment.d.ts.map +1 -0
  18. package/dist/types/Logger.d.ts +47 -0
  19. package/dist/types/Logger.d.ts.map +1 -0
  20. package/dist/types/Schema.d.ts +3 -0
  21. package/dist/types/Schema.d.ts.map +1 -0
  22. package/dist/types/cli.d.ts +2 -2
  23. package/dist/types/cli.d.ts.map +1 -10
  24. package/package.json +24 -23
  25. package/src/Action.js +9 -0
  26. package/src/BeDoc.js +276 -0
  27. package/src/CLIOutput.js +198 -0
  28. package/src/{core/Configuration.js → Configuration.js} +72 -58
  29. package/src/{core/ConfigurationParameters.js → ConfigurationParameters.js} +35 -27
  30. package/src/Conveyor.js +256 -0
  31. package/src/Discovery.js +442 -0
  32. package/src/Environment.js +8 -0
  33. package/src/{core/Logger.js → Logger.js} +30 -18
  34. package/src/Schema.js +6 -0
  35. package/src/cli.js +77 -34
  36. package/tsconfig.types.json +42 -0
  37. package/LICENSE +0 -24
  38. package/dist/types/core/ActionManager.d.ts +0 -58
  39. package/dist/types/core/ActionManager.d.ts.map +0 -10
  40. package/dist/types/core/Configuration.d.ts +0 -27
  41. package/dist/types/core/Configuration.d.ts.map +0 -10
  42. package/dist/types/core/ConfigurationParameters.d.ts +0 -38
  43. package/dist/types/core/ConfigurationParameters.d.ts.map +0 -10
  44. package/dist/types/core/Conveyor.d.ts +0 -49
  45. package/dist/types/core/Conveyor.d.ts.map +0 -10
  46. package/dist/types/core/Core.d.ts +0 -48
  47. package/dist/types/core/Core.d.ts.map +0 -10
  48. package/dist/types/core/Discovery.d.ts +0 -73
  49. package/dist/types/core/Discovery.d.ts.map +0 -10
  50. package/dist/types/core/HookManager.d.ts +0 -60
  51. package/dist/types/core/HookManager.d.ts.map +0 -10
  52. package/dist/types/core/Logger.d.ts +0 -63
  53. package/dist/types/core/Logger.d.ts.map +0 -10
  54. package/dist/types/core/action/ParseManager.d.ts +0 -8
  55. package/dist/types/core/action/ParseManager.d.ts.map +0 -10
  56. package/dist/types/core/action/PrintManager.d.ts +0 -8
  57. package/dist/types/core/action/PrintManager.d.ts.map +0 -10
  58. package/dist/types/core/util/ActionUtil.d.ts +0 -35
  59. package/dist/types/core/util/ActionUtil.d.ts.map +0 -10
  60. package/dist/types/core/util/DataUtil.d.ts +0 -52
  61. package/dist/types/core/util/DataUtil.d.ts.map +0 -10
  62. package/dist/types/core/util/FDUtil.d.ts +0 -171
  63. package/dist/types/core/util/FDUtil.d.ts.map +0 -10
  64. package/dist/types/core/util/ModuleUtil.d.ts +0 -27
  65. package/dist/types/core/util/ModuleUtil.d.ts.map +0 -10
  66. package/dist/types/core/util/StringUtil.d.ts +0 -5
  67. package/dist/types/core/util/StringUtil.d.ts.map +0 -10
  68. package/dist/types/core/util/TypeSpec.d.ts +0 -42
  69. package/dist/types/core/util/TypeSpec.d.ts.map +0 -10
  70. package/dist/types/core/util/ValidUtil.d.ts +0 -29
  71. package/dist/types/core/util/ValidUtil.d.ts.map +0 -10
  72. package/src/core/ActionManager.js +0 -147
  73. package/src/core/ContractManager.js +0 -112
  74. package/src/core/Conveyor.js +0 -185
  75. package/src/core/Core.js +0 -166
  76. package/src/core/Discovery.js +0 -403
  77. package/src/core/HookManager.js +0 -143
  78. package/src/core/action/ParseManager.js +0 -7
  79. package/src/core/action/PrintManager.js +0 -7
  80. package/src/core/contract/ParseContract.js +0 -7
  81. package/src/core/contract/PrintContract.js +0 -7
  82. package/src/core/util/ActionUtil.js +0 -53
  83. package/src/core/util/ContractUtil.js +0 -63
  84. package/src/core/util/DataUtil.js +0 -540
  85. package/src/core/util/FDUtil.js +0 -388
  86. package/src/core/util/ModuleUtil.js +0 -40
  87. package/src/core/util/StringUtil.js +0 -11
  88. package/src/core/util/TypeSpec.js +0 -114
  89. package/src/core/util/ValidUtil.js +0 -50
@@ -1,24 +1,16 @@
1
- import process from "node:process"
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
- if(configValidationErrors.length > 0)
38
- throw new AggregateError(
39
- configValidationErrors,
40
- `ConfigurationParameters validation errors: `+
41
- `${configValidationErrors.join(", ")}`,
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 Error(`Invalid priority key: ${key}`)
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
- // Special for `input` and `exclude` because they can be a comma-
114
- // separated list of glob patterns.
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, "array"))
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
- finalOptions[key] = value.flat()
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
- pathType === fdType.FILE
134
- ? resolveFilename(value)
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 = resolveDirectory(cwd)
165
+ const dir = new DirectoryObject(cwd)
160
166
 
161
167
  // Inject basePath if not available
162
168
  if(!options.basePath)
163
- options.basePath = {value: dir, source}
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?.packageJson
227
+ const packageJson = entryOptions?.project
228
+
220
229
  if(packageJson) {
221
230
  allOptions.push({source: "packageJson", options: packageJson})
222
231
  } else {
223
- const packageJsonFile = composeFilename(process.cwd(), "package.json")
224
- if(fileExists(packageJsonFile)) {
225
- const packageJson = loadJson(packageJsonFile)
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
- ? resolveFilename(packageJson?.config)
251
+ ? new FileObject(packageJson.config)
242
252
  : entryOptions.config?.value
243
- ? resolveFilename(entryOptions.config.value)
244
- : null
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 Error("No config file specified")
259
+ throw Sass.new("No config file specified")
248
260
 
249
- const configObject = loadJson(configFile)
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 Error(`No such subconfiguration \`${subConfigName}\``)
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, (option, value) => {
332
- const {value: entryValue, source: entrySource} = entryOptions[option] ?? {
333
- value: undefined,
334
- source: undefined,
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
- const entryDefaulted = entrySource === "default"
351
+ const entryDefaulted = entrySource === "default"
338
352
 
339
- if(entryValue && value !== entryValue)
340
- return entryDefaulted ? value : entryValue
353
+ if(entryValue && value !== entryValue)
354
+ return entryDefaulted ? value : entryValue
341
355
 
342
- return value
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 * as DataUtil from "./util/DataUtil.js"
1
+ import {Data} from "@gesslar/toolkit"
2
2
 
3
- const {newTypeSpec} = DataUtil
4
-
5
- const ConfigurationParameters = Object.freeze({
3
+ const ConfigurationParameters = Data.deepFreezeObject({
6
4
  input: {
7
5
  short: "i",
8
6
  param: "file",
9
- description: "Comma-separated glob patterns to match files",
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: "Comma-separated glob patterns to exclude files",
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: "printer",
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
- printer: {
79
+ formatter: {
82
80
  short: "P",
83
81
  param: "file",
84
- description: "Custom printer JS file",
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 printers",
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 = Object.freeze(["exclude", "input"])
153
+ const ConfigurationPriorityKeys = Data.deepFreezeObject(["exclude", "input"])
149
154
 
150
- export {ConfigurationParameters, ConfigurationPriorityKeys}
155
+ export {
156
+ ConfigurationParameters,
157
+ ConfigurationPriorityKeys
158
+ }
@@ -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
+ }