@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
package/dist/types/cli.d.ts.map
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gesslar/bedoc",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Pluggable documentation engine for any language and format",
|
|
5
5
|
"publisher": "gesslar",
|
|
6
6
|
"author": "gesslar",
|
|
@@ -10,9 +10,6 @@
|
|
|
10
10
|
"type": "git",
|
|
11
11
|
"url": "git+https://github.com/gesslar/BeDoc.git"
|
|
12
12
|
},
|
|
13
|
-
"bugs": {
|
|
14
|
-
"url": "https://github.com/gesslar/BeDoc/issues"
|
|
15
|
-
},
|
|
16
13
|
"bin": {
|
|
17
14
|
"bedoc": "src/cli.js"
|
|
18
15
|
},
|
|
@@ -32,30 +29,34 @@
|
|
|
32
29
|
},
|
|
33
30
|
"type": "module",
|
|
34
31
|
"types": "dist/types/index.d.ts",
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=24"
|
|
34
|
+
},
|
|
35
35
|
"scripts": {
|
|
36
|
-
"
|
|
37
|
-
"lint
|
|
36
|
+
"update": "npx npm-check-updates -u && npm install",
|
|
37
|
+
"lint": "npx eslint .",
|
|
38
|
+
"lint:fix": "npx eslint . --fix",
|
|
39
|
+
"pr": "gt submit -p --ai",
|
|
40
|
+
"patch": "npm version patch",
|
|
41
|
+
"minor": "npm version minor",
|
|
42
|
+
"major": "npm version major"
|
|
38
43
|
},
|
|
39
44
|
"dependencies": {
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"micromatch": "^4.0.8",
|
|
47
|
-
"node-fetch": "^3.3.2",
|
|
48
|
-
"yaml": "^2.7.0"
|
|
45
|
+
"@gesslar/actioneer": "^3.0.1",
|
|
46
|
+
"@gesslar/colours": "^1.0.0",
|
|
47
|
+
"@gesslar/negotiator": "^1.0.0",
|
|
48
|
+
"@gesslar/toolkit": "^5.5.1",
|
|
49
|
+
"commander": "^15.0.0",
|
|
50
|
+
"error-stack-parser": "^2.1.4"
|
|
49
51
|
},
|
|
50
52
|
"devDependencies": {
|
|
51
|
-
"@
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"form-data": "^4.0.1"
|
|
53
|
+
"@gesslar/uglier": "^2.4.1",
|
|
54
|
+
"axios": "^1.17.0",
|
|
55
|
+
"chokidar": "^5.0.0",
|
|
56
|
+
"dotenv": "^17.4.2",
|
|
57
|
+
"eslint": "^10.4.1",
|
|
58
|
+
"form-data": "^4.0.5",
|
|
59
|
+
"node-fetch": "^3.3.2"
|
|
59
60
|
},
|
|
60
61
|
"keywords": [
|
|
61
62
|
"documentation",
|
package/src/Action.js
ADDED
package/src/BeDoc.js
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import {Contract} from "@gesslar/negotiator"
|
|
2
|
+
import {Data, Sass, Tantrum} from "@gesslar/toolkit"
|
|
3
|
+
import {hrtime} from "node:process"
|
|
4
|
+
|
|
5
|
+
import Configuration from "./Configuration.js"
|
|
6
|
+
import Conveyor from "./Conveyor.js"
|
|
7
|
+
import Discovery from "./Discovery.js"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @import {DirectoryObject, FileObject, Glog} from "@gesslar/toolkit"
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export default class BeDoc {
|
|
14
|
+
#glog
|
|
15
|
+
#options
|
|
16
|
+
#actionDefs
|
|
17
|
+
#validCrit
|
|
18
|
+
#validSchemas
|
|
19
|
+
#contract
|
|
20
|
+
#actions
|
|
21
|
+
#validateBeDocSchema
|
|
22
|
+
#hooks
|
|
23
|
+
#basePath
|
|
24
|
+
#cli
|
|
25
|
+
|
|
26
|
+
constructor({basePath, glog, cliOutput}) {
|
|
27
|
+
this.#glog = glog
|
|
28
|
+
this.#basePath = basePath
|
|
29
|
+
this.#cli = cliOutput
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolve and validate configuration from all sources (CLI, environment,
|
|
34
|
+
* package.json, config file). This runs independently of any BeDoc instance
|
|
35
|
+
* so the resulting config object can be built first and shared with other
|
|
36
|
+
* consumers (e.g. CLIOutput) before a BeDoc instance exists.
|
|
37
|
+
*
|
|
38
|
+
* @param {object} args
|
|
39
|
+
* @param {object} args.options - The raw options (with sources) to resolve
|
|
40
|
+
* @param {string} args.source - The environment BeDoc is running in
|
|
41
|
+
* @returns {Promise<object>} The validated configuration object
|
|
42
|
+
*/
|
|
43
|
+
static async resolveConfig({options, source}) {
|
|
44
|
+
const config = new Configuration()
|
|
45
|
+
|
|
46
|
+
return await config.validate({options, source})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Create a new instance of BeDoc.
|
|
51
|
+
*
|
|
52
|
+
* Configuration may be supplied pre-resolved (via {@link resolveConfig}) when
|
|
53
|
+
* a caller needs the config object before the instance exists — e.g. the CLI
|
|
54
|
+
* resolves it first to share with CLIOutput. Otherwise pass raw `options` and
|
|
55
|
+
* `source` and BeDoc resolves them itself, so any environment can construct a
|
|
56
|
+
* BeDoc in a single call.
|
|
57
|
+
*
|
|
58
|
+
* @param {object} args
|
|
59
|
+
* @param {object} [args.config] - Pre-validated configuration (see resolveConfig)
|
|
60
|
+
* @param {object} [args.options] - Raw options to resolve, if config is absent
|
|
61
|
+
* @param {string} [args.source] - The environment BeDoc is running in
|
|
62
|
+
* @param {DirectoryObject} [args.basePath] - The project base path
|
|
63
|
+
* @param {Glog} args.glog - The Glog logger instance
|
|
64
|
+
* @param {Function} args.validateBeDocSchema - The action schema validator
|
|
65
|
+
* @param {object} args.cliOutput - The CLI output instance
|
|
66
|
+
* @returns {Promise<BeDoc>} A new instance of BeDoc
|
|
67
|
+
*/
|
|
68
|
+
static async new({
|
|
69
|
+
config, options, source, basePath, glog, validateBeDocSchema, cliOutput
|
|
70
|
+
}) {
|
|
71
|
+
const resolved = config ?? await BeDoc.resolveConfig({options, source})
|
|
72
|
+
const base = basePath ?? resolved.basePath ?? options?.basePath
|
|
73
|
+
|
|
74
|
+
const bedoc = new this({basePath: base, glog, cliOutput})
|
|
75
|
+
|
|
76
|
+
bedoc.#configure({config: resolved, glog, validateBeDocSchema})
|
|
77
|
+
|
|
78
|
+
const discovered = await bedoc.#discover()
|
|
79
|
+
|
|
80
|
+
if(!discovered) {
|
|
81
|
+
return {
|
|
82
|
+
status: "fail",
|
|
83
|
+
message: "No matching actions specified or discovered.",
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return await (await bedoc.#negotiate())
|
|
88
|
+
.#validateActions()
|
|
89
|
+
.#setupHooks()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Apply validated configuration to this instance.
|
|
94
|
+
*
|
|
95
|
+
* @param {object} config - The validated configuration object
|
|
96
|
+
* @param {Glog} glog - The Glog logger instance
|
|
97
|
+
* @param {Function} validateBeDocSchema - The action schema validator
|
|
98
|
+
*/
|
|
99
|
+
#configure({config, glog, validateBeDocSchema}) {
|
|
100
|
+
this.#glog = glog
|
|
101
|
+
this.#validateBeDocSchema = validateBeDocSchema
|
|
102
|
+
|
|
103
|
+
if(config.debug && config.debugLevel > 0)
|
|
104
|
+
glog.withLogLevel(config.debugLevel)
|
|
105
|
+
else
|
|
106
|
+
glog.withLogLevel(0)
|
|
107
|
+
|
|
108
|
+
if(config.status === "error")
|
|
109
|
+
throw Tantrum.new("BeDoc configuration failed", config.errors)
|
|
110
|
+
|
|
111
|
+
glog.debug("Creating new BeDoc instance with options: `%o`", 4, config)
|
|
112
|
+
|
|
113
|
+
this.#options = config
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Discover and filter actions that match the configuration criteria.
|
|
118
|
+
*
|
|
119
|
+
* @returns {Promise<boolean>} Whether matching actions were found
|
|
120
|
+
*/
|
|
121
|
+
async #discover() {
|
|
122
|
+
const glog = this.#glog
|
|
123
|
+
const options = this.#options
|
|
124
|
+
|
|
125
|
+
const discovery = new Discovery({options, glog})
|
|
126
|
+
|
|
127
|
+
this.#actionDefs = await discovery.discoverActions({
|
|
128
|
+
parser: options.parser,
|
|
129
|
+
formatter: options.formatter,
|
|
130
|
+
}, this.#validateBeDocSchema)
|
|
131
|
+
|
|
132
|
+
this.#validCrit = discovery.satisfyCriteria(this.#actionDefs, options)
|
|
133
|
+
|
|
134
|
+
glog.debug("Actions that met criteria %o", 4, this.#validCrit)
|
|
135
|
+
|
|
136
|
+
return !Object.values(this.#validCrit).some(arr => arr.length === 0)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Negotiate contracts between discovered parsers and formatters.
|
|
141
|
+
*
|
|
142
|
+
* @returns {Promise<BeDoc>} This object for chaining.
|
|
143
|
+
*/
|
|
144
|
+
async #negotiate() {
|
|
145
|
+
const glog = this.#glog
|
|
146
|
+
const validSchemas = {parser: [], formatter: []}
|
|
147
|
+
|
|
148
|
+
let formatters = this.#validCrit.formatter.length
|
|
149
|
+
|
|
150
|
+
while(formatters--) {
|
|
151
|
+
const formatter = this.#validCrit.formatter[formatters]
|
|
152
|
+
const {terms: consumes} = formatter
|
|
153
|
+
const satisfied = []
|
|
154
|
+
|
|
155
|
+
for(const parser of this.#validCrit.parser) {
|
|
156
|
+
try {
|
|
157
|
+
const {terms: provides} = parser
|
|
158
|
+
|
|
159
|
+
const contract = await Contract.negotiate(provides, consumes)
|
|
160
|
+
|
|
161
|
+
satisfied.push({...parser, contract})
|
|
162
|
+
} catch(err) {
|
|
163
|
+
glog.error(err)
|
|
164
|
+
|
|
165
|
+
glog.debug("%o action incompatible with %o action", 3,
|
|
166
|
+
parser.action.default.meta.input,
|
|
167
|
+
formatter.action.default.meta.format
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if(satisfied.length > 0) {
|
|
173
|
+
validSchemas.formatter.push(formatter)
|
|
174
|
+
validSchemas.parser.push(...satisfied)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
this.#validSchemas = validSchemas
|
|
179
|
+
|
|
180
|
+
return this
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Validate that exactly one action per type was negotiated, then
|
|
185
|
+
* instantiate the action classes.
|
|
186
|
+
*
|
|
187
|
+
* @returns {BeDoc} This object for chaining.
|
|
188
|
+
*/
|
|
189
|
+
#validateActions() {
|
|
190
|
+
const glog = this.#glog
|
|
191
|
+
|
|
192
|
+
const schemas = {}
|
|
193
|
+
|
|
194
|
+
for(const [key, value] of Object.entries(this.#validSchemas)) {
|
|
195
|
+
if(value.length === 0)
|
|
196
|
+
throw Sass.new(`No matching '${key}' action found`)
|
|
197
|
+
|
|
198
|
+
if(value.length > 1)
|
|
199
|
+
throw Sass.new(`Multiple matching '${key}' actions found`)
|
|
200
|
+
|
|
201
|
+
schemas[key] = value[0]
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
this.#validSchemas = schemas
|
|
205
|
+
this.#contract = schemas.parser.contract
|
|
206
|
+
|
|
207
|
+
glog.debug("Contracts satisfied between parser and formatter", 2)
|
|
208
|
+
|
|
209
|
+
const actions = {}
|
|
210
|
+
|
|
211
|
+
for(const [, {action}] of Object.entries(this.#validSchemas)) {
|
|
212
|
+
const {kind} = action.default.meta
|
|
213
|
+
|
|
214
|
+
glog.debug("Assigning %o action", 2, kind)
|
|
215
|
+
|
|
216
|
+
actions[kind] = action.default
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
this.#actions = actions
|
|
220
|
+
|
|
221
|
+
return this
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async #setupHooks() {
|
|
225
|
+
/** @type {Glog} */
|
|
226
|
+
const glog = this.#glog
|
|
227
|
+
|
|
228
|
+
if(!this.#options.hooks)
|
|
229
|
+
return this
|
|
230
|
+
|
|
231
|
+
/** @type {FileObject} */
|
|
232
|
+
const hooksFile = this.#options.hooks
|
|
233
|
+
|
|
234
|
+
if(!hooksFile)
|
|
235
|
+
return this
|
|
236
|
+
|
|
237
|
+
if(!await hooksFile.exists) {
|
|
238
|
+
glog.warn(`File not found: ${hooksFile.path}`)
|
|
239
|
+
|
|
240
|
+
return this
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const loaded = await hooksFile.import()
|
|
244
|
+
if(!loaded)
|
|
245
|
+
return this
|
|
246
|
+
|
|
247
|
+
const hooks = {}
|
|
248
|
+
if(loaded.Parse && Data.isType(loaded.Parse, "Function"))
|
|
249
|
+
hooks.Parse = loaded.Parse
|
|
250
|
+
|
|
251
|
+
if(loaded.Format && Data.isType(loaded.Format, "Function"))
|
|
252
|
+
hooks.Format = loaded.Format
|
|
253
|
+
|
|
254
|
+
if(Data.isEmpty(hooks)) {
|
|
255
|
+
glog.warn(`No hooks found in ${hooksFile.path}`)
|
|
256
|
+
|
|
257
|
+
return this
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
this.#hooks = hooks
|
|
261
|
+
|
|
262
|
+
return this
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async processFiles() {
|
|
266
|
+
const glog = this.#glog
|
|
267
|
+
|
|
268
|
+
glog.debug("Starting file processing with conveyor", 1)
|
|
269
|
+
|
|
270
|
+
const {input, output, maxConcurrent} = this.#options
|
|
271
|
+
|
|
272
|
+
if(!input?.length)
|
|
273
|
+
throw Sass.new("No input files specified")
|
|
274
|
+
|
|
275
|
+
const conveyor = new Conveyor({
|
|
276
|
+
parser: this.#actions.parser,
|
|
277
|
+
formatter: this.#actions.formatter,
|
|
278
|
+
contract: this.#contract,
|
|
279
|
+
hooks: this.#hooks,
|
|
280
|
+
glog,
|
|
281
|
+
output,
|
|
282
|
+
basePath: this.#basePath,
|
|
283
|
+
cli: this.#cli
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
const processStart = hrtime.bigint()
|
|
287
|
+
const processResult = await conveyor.convey(input, maxConcurrent)
|
|
288
|
+
const processEnd = hrtime.bigint()
|
|
289
|
+
|
|
290
|
+
glog.debug("Conveyor complete", 1)
|
|
291
|
+
|
|
292
|
+
const result = {
|
|
293
|
+
totalFiles: input.length,
|
|
294
|
+
succeeded: processResult.succeeded,
|
|
295
|
+
warned: processResult.warned,
|
|
296
|
+
errored: processResult.errored,
|
|
297
|
+
duration: ((Number(processEnd - processStart)) / 1_000_000).toFixed(2),
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
glog.debug("File processing complete", 1)
|
|
301
|
+
|
|
302
|
+
return result
|
|
303
|
+
}
|
|
304
|
+
}
|
package/src/CLIOutput.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import c from "@gesslar/colours"
|
|
2
|
+
import {FileSystem as FS, Notify, Term} from "@gesslar/toolkit"
|
|
3
|
+
import process from "node:process"
|
|
4
|
+
import {stripVTControlCharacters} from "node:util"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Glyph sets for the framed output blocks. The `unicode` set is used when the
|
|
8
|
+
* terminal can render box-drawing/geometric characters; `ascii` is the plain
|
|
9
|
+
* fallback. In the ascii set the status markers are shape-distinct (not just
|
|
10
|
+
* colour-distinct) so they remain legible when colour is also unavailable.
|
|
11
|
+
*/
|
|
12
|
+
const GLYPHS = {
|
|
13
|
+
unicode: {
|
|
14
|
+
upper: "╭▸", upperDone: "╭─", mid: "│", lower: "╰▸", lowerDone: "╰─",
|
|
15
|
+
pending: "□", active: "▸", success: "■", warning: "■", error: "■",
|
|
16
|
+
},
|
|
17
|
+
ascii: {
|
|
18
|
+
upper: ",>", upperDone: ",-", mid: "|", lower: "`>", lowerDone: "`-",
|
|
19
|
+
pending: ".", active: ">", success: "#", warning: "!", error: "x",
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Rough check for whether the terminal can render Unicode glyphs. Mirrors the
|
|
25
|
+
* common heuristic: everything outside the raw Linux kernel console is assumed
|
|
26
|
+
* capable on POSIX; on Windows, only modern terminals are.
|
|
27
|
+
*
|
|
28
|
+
* @returns {boolean} True if Unicode glyphs are safe to emit.
|
|
29
|
+
*/
|
|
30
|
+
function supportsUnicode() {
|
|
31
|
+
if(process.platform === "win32")
|
|
32
|
+
return Boolean(process.env.WT_SESSION || process.env.TERM_PROGRAM)
|
|
33
|
+
|
|
34
|
+
return process.env.TERM !== "linux"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Handles CLI output for the BeDoc pipeline, rendering progress and status
|
|
39
|
+
* for each file as it moves through the read → parse → validate → format →
|
|
40
|
+
* write stages.
|
|
41
|
+
*
|
|
42
|
+
* Listens to a {@link Notify} instance and tracks per-file state in an
|
|
43
|
+
* internal map so that output can be updated in-place as each stage completes.
|
|
44
|
+
*
|
|
45
|
+
* Each file is drawn as a framed block: an Input line (top border), one line
|
|
46
|
+
* per pipeline stage, and an Output line (bottom border). Colour is emitted
|
|
47
|
+
* only when {@link Term.hasColor} is true (honouring NO_COLOR/FORCE_COLOR), and
|
|
48
|
+
* the glyph set degrades to ASCII when Unicode is unsupported.
|
|
49
|
+
*/
|
|
50
|
+
export default class CLIOutput {
|
|
51
|
+
/** Pipeline stages, in order, matching the Conveyor activity names. */
|
|
52
|
+
#stages = ["read", "parse", "validate", "format", "write"]
|
|
53
|
+
|
|
54
|
+
/** The active glyph set (unicode or ascii). */
|
|
55
|
+
#g = supportsUnicode() ? GLYPHS.unicode : GLYPHS.ascii
|
|
56
|
+
|
|
57
|
+
#basePath
|
|
58
|
+
|
|
59
|
+
/** When true, only Input/Output lines are drawn (no per-stage lines). */
|
|
60
|
+
#terse
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Tracks per-file state keyed by file path/identifier.
|
|
64
|
+
*
|
|
65
|
+
* @type {Map<string, object>}
|
|
66
|
+
*/
|
|
67
|
+
#files = new Map()
|
|
68
|
+
|
|
69
|
+
constructor({config}) {
|
|
70
|
+
this.#basePath = config.basePath
|
|
71
|
+
this.#terse = Boolean(config.terse)
|
|
72
|
+
|
|
73
|
+
Notify.on("conveyor-start", this.#conveyorStart)
|
|
74
|
+
Notify.on("update-data", this.#updateData)
|
|
75
|
+
|
|
76
|
+
c.alias.set("border", "{F033}")
|
|
77
|
+
c.alias.set("fileName", "{<B}")
|
|
78
|
+
c.alias.set("fileSize", "{F070}")
|
|
79
|
+
|
|
80
|
+
c.alias.set("success", "{F064}")
|
|
81
|
+
c.alias.set("pending", "{F033}")
|
|
82
|
+
c.alias.set("warning", "{F178}")
|
|
83
|
+
c.alias.set("error", "{F124}")
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Seeds the tracking map when the conveyor announces its work set.
|
|
88
|
+
*
|
|
89
|
+
* @param {Array<object>} ctx - One entry per file, each {file, output}.
|
|
90
|
+
*/
|
|
91
|
+
#conveyorStart = ctx => {
|
|
92
|
+
ctx.forEach(e => this.#files.set(e.file, {
|
|
93
|
+
output: e.output,
|
|
94
|
+
stages: Object.fromEntries(this.#stages.map(s => [s, "pending"])),
|
|
95
|
+
size: {
|
|
96
|
+
input: undefined, // undefined = pending, null = err, 0 = warning, number = success
|
|
97
|
+
output: undefined, // undefined = pending, null = err, 0 = warning, number = success
|
|
98
|
+
}
|
|
99
|
+
}))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Applies a single update for a file and re-renders. Messages are either a
|
|
104
|
+
* size update ({kind: "input-size"|"output-size", value}) or a stage
|
|
105
|
+
* transition ({kind: "stage", stage, state}).
|
|
106
|
+
*
|
|
107
|
+
* @param {object} update - The update payload.
|
|
108
|
+
* @param {object} update.file - The file the update pertains to.
|
|
109
|
+
* @param {object} update.message - The update message (see above).
|
|
110
|
+
*/
|
|
111
|
+
#updateData = ({file, message}) => {
|
|
112
|
+
const item = this.#files.get(file)
|
|
113
|
+
|
|
114
|
+
if(!item)
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
switch(message.kind) {
|
|
118
|
+
case "input-size":
|
|
119
|
+
item.size.input = message.value
|
|
120
|
+
break
|
|
121
|
+
|
|
122
|
+
case "output-size":
|
|
123
|
+
item.size.output = message.value
|
|
124
|
+
break
|
|
125
|
+
|
|
126
|
+
case "stage":
|
|
127
|
+
item.stages[message.stage] = message.state
|
|
128
|
+
break
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this.render()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Maps a stage state to its coloured glyph.
|
|
136
|
+
*
|
|
137
|
+
* @param {string} state - One of pending|active|done|warning|error.
|
|
138
|
+
* @returns {string} The coloured marker.
|
|
139
|
+
*/
|
|
140
|
+
#marker(state) {
|
|
141
|
+
switch(state) {
|
|
142
|
+
case "active": return `{pending}${this.#g.active}{/}`
|
|
143
|
+
case "done": return `{success}${this.#g.success}{/}`
|
|
144
|
+
case "warning": return `{warning}${this.#g.warning}{/}`
|
|
145
|
+
case "error": return `{error}${this.#g.error}{/}`
|
|
146
|
+
default: return `{pending}${this.#g.pending}{/}`
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Maps an input/output size to its coloured glyph. undefined is pending,
|
|
152
|
+
* null is error, 0 is warning, any other number is success.
|
|
153
|
+
*
|
|
154
|
+
* @param {number|null|undefined} size - The tracked size value.
|
|
155
|
+
* @returns {string} The coloured marker.
|
|
156
|
+
*/
|
|
157
|
+
#sizeMarker(size) {
|
|
158
|
+
if(size === undefined)
|
|
159
|
+
return `{pending}${this.#g.pending}{/}`
|
|
160
|
+
|
|
161
|
+
if(size === null)
|
|
162
|
+
return `{error}${this.#g.error}{/}`
|
|
163
|
+
|
|
164
|
+
if(size === 0)
|
|
165
|
+
return `{warning}${this.#g.warning}{/}`
|
|
166
|
+
|
|
167
|
+
return `{success}${this.#g.success}{/}`
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
render(cls=true) {
|
|
171
|
+
const lines = []
|
|
172
|
+
|
|
173
|
+
for(const [file, {output, stages, size}] of this.#files) {
|
|
174
|
+
const srcRel = FS.toRelativePath(this.#basePath.path, file.path)
|
|
175
|
+
const outRel = FS.toRelativePath(this.#basePath.path, output.path)
|
|
176
|
+
const done = size.output !== undefined
|
|
177
|
+
|
|
178
|
+
lines.push(
|
|
179
|
+
`{border}${done ? this.#g.upperDone : this.#g.upper}{/}${this.#sizeMarker(size.input)} Input {fileName}${srcRel}{B>}` +
|
|
180
|
+
(size.input ? ` ({fileSize}${size.input.toLocaleString()}{/} bytes)` : "")
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if(!this.#terse)
|
|
184
|
+
for(const stage of this.#stages)
|
|
185
|
+
lines.push(`{border}${this.#g.mid}{/} ${this.#marker(stages[stage])} ${stage}`)
|
|
186
|
+
|
|
187
|
+
lines.push(
|
|
188
|
+
`{border}${done ? this.#g.lowerDone : this.#g.lower}{/}${this.#sizeMarker(size.output)} Output {fileName}${outRel}{/}` +
|
|
189
|
+
(size.output ? ` ({fileSize}${size.output.toLocaleString()}{/} bytes)` : "")
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const out = c`${lines.join("\n")}\n`
|
|
194
|
+
|
|
195
|
+
cls && Term.cls()
|
|
196
|
+
Term.write(Term.hasColor ? out : stripVTControlCharacters(out))
|
|
197
|
+
}
|
|
198
|
+
}
|