@jti/adoc2typst2pdf 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.adoc +21 -0
- package/README.adoc +2 -0
- package/adoc2typst2pdf.js +761 -0
- package/assembler-typst.dist.yml +27 -0
- package/docbook2typst.js +1195 -0
- package/install.js +27 -0
- package/package.json +33 -0
|
@@ -0,0 +1,761 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* adoc2typst2pdf.js
|
|
5
|
+
*
|
|
6
|
+
* Drop-in replacement for asciidoctor-pdf using the pipeline:
|
|
7
|
+
* asciidoctor -b docbook → docbook2typst.js → typst compile
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node adoc2typst2pdf.js [options] <input.adoc>
|
|
11
|
+
*
|
|
12
|
+
* Recognized options (all others are silently ignored, so this can replace
|
|
13
|
+
* asciidoctor-pdf on existing command lines):
|
|
14
|
+
* -a name[=value] AsciiDoc attribute definition (repeatable)
|
|
15
|
+
* -o <file> Output PDF path (default: <input>.pdf)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const { spawn, execFileSync } = require('child_process')
|
|
21
|
+
const fs = require('fs')
|
|
22
|
+
const path = require('path')
|
|
23
|
+
|
|
24
|
+
const VERSION = '1.0.0'
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
// ── 1. Parse CLI arguments ────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
const argv = process.argv.slice(2)
|
|
30
|
+
|
|
31
|
+
if (argv.includes('--version')) {
|
|
32
|
+
console.log(VERSION)
|
|
33
|
+
process.exit(0)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (argv.length === 0 || argv.includes('--help') || argv.includes('-h')) {
|
|
37
|
+
console.log(`
|
|
38
|
+
Usage: node adoc2typst2pdf.js [options] <input.adoc|->
|
|
39
|
+
|
|
40
|
+
Converts an AsciiDoc file to PDF using the pipeline:
|
|
41
|
+
asciidoctor -b docbook → docbook2typst.js → typst compile
|
|
42
|
+
|
|
43
|
+
Requires asciidoctor and typst.
|
|
44
|
+
Pass - as the input file to read AsciiDoc from stdin (-o is required in that case).
|
|
45
|
+
|
|
46
|
+
Options:
|
|
47
|
+
-a name[=value] AsciiDoc attribute definition (passed through to
|
|
48
|
+
asciidoctor; may be repeated)
|
|
49
|
+
-d, --doctype <type> Set the document type (passed through to asciidoctor)
|
|
50
|
+
-n, --section-numbers Auto-number section titles (passed through to asciidoctor)
|
|
51
|
+
-o <file> Output PDF path (required when reading from stdin;
|
|
52
|
+
default for file input: <input-dir>/<component>-<date>.pdf)
|
|
53
|
+
-S, --safe-mode <lvl> Set safe mode: unsafe, safe, server, secure
|
|
54
|
+
(passed through to asciidoctor)
|
|
55
|
+
--safe Set safe mode to "safe" (passed through to asciidoctor)
|
|
56
|
+
-t, --typst-template <file>
|
|
57
|
+
Path to a Typst preamble file. When provided, the
|
|
58
|
+
generated .typ file is prefixed with an import of
|
|
59
|
+
this file and title/subtitle are passed to typst via
|
|
60
|
+
--input. When omitted, no preamble is added.
|
|
61
|
+
-v, --verbose Show full command lines for each pipeline step
|
|
62
|
+
--version Print the version and exit
|
|
63
|
+
-h, --help Show this help and exit
|
|
64
|
+
`)
|
|
65
|
+
process.exit(0)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Flags that take a following argument but whose value we don't need.
|
|
69
|
+
const SKIP_VALUE_FLAGS = new Set([
|
|
70
|
+
'-D', '--destination-dir',
|
|
71
|
+
'-B', '--base-dir',
|
|
72
|
+
'-T', '--template-dir',
|
|
73
|
+
'-E', '--template-engine',
|
|
74
|
+
'--failure-level',
|
|
75
|
+
'--log-level',
|
|
76
|
+
'-I', '--load-path',
|
|
77
|
+
'--theme', '--pdf-theme'
|
|
78
|
+
])
|
|
79
|
+
|
|
80
|
+
const attributes = [] // collected -a / --attribute entries
|
|
81
|
+
const safeArgs = [] // collected --safe / --safe-mode / -S entries
|
|
82
|
+
const requireArgs = [] // collected --require / -r entries
|
|
83
|
+
let sectionNumbers = false
|
|
84
|
+
let doctype = null
|
|
85
|
+
let typstTemplate = null
|
|
86
|
+
let verbose = false
|
|
87
|
+
let inputFile = null
|
|
88
|
+
let outputFile = null
|
|
89
|
+
|
|
90
|
+
// Process CLI parameters.
|
|
91
|
+
for (let i = 0; i < argv.length; i++) {
|
|
92
|
+
const arg = argv[i]
|
|
93
|
+
|
|
94
|
+
// -a name[=value]
|
|
95
|
+
if (arg === '-a' || arg === '--attribute') {
|
|
96
|
+
const val = argv[++i]
|
|
97
|
+
if (val !== undefined) attributes.push(val)
|
|
98
|
+
continue
|
|
99
|
+
}
|
|
100
|
+
if (arg.startsWith('-a') && arg.length > 2) {
|
|
101
|
+
attributes.push(arg.slice(2))
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
if (arg.startsWith('--attribute=')) {
|
|
105
|
+
attributes.push(arg.slice('--attribute='.length))
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// -d / --doctype <type>
|
|
110
|
+
if (arg === '-d' || arg === '--doctype') {
|
|
111
|
+
const val = argv[++i]
|
|
112
|
+
if (val !== undefined) doctype = val
|
|
113
|
+
continue
|
|
114
|
+
}
|
|
115
|
+
if (arg.startsWith('--doctype=')) {
|
|
116
|
+
doctype = arg.slice('--doctype='.length)
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// -n / --section-numbers (boolean flag)
|
|
121
|
+
if (arg === '-n' || arg === '--section-numbers') {
|
|
122
|
+
sectionNumbers = true
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// --require (string)
|
|
127
|
+
if (arg === '--require' || arg === '-r') {
|
|
128
|
+
const val = argv[++i]
|
|
129
|
+
if (val !== undefined) requireArgs.push('-r', val)
|
|
130
|
+
continue
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// --safe (boolean flag)
|
|
134
|
+
if (arg === '--safe') {
|
|
135
|
+
safeArgs.push('--safe')
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
// --safe-mode <level> / -S <level>
|
|
139
|
+
if (arg === '--safe-mode' || arg === '-S') {
|
|
140
|
+
const val = argv[++i]
|
|
141
|
+
if (val !== undefined) safeArgs.push('--safe-mode', val)
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
if (arg.startsWith('--safe-mode=')) {
|
|
145
|
+
safeArgs.push(arg)
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// -t / --typst-template <file>
|
|
150
|
+
if (arg === '-t' || arg === '--typst-template') {
|
|
151
|
+
const val = argv[++i]
|
|
152
|
+
if (val !== undefined) typstTemplate = val
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
if (arg.startsWith('--typst-template=')) {
|
|
156
|
+
typstTemplate = arg.slice('--typst-template='.length)
|
|
157
|
+
continue
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// -v / --verbose
|
|
161
|
+
if (arg === '-v' || arg === '--verbose') {
|
|
162
|
+
verbose = true
|
|
163
|
+
continue
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// -o / --out-file
|
|
167
|
+
if (arg === '-o' || arg === '--out-file') {
|
|
168
|
+
outputFile = argv[++i]
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
171
|
+
if (arg.startsWith('--out-file=')) {
|
|
172
|
+
outputFile = arg.slice('--out-file='.length)
|
|
173
|
+
continue
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// flags whose value we skip
|
|
177
|
+
if (SKIP_VALUE_FLAGS.has(arg)) {
|
|
178
|
+
i++ // consume the next token
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
// long --flag=value forms that we skip
|
|
182
|
+
if (arg.startsWith('--') && arg.includes('=')) {
|
|
183
|
+
continue
|
|
184
|
+
}
|
|
185
|
+
// '-' on its own means read from stdin — treat as a positional
|
|
186
|
+
if (arg === '-') {
|
|
187
|
+
inputFile = arg
|
|
188
|
+
continue
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// other short/long flags with no value
|
|
192
|
+
if (arg.startsWith('-')) {
|
|
193
|
+
continue
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// positional argument → the AsciiDoc input file
|
|
197
|
+
inputFile = arg
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!inputFile) {
|
|
201
|
+
console.error('Error: no input file specified.')
|
|
202
|
+
console.error('Run with --help for usage information.')
|
|
203
|
+
process.exit(1)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const stdinMode = inputFile === '-'
|
|
207
|
+
|
|
208
|
+
if (stdinMode && !outputFile) {
|
|
209
|
+
console.error('Error: -o <file> is required when reading from stdin.')
|
|
210
|
+
process.exit(1)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (stdinMode) {
|
|
214
|
+
const adocFile = path.join(
|
|
215
|
+
path.dirname(outputFile),
|
|
216
|
+
path.basename(outputFile, path.extname(outputFile)) + '.adoc'
|
|
217
|
+
)
|
|
218
|
+
fs.writeFileSync(adocFile, fs.readFileSync(0, 'utf-8'), 'utf-8')
|
|
219
|
+
inputFile = adocFile
|
|
220
|
+
} else if (!fs.existsSync(inputFile)) {
|
|
221
|
+
console.error(`Error: input file not found: ${inputFile}`)
|
|
222
|
+
process.exit(1)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
// ── Derive Antora component title ─────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
// Walk upward from `startDir` looking for an antora.yml that declares a `title`.
|
|
229
|
+
function findAntoraTitle(startDir) {
|
|
230
|
+
let dir = path.resolve(startDir)
|
|
231
|
+
while (true) {
|
|
232
|
+
const candidate = path.join(dir, 'antora.yml')
|
|
233
|
+
if (fs.existsSync(candidate)) {
|
|
234
|
+
// Match: title: "quoted value" | title: 'quoted value' | title: bare value
|
|
235
|
+
const m = fs.readFileSync(candidate, 'utf-8')
|
|
236
|
+
.match(/^title:\s*(?:"([^"]*?)"|'([^']*?)'|(.*?))\s*$/m)
|
|
237
|
+
if (m) {
|
|
238
|
+
const title = (m[1] ?? m[2] ?? m[3] ?? '').trim()
|
|
239
|
+
if (title) return title
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const parent = path.dirname(dir)
|
|
243
|
+
if (parent === dir) break // reached filesystem root
|
|
244
|
+
dir = parent
|
|
245
|
+
}
|
|
246
|
+
return null
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Convert a title string to a filename-safe slug:
|
|
250
|
+
// spaces → underscores, strip punctuation, collapse runs of underscores.
|
|
251
|
+
function titleToSlug(title) {
|
|
252
|
+
return title
|
|
253
|
+
.replace(/\s+/g, '_') // spaces → underscores
|
|
254
|
+
.replace(/[^\w]/g, '') // remove anything not a word char (a-z, A-Z, 0-9, _)
|
|
255
|
+
.replace(/_+/g, '_') // collapse consecutive underscores
|
|
256
|
+
.replace(/^_|_$/g, '') // strip leading/trailing underscores
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const componentTitle =
|
|
260
|
+
findAntoraTitle(stdinMode ? process.cwd() : path.dirname(path.resolve(inputFile))) ||
|
|
261
|
+
findAntoraTitle(path.join(__dirname, 'docs')) ||
|
|
262
|
+
'document'
|
|
263
|
+
|
|
264
|
+
const componentName = titleToSlug(componentTitle)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
// ── Derive intermediate and final paths ───────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
const today = new Date()
|
|
270
|
+
const year = today.getFullYear()
|
|
271
|
+
const month = today.getMonth() + 1
|
|
272
|
+
const day = today.getDate()
|
|
273
|
+
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`
|
|
274
|
+
|
|
275
|
+
const inputBase = inputFile.replace(/\.(adoc|asciidoc|asc)$/i, '')
|
|
276
|
+
const xmlFile = inputBase + '.xml'
|
|
277
|
+
const typFile = inputBase + '.typ'
|
|
278
|
+
const pdfFile = outputFile ||
|
|
279
|
+
path.join(process.cwd(), `${componentName}-${dateStr}.pdf`)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
// ── 2. Check tool availability ────────────────────────────────────────────────
|
|
283
|
+
|
|
284
|
+
function toolExists(name) {
|
|
285
|
+
try {
|
|
286
|
+
execFileSync('which', [name], { stdio: 'ignore' })
|
|
287
|
+
return true
|
|
288
|
+
} catch {
|
|
289
|
+
return false
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const missing = ['asciidoctor'].filter(t => !toolExists(t))
|
|
294
|
+
if (missing.length > 0) {
|
|
295
|
+
console.error(
|
|
296
|
+
`Error: the following required tools are not on your PATH: ${missing.join(', ')}`
|
|
297
|
+
)
|
|
298
|
+
console.error('Please install them before running this script.')
|
|
299
|
+
process.exit(1)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
303
|
+
|
|
304
|
+
// Return p as a path relative to cwd; if p is not absolute, resolve it first.
|
|
305
|
+
function relPath(p) {
|
|
306
|
+
return path.relative(process.cwd(), path.resolve(p)) || p
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// For verbose display: only convert absolute paths, leave flags/values as-is.
|
|
310
|
+
function relIfAbs(s) {
|
|
311
|
+
return path.isAbsolute(s) ? relPath(s) : s
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function extractTitle(adocPath) {
|
|
315
|
+
try {
|
|
316
|
+
const lines = fs.readFileSync(adocPath, 'utf-8').split('\n')
|
|
317
|
+
for (const line of lines) {
|
|
318
|
+
const m = line.match(/^=\s+(.+)/)
|
|
319
|
+
if (m) return m[1].trim()
|
|
320
|
+
}
|
|
321
|
+
} catch { /* fall through */ }
|
|
322
|
+
return path.basename(adocPath, path.extname(adocPath))
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
// ── Timing ────────────────────────────────────────────────────────────────────
|
|
327
|
+
|
|
328
|
+
const t0 = performance.now()
|
|
329
|
+
|
|
330
|
+
function fmtElapsed() {
|
|
331
|
+
const ms = performance.now() - t0
|
|
332
|
+
const s = ms / 1000
|
|
333
|
+
if (s < 60) return `${s.toFixed(2)}s`
|
|
334
|
+
|
|
335
|
+
const m = Math.floor(s / 60)
|
|
336
|
+
const sRem = (s % 60).toFixed(2).padStart(5, '0')
|
|
337
|
+
return `${m}:${sRem}s`
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ── Command runner ─────────────────────────────────────────────────────────────
|
|
341
|
+
|
|
342
|
+
async function run(phase, cmd, args) {
|
|
343
|
+
const phaseStart = performance.now()
|
|
344
|
+
if (verbose) {
|
|
345
|
+
const display = cmd === process.execPath ? 'node' : cmd
|
|
346
|
+
console.log(`\n→ ${display} ${args.map(relIfAbs).join(' ')}`)
|
|
347
|
+
}
|
|
348
|
+
setPhase(phase)
|
|
349
|
+
|
|
350
|
+
return new Promise((resolve, reject) => {
|
|
351
|
+
// In non-verbose mode suppress child stdout/stderr so they don't tear
|
|
352
|
+
// the spinner; capture stderr to surface on failure.
|
|
353
|
+
const stdio = verbose ? 'inherit' : ['ignore', 'ignore', 'pipe']
|
|
354
|
+
const proc = spawn(cmd, args, { stdio })
|
|
355
|
+
|
|
356
|
+
let stderr = ''
|
|
357
|
+
if (proc.stderr) proc.stderr.on('data', d => { stderr += d })
|
|
358
|
+
|
|
359
|
+
proc.on('close', code => {
|
|
360
|
+
if (verbose) {
|
|
361
|
+
const ms = performance.now() - phaseStart
|
|
362
|
+
console.log(`Elapsed: ${(ms / 1000).toFixed(2)} secs`)
|
|
363
|
+
}
|
|
364
|
+
if (code !== 0) {
|
|
365
|
+
if (stderr) process.stderr.write(stderr)
|
|
366
|
+
const cmdLabel = cmd === process.execPath ? 'docbook2typst converter' : cmd
|
|
367
|
+
reject(new Error(`${cmdLabel} exited with an error.`))
|
|
368
|
+
} else {
|
|
369
|
+
resolve()
|
|
370
|
+
}
|
|
371
|
+
})
|
|
372
|
+
proc.on('error', reject)
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
// ── Setup: symlinks and build args ────────────────────────────────────────────
|
|
378
|
+
|
|
379
|
+
const typstRoot = path.dirname(path.dirname(path.resolve(pdfFile)))
|
|
380
|
+
const fontRoot = typstTemplate
|
|
381
|
+
? path.join(path.dirname(path.resolve(typstTemplate)), 'fonts')
|
|
382
|
+
: null
|
|
383
|
+
|
|
384
|
+
// Create a symlink idempotently. EEXIST is silently ignored so concurrent
|
|
385
|
+
// invocations don't race; the symlinks are stable build artifacts and are
|
|
386
|
+
// intentionally left in place after the process exits.
|
|
387
|
+
function ensureSymlink(target, linkPath, type) {
|
|
388
|
+
try {
|
|
389
|
+
fs.symlinkSync(target, linkPath, type)
|
|
390
|
+
} catch (err) {
|
|
391
|
+
if (err.code !== 'EEXIST') throw err
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
ensureSymlink(path.join(__dirname, 'typst'), path.join(typstRoot, 'typst'), 'dir')
|
|
396
|
+
|
|
397
|
+
const componentFolder = path.relative(typstRoot, outputFile).split(path.sep)[0]
|
|
398
|
+
const imageSymlinkDir = path.join(typstRoot, componentFolder)
|
|
399
|
+
fs.mkdirSync(imageSymlinkDir, { recursive: true })
|
|
400
|
+
ensureSymlink(path.join('..', componentFolder), path.join(imageSymlinkDir, componentFolder), 'dir')
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
// ── Concurrent progress display ───────────────────────────────────────────────
|
|
404
|
+
//
|
|
405
|
+
// Multiple concurrent invocations each own one line in a shared "progress
|
|
406
|
+
// block" at the bottom of the terminal. A file-based lock serialises all
|
|
407
|
+
// writes; each writer moves the cursor up to its line, rewrites it, then
|
|
408
|
+
// moves the cursor back down so the next writer finds the anchor in the
|
|
409
|
+
// expected position.
|
|
410
|
+
|
|
411
|
+
const isTTY = Boolean(process.stdout.isTTY)
|
|
412
|
+
const progressDir = path.join(typstRoot, '.progress')
|
|
413
|
+
const lockFile = path.join(progressDir, 'lock')
|
|
414
|
+
const totalFile = path.join(progressDir, 'total')
|
|
415
|
+
|
|
416
|
+
fs.mkdirSync(progressDir, { recursive: true })
|
|
417
|
+
cleanStaleSlots()
|
|
418
|
+
|
|
419
|
+
// Remove slot files whose owning process is no longer alive. When no live
|
|
420
|
+
// peers remain, reset the line counter so the next batch starts a fresh block.
|
|
421
|
+
function cleanStaleSlots() {
|
|
422
|
+
// Clear a stale TTY lock left by a crashed process.
|
|
423
|
+
try {
|
|
424
|
+
const lockPid = parseInt(fs.readFileSync(lockFile, 'utf-8').trim(), 10)
|
|
425
|
+
if (Number.isFinite(lockPid)) {
|
|
426
|
+
try { process.kill(lockPid, 0) }
|
|
427
|
+
catch (e) { if (e.code === 'ESRCH') try { fs.unlinkSync(lockFile) } catch {} }
|
|
428
|
+
}
|
|
429
|
+
} catch {}
|
|
430
|
+
|
|
431
|
+
let hasLive = false
|
|
432
|
+
let files
|
|
433
|
+
try { files = fs.readdirSync(progressDir) }
|
|
434
|
+
catch { return }
|
|
435
|
+
|
|
436
|
+
for (const f of files) {
|
|
437
|
+
if (!f.startsWith('slot-')) continue
|
|
438
|
+
const slotFile = path.join(progressDir, f)
|
|
439
|
+
try {
|
|
440
|
+
const pid = parseInt(fs.readFileSync(slotFile, 'utf-8').trim(), 10)
|
|
441
|
+
if (!Number.isFinite(pid)) {
|
|
442
|
+
try { fs.unlinkSync(slotFile) }
|
|
443
|
+
catch {} continue
|
|
444
|
+
}
|
|
445
|
+
process.kill(pid, 0) // throws ESRCH if dead
|
|
446
|
+
hasLive = true
|
|
447
|
+
} catch (e) {
|
|
448
|
+
if (e.code === 'ESRCH') try { fs.unlinkSync(slotFile) } catch {}
|
|
449
|
+
else hasLive = true // EPERM → process exists but belongs to another user
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
if (!hasLive) try { fs.writeFileSync(totalFile, '0', 'utf-8') } catch {}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Spin-acquire the file-based TTY write lock (held only for the duration of
|
|
456
|
+
// a single terminal write, so contention is brief).
|
|
457
|
+
function acquireTTYLock() {
|
|
458
|
+
const deadline = Date.now() + 5000
|
|
459
|
+
while (Date.now() < deadline) {
|
|
460
|
+
try {
|
|
461
|
+
fs.writeFileSync(lockFile, String(process.pid), { flag: 'wx' })
|
|
462
|
+
return
|
|
463
|
+
} catch (e) {
|
|
464
|
+
if (e.code !== 'EEXIST') throw e
|
|
465
|
+
const t = Date.now() + 1
|
|
466
|
+
while (Date.now() < t) {} // 1 ms busy-wait
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
fs.writeFileSync(lockFile, String(process.pid)) // stale lock: forcibly claim
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function releaseTTYLock() {
|
|
473
|
+
try { fs.unlinkSync(lockFile) }
|
|
474
|
+
catch {}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function readTotal() {
|
|
478
|
+
try { return parseInt(fs.readFileSync(totalFile, 'utf-8'), 10) || 0 }
|
|
479
|
+
catch { return 0 }
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Claim the lowest available slot index (one per concurrent invocation).
|
|
483
|
+
let mySlot = -1
|
|
484
|
+
function claimSlot() {
|
|
485
|
+
for (let n = 0; ; n++) {
|
|
486
|
+
try {
|
|
487
|
+
fs.writeFileSync(path.join(progressDir, `slot-${n}`), String(process.pid), { flag: 'wx' })
|
|
488
|
+
mySlot = n
|
|
489
|
+
return
|
|
490
|
+
} catch (e) {
|
|
491
|
+
if (e.code !== 'EEXIST') throw e
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function releaseSlot() {
|
|
497
|
+
if (mySlot < 0) return
|
|
498
|
+
try { fs.unlinkSync(path.join(progressDir, `slot-${mySlot}`)) }
|
|
499
|
+
catch {}
|
|
500
|
+
|
|
501
|
+
mySlot = -1
|
|
502
|
+
// If no slots remain, we are the last invocation: remove build symlinks so
|
|
503
|
+
// subsequent tools (e.g. htmltest) don't follow them into a loop.
|
|
504
|
+
try {
|
|
505
|
+
if (fs.readdirSync(progressDir).every(f => !f.startsWith('slot-'))) {
|
|
506
|
+
cleanupSymlinks()
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
catch {}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function cleanupSymlinks() {
|
|
513
|
+
// Shared typst theme symlink.
|
|
514
|
+
try {
|
|
515
|
+
fs.unlinkSync(path.join(typstRoot, 'typst'))
|
|
516
|
+
}
|
|
517
|
+
catch {}
|
|
518
|
+
|
|
519
|
+
// Per-component image symlinks: each lives at typstRoot/<dir>/<dir>.
|
|
520
|
+
try {
|
|
521
|
+
for (const entry of fs.readdirSync(typstRoot, { withFileTypes: true })) {
|
|
522
|
+
if (!entry.isDirectory() || entry.name.startsWith('.')) continue
|
|
523
|
+
const link = path.join(typstRoot, entry.name, entry.name)
|
|
524
|
+
try {
|
|
525
|
+
if (fs.lstatSync(link).isSymbolicLink()) fs.unlinkSync(link)
|
|
526
|
+
}
|
|
527
|
+
catch {}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
catch {}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Write the initial two-line block for this invocation (header + spinner),
|
|
534
|
+
// claiming the next two rows in the shared progress block. myLineIndex is
|
|
535
|
+
// set to the spinner line so that updateProgressLine targets it correctly.
|
|
536
|
+
let myLineIndex = -1
|
|
537
|
+
function initProgressLine(header, spinner) {
|
|
538
|
+
acquireTTYLock()
|
|
539
|
+
try {
|
|
540
|
+
const T = readTotal()
|
|
541
|
+
myLineIndex = T + 1 // spinner is the second of the two lines
|
|
542
|
+
process.stdout.write(`${header}\n${spinner}\n`)
|
|
543
|
+
fs.writeFileSync(totalFile, String(T + 2), 'utf-8')
|
|
544
|
+
} finally {
|
|
545
|
+
releaseTTYLock()
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Overwrite this invocation's line in-place. The cursor is left at the
|
|
550
|
+
// bottom of the progress block (base + totalLines) after every locked write,
|
|
551
|
+
// so the distance from cursor to our line is always (total − myLineIndex).
|
|
552
|
+
function updateProgressLine(text) {
|
|
553
|
+
if (myLineIndex < 0) return
|
|
554
|
+
acquireTTYLock()
|
|
555
|
+
try {
|
|
556
|
+
const dist = readTotal() - myLineIndex
|
|
557
|
+
process.stdout.write(`\x1b[${dist}A\r\x1b[2K${text}\x1b[${dist}B\r`)
|
|
558
|
+
}
|
|
559
|
+
finally {
|
|
560
|
+
releaseTTYLock()
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// ── Spinner state ─────────────────────────────────────────────────────────────
|
|
565
|
+
|
|
566
|
+
// Select spinners from the cli-spinners package.
|
|
567
|
+
const spinners = {
|
|
568
|
+
"dots": [ "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" ],
|
|
569
|
+
"dots2": [ "⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷" ],
|
|
570
|
+
"dots3": [ "⠋", "⠙", "⠚", "⠞", "⠖", "⠦", "⠴", "⠲", "⠳", "⠓" ],
|
|
571
|
+
"dots4": [ "⠄", "⠆", "⠇", "⠋", "⠙", "⠸", "⠰", "⠠", "⠰", "⠸", "⠙", "⠋", "⠇", "⠆" ],
|
|
572
|
+
"dots5": [ "⠋", "⠙", "⠚", "⠒", "⠂", "⠂", "⠒", "⠲", "⠴", "⠦", "⠖", "⠒", "⠐", "⠐", "⠒", "⠓", "⠋" ],
|
|
573
|
+
"dots6": [ "⠁", "⠉", "⠙", "⠚", "⠒", "⠂", "⠂", "⠒", "⠲", "⠴", "⠤", "⠄", "⠄", "⠤", "⠴", "⠲", "⠒", "⠂", "⠂", "⠒", "⠚", "⠙", "⠉", "⠁" ],
|
|
574
|
+
"dots7": [ "⠈", "⠉", "⠋", "⠓", "⠒", "⠐", "⠐", "⠒", "⠖", "⠦", "⠤", "⠠", "⠠", "⠤", "⠦", "⠖", "⠒", "⠐", "⠐", "⠒", "⠓", "⠋", "⠉", "⠈" ],
|
|
575
|
+
"dots8": [ "⠁", "⠁", "⠉", "⠙", "⠚", "⠒", "⠂", "⠂", "⠒", "⠲", "⠴", "⠤", "⠄", "⠄", "⠤", "⠠", "⠠", "⠤", "⠦", "⠖", "⠒", "⠐", "⠐", "⠒", "⠓", "⠋", "⠉", "⠈", "⠈" ],
|
|
576
|
+
"dots9": [ "⢹", "⢺", "⢼", "⣸", "⣇", "⡧", "⡗", "⡏" ],
|
|
577
|
+
"dots10": [ "⢄", "⢂", "⢁", "⡁", "⡈", "⡐", "⡠" ],
|
|
578
|
+
"dots11": [ "⠁", "⠂", "⠄", "⡀", "⢀", "⠠", "⠐", "⠈" ],
|
|
579
|
+
"dots12": [ "⢀⠀", "⡀⠀", "⠄⠀", "⢂⠀", "⡂⠀", "⠅⠀", "⢃⠀", "⡃⠀", "⠍⠀", "⢋⠀", "⡋⠀", "⠍⠁", "⢋⠁", "⡋⠁", "⠍⠉", "⠋⠉", "⠋⠉", "⠉⠙", "⠉⠙", "⠉⠩", "⠈⢙", "⠈⡙", "⢈⠩", "⡀⢙", "⠄⡙", "⢂⠩", "⡂⢘", "⠅⡘", "⢃⠨", "⡃⢐", "⠍⡐", "⢋⠠", "⡋⢀", "⠍⡁", "⢋⠁", "⡋⠁", "⠍⠉", "⠋⠉", "⠋⠉", "⠉⠙", "⠉⠙", "⠉⠩", "⠈⢙", "⠈⡙", "⠈⠩", "⠀⢙", "⠀⡙", "⠀⠩", "⠀⢘", "⠀⡘", "⠀⠨", "⠀⢐", "⠀⡐", "⠀⠠", "⠀⢀", "⠀⡀" ],
|
|
580
|
+
"dots13": [ "⣼", "⣹", "⢻", "⠿", "⡟", "⣏", "⣧", "⣶" ],
|
|
581
|
+
"dots14": [ "⠉⠉", "⠈⠙", "⠀⠹", "⠀⢸", "⠀⣰", "⢀⣠", "⣀⣀", "⣄⡀", "⣆⠀", "⡇⠀", "⠏⠀", "⠋⠁" ],
|
|
582
|
+
"dots8Bit": [ "⠀", "⠁", "⠂", "⠃", "⠄", "⠅", "⠆", "⠇", "⡀", "⡁", "⡂", "⡃", "⡄", "⡅", "⡆", "⡇", "⠈", "⠉", "⠊", "⠋", "⠌", "⠍", "⠎", "⠏", "⡈", "⡉", "⡊", "⡋", "⡌", "⡍", "⡎", "⡏", "⠐", "⠑", "⠒", "⠓", "⠔", "⠕", "⠖", "⠗", "⡐", "⡑", "⡒", "⡓", "⡔", "⡕", "⡖", "⡗", "⠘", "⠙", "⠚", "⠛", "⠜", "⠝", "⠞", "⠟", "⡘", "⡙", "⡚", "⡛", "⡜", "⡝", "⡞", "⡟", "⠠", "⠡", "⠢", "⠣", "⠤", "⠥", "⠦", "⠧", "⡠", "⡡", "⡢", "⡣", "⡤", "⡥", "⡦", "⡧", "⠨", "⠩", "⠪", "⠫", "⠬", "⠭", "⠮", "⠯", "⡨", "⡩", "⡪", "⡫", "⡬", "⡭", "⡮", "⡯", "⠰", "⠱", "⠲", "⠳", "⠴", "⠵", "⠶", "⠷", "⡰", "⡱", "⡲", "⡳", "⡴", "⡵", "⡶", "⡷", "⠸", "⠹", "⠺", "⠻", "⠼", "⠽", "⠾", "⠿", "⡸", "⡹", "⡺", "⡻", "⡼", "⡽", "⡾", "⡿", "⢀", "⢁", "⢂", "⢃", "⢄", "⢅", "⢆", "⢇", "⣀", "⣁", "⣂", "⣃", "⣄", "⣅", "⣆", "⣇", "⢈", "⢉", "⢊", "⢋", "⢌", "⢍", "⢎", "⢏", "⣈", "⣉", "⣊", "⣋", "⣌", "⣍", "⣎", "⣏", "⢐", "⢑", "⢒", "⢓", "⢔", "⢕", "⢖", "⢗", "⣐", "⣑", "⣒", "⣓", "⣔", "⣕", "⣖", "⣗", "⢘", "⢙", "⢚", "⢛", "⢜", "⢝", "⢞", "⢟", "⣘", "⣙", "⣚", "⣛", "⣜", "⣝", "⣞", "⣟", "⢠", "⢡", "⢢", "⢣", "⢤", "⢥", "⢦", "⢧", "⣠", "⣡", "⣢", "⣣", "⣤", "⣥", "⣦", "⣧", "⢨", "⢩", "⢪", "⢫", "⢬", "⢭", "⢮", "⢯", "⣨", "⣩", "⣪", "⣫", "⣬", "⣭", "⣮", "⣯", "⢰", "⢱", "⢲", "⢳", "⢴", "⢵", "⢶", "⢷", "⣰", "⣱", "⣲", "⣳", "⣴", "⣵", "⣶", "⣷", "⢸", "⢹", "⢺", "⢻", "⢼", "⢽", "⢾", "⢿", "⣸", "⣹", "⣺", "⣻", "⣼", "⣽", "⣾", "⣿" ],
|
|
583
|
+
"dotsCircle": [ "⢎ ", "⠎⠁", "⠊⠑", "⠈⠱", " ⡱", "⢀⡰", "⢄⡠", "⢆⡀" ],
|
|
584
|
+
"sand": [ "⠁", "⠂", "⠄", "⡀", "⡈", "⡐", "⡠", "⣀", "⣁", "⣂", "⣄", "⣌", "⣔", "⣤", "⣥", "⣦", "⣮", "⣶", "⣷", "⣿", "⡿", "⠿", "⢟", "⠟", "⡛", "⠛", "⠫", "⢋", "⠋", "⠍", "⡉", "⠉", "⠑", "⠡", "⢁" ],
|
|
585
|
+
"line": [ "-", "\\", "|", "/" ],
|
|
586
|
+
"line2": [ "⠂", "-", "–", "—", "–", "-" ],
|
|
587
|
+
"rollingLine": [ "/ ", " - ", " \\ ", " |", " |", " \\ ", " - ", "/ " ],
|
|
588
|
+
"pipe": [ "┤", "┘", "┴", "└", "├", "┌", "┬", "┐" ],
|
|
589
|
+
"simpleDots": [ ". ", ".. ", "...", " " ],
|
|
590
|
+
"simpleDotsScrolling": [ ". ", ".. ", "...", " ..", " .", " " ],
|
|
591
|
+
"star": [ "✶", "✸", "✹", "✺", "✹", "✷" ],
|
|
592
|
+
"star2": [ "+", "x", "*" ],
|
|
593
|
+
"flip": [ "_", "_", "_", "-", "`", "`", "'", "´", "-", "_", "_", "_" ],
|
|
594
|
+
"hamburger": [ "☱", "☲", "☴" ],
|
|
595
|
+
"growVertical": [ "▁", "▃", "▄", "▅", "▆", "▇", "▆", "▅", "▄", "▃" ],
|
|
596
|
+
"growHorizontal": [ "▏", "▎", "▍", "▌", "▋", "▊", "▉", "▊", "▋", "▌", "▍", "▎" ],
|
|
597
|
+
"balloon": [ " ", ".", "o", "O", "@", "*", " " ],
|
|
598
|
+
"balloon2": [ ".", "o", "O", "°", "O", "o", "." ],
|
|
599
|
+
"noise": [ "▓", "▒", "░" ],
|
|
600
|
+
"bounce": [ "⠁", "⠂", "⠄", "⠂" ]
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const SPINNER_CHARS = spinners['dots']
|
|
604
|
+
let spinFrame = 0
|
|
605
|
+
let spinTimer = null
|
|
606
|
+
let phaseMsg = ''
|
|
607
|
+
const progressLabel = `[${componentName}]`
|
|
608
|
+
|
|
609
|
+
function fmtLine(ch) {
|
|
610
|
+
return ` ${ch} ${fmtElapsed()} ${phaseMsg}`
|
|
611
|
+
// return ` ${ch} ${fmtElapsed()} ${progressLabel} ${phaseMsg}`
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function tickSpinner() {
|
|
615
|
+
if (!isTTY) return
|
|
616
|
+
updateProgressLine(fmtLine(SPINNER_CHARS[spinFrame++ % SPINNER_CHARS.length]))
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function setPhase(msg) {
|
|
620
|
+
phaseMsg = msg
|
|
621
|
+
if (!verbose) tickSpinner()
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function endProgress() {
|
|
625
|
+
if (spinTimer) {
|
|
626
|
+
clearInterval(spinTimer)
|
|
627
|
+
spinTimer = null
|
|
628
|
+
}
|
|
629
|
+
if (!verbose && isTTY) {
|
|
630
|
+
updateProgressLine(` ✓ ${fmtElapsed()} Done.`)
|
|
631
|
+
// updateProgressLine(` ✓ ${fmtElapsed()} ${progressLabel} Done.`)
|
|
632
|
+
} else if (!verbose) {
|
|
633
|
+
process.stdout.write(` ✓ ${fmtElapsed()} Done.\n`)
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
process.on('exit', releaseSlot)
|
|
638
|
+
|
|
639
|
+
const asciidoctorArgs = [
|
|
640
|
+
'-b', 'docbook',
|
|
641
|
+
'-a', 'experimental',
|
|
642
|
+
'-o', xmlFile,
|
|
643
|
+
...requireArgs,
|
|
644
|
+
...safeArgs,
|
|
645
|
+
...(sectionNumbers ? ['-n'] : []),
|
|
646
|
+
...(doctype ? ['-d', doctype] : []),
|
|
647
|
+
...attributes.flatMap(a => ['-a', a]),
|
|
648
|
+
inputFile,
|
|
649
|
+
]
|
|
650
|
+
|
|
651
|
+
const converterArgs = [
|
|
652
|
+
'docbook2typst',
|
|
653
|
+
'-o', typFile,
|
|
654
|
+
...(typstTemplate ? [] : ['--no-theme']),
|
|
655
|
+
xmlFile,
|
|
656
|
+
]
|
|
657
|
+
|
|
658
|
+
const typstBaseArgs = [
|
|
659
|
+
'typst',
|
|
660
|
+
'compile',
|
|
661
|
+
'--root', typstRoot,
|
|
662
|
+
'--pdf-standard', '2.0',
|
|
663
|
+
...(fontRoot ? ['--font-path', fontRoot, '--ignore-system-fonts'] : []),
|
|
664
|
+
]
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
// ── 3–6. Pipeline ─────────────────────────────────────────────────────────────
|
|
668
|
+
|
|
669
|
+
(async () => {
|
|
670
|
+
claimSlot()
|
|
671
|
+
if (verbose) {
|
|
672
|
+
console.log(`Generating PDF: ${relPath(pdfFile)}`)
|
|
673
|
+
} else if (isTTY) {
|
|
674
|
+
initProgressLine(`Generating PDF: ${path.basename(pdfFile)}`, fmtLine(SPINNER_CHARS[0]))
|
|
675
|
+
spinTimer = setInterval(tickSpinner, 80)
|
|
676
|
+
} else {
|
|
677
|
+
process.stdout.write(`Generating PDF: ${relPath(pdfFile)}\n`)
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
try {
|
|
681
|
+
await run('Running asciidoctor...', 'asciidoctor', asciidoctorArgs)
|
|
682
|
+
|
|
683
|
+
// Postprocess DocBook XML: the Assembler titles each assembled file as
|
|
684
|
+
// "Component Name: Page Title", which Asciidoctor splits into <title> and
|
|
685
|
+
// <subtitle> on the <book><info> block. The first content section becomes
|
|
686
|
+
// a <preface> whose <title> is left empty. Copy the subtitle into it so
|
|
687
|
+
// the first entry in the PDF table of contents has a visible title.
|
|
688
|
+
// Also capture the title and subtitle for passing to Typst via --input.
|
|
689
|
+
// The XML <info><title> holds the clean component title; the assembled
|
|
690
|
+
// AsciiDoc heading combines both as "Title: Subtitle", so reading from
|
|
691
|
+
// the XML avoids duplicating the subtitle on the cover page.
|
|
692
|
+
let xmlTitle = ''
|
|
693
|
+
let subtitle = ''
|
|
694
|
+
{
|
|
695
|
+
let xml = fs.readFileSync(xmlFile, 'utf-8')
|
|
696
|
+
const info = xml.match(/<info>([\s\S]*?)<\/info>/)
|
|
697
|
+
if (info) {
|
|
698
|
+
const mTitle = info[1].match(/<title>([^<]*)<\/title>/)
|
|
699
|
+
if (mTitle) xmlTitle = mTitle[1].trim()
|
|
700
|
+
const mSub = info[1].match(/<subtitle>([^<]*)<\/subtitle>/)
|
|
701
|
+
if (mSub) subtitle = mSub[1].trim()
|
|
702
|
+
}
|
|
703
|
+
if (subtitle) {
|
|
704
|
+
xml = xml.replace(
|
|
705
|
+
/(<preface(?:\s[^>]*)?>)([ \t\n]*)<title>([ \t\n]*)<\/title>/,
|
|
706
|
+
(_, tag, ws) => `${tag}${ws}<title>${subtitle}</title>`
|
|
707
|
+
)
|
|
708
|
+
fs.writeFileSync(xmlFile, xml, 'utf-8')
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
await run('Converting to Docbook...', 'npx', converterArgs)
|
|
713
|
+
|
|
714
|
+
// Prepend Typst preamble import; title/subtitle are passed via --input.
|
|
715
|
+
// When no template is specified the generated .typ is compiled as-is.
|
|
716
|
+
const typstInputArgs = []
|
|
717
|
+
if (typstTemplate) {
|
|
718
|
+
const preamble = fs.readFileSync(path.resolve(typstTemplate), 'utf-8').trimEnd()
|
|
719
|
+
+ '\n#show: book-setup\n'
|
|
720
|
+
fs.writeFileSync(typFile, preamble + fs.readFileSync(typFile, 'utf-8'), 'utf-8')
|
|
721
|
+
if (verbose) console.log(`\n→ Prepended Typst preamble to ${relPath(typFile)}`)
|
|
722
|
+
|
|
723
|
+
const title = xmlTitle || extractTitle(inputFile)
|
|
724
|
+
typstInputArgs.push('--input', `title=${title}`, '--input', `subtitle=${subtitle}`)
|
|
725
|
+
} else {
|
|
726
|
+
// No theme — emit a bare document-title directive so PDF/UA-1 is satisfied.
|
|
727
|
+
const title = xmlTitle || extractTitle(inputFile)
|
|
728
|
+
const docTitle = subtitle ? `${title}: ${subtitle}` : title
|
|
729
|
+
const header = `#set document(title: [${docTitle}])\n`
|
|
730
|
+
fs.writeFileSync(typFile, header + fs.readFileSync(typFile, 'utf-8'), 'utf-8')
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// FIXME
|
|
734
|
+
await run('Running Typst...', 'npx', [
|
|
735
|
+
'typst',
|
|
736
|
+
...typstBaseArgs,
|
|
737
|
+
...typstInputArgs,
|
|
738
|
+
typFile,
|
|
739
|
+
pdfFile,
|
|
740
|
+
])
|
|
741
|
+
|
|
742
|
+
endProgress()
|
|
743
|
+
|
|
744
|
+
if (verbose) console.log(`\nDone. PDF written to: ${relPath(pdfFile)}`)
|
|
745
|
+
|
|
746
|
+
// Typst files are intentionally preserved for inspection.
|
|
747
|
+
} catch (err) {
|
|
748
|
+
if (spinTimer) {
|
|
749
|
+
clearInterval(spinTimer)
|
|
750
|
+
spinTimer = null
|
|
751
|
+
}
|
|
752
|
+
if (!verbose && isTTY) {
|
|
753
|
+
updateProgressLine(` ✗ ${fmtElapsed()} Error.`)
|
|
754
|
+
// updateProgressLine(` ✗ ${fmtElapsed()} ${progressLabel} Error.`)
|
|
755
|
+
} else if (!verbose) {
|
|
756
|
+
process.stdout.write('\n')
|
|
757
|
+
}
|
|
758
|
+
console.error(err.message)
|
|
759
|
+
process.exit(1)
|
|
760
|
+
}
|
|
761
|
+
})()
|