mikser-io 9.23.0 → 9.24.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/docs/diagnostics.md +15 -2
- package/package.json +1 -1
- package/src/engine.js +44 -5
- package/src/report.js +39 -1
package/docs/diagnostics.md
CHANGED
|
@@ -102,15 +102,28 @@ under this flag, so stdout parses whole.
|
|
|
102
102
|
npx mikser --json | jq '.summary'
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
Five buckets, and the distinction between them is the point:
|
|
106
106
|
|
|
107
107
|
| Bucket | Meaning |
|
|
108
108
|
| --- | --- |
|
|
109
|
-
| `rendered` | the render ran, with a `reason` per entity |
|
|
109
|
+
| `rendered` | the render ran and the output moved, with a `reason` per entity |
|
|
110
110
|
| `skipped` | the manifest decided not to render, with a `reason` |
|
|
111
111
|
| `unchanged` | the render ran and produced bytes identical to what was already on disk |
|
|
112
|
+
| `errors` | the render ran and **threw** — with `id`, `destination`, `error`, `layout` |
|
|
112
113
|
| `gated` | a count — the source was unchanged, so no render was ever scheduled |
|
|
113
114
|
|
|
115
|
+
A failed render appears in `errors` and **not** in `rendered`: that bucket
|
|
116
|
+
means the output moved, and a throw writes nothing. The previous good bytes
|
|
117
|
+
stay on disk, which is what makes a failed render survivable — and also
|
|
118
|
+
what makes it invisible without this bucket.
|
|
119
|
+
|
|
120
|
+
**A one-shot build with render errors exits `1`.** That is the signal a CI
|
|
121
|
+
gate needs, because `mikser && mikser --verify` would otherwise pass a
|
|
122
|
+
build in which nothing rendered: `--verify` compares the output against the
|
|
123
|
+
manifest, both of which still describe the last good render. Watch mode
|
|
124
|
+
keeps running — a failed render there is a state to fix on the next cycle,
|
|
125
|
+
not a reason to tear down the watcher.
|
|
126
|
+
|
|
114
127
|
`reason` is a stable vocabulary you can assert on: `unchanged`,
|
|
115
128
|
`never-rendered`, `inputs-changed`, `ref-changed`, `query-matched`,
|
|
116
129
|
`cache-disabled`, `postprocessor`, `force`, `no-manifest`.
|
package/package.json
CHANGED
package/src/engine.js
CHANGED
|
@@ -10,7 +10,7 @@ import { useJournal, updateEntry } from './journal.js'
|
|
|
10
10
|
import { globby } from 'globby'
|
|
11
11
|
import { OPERATION, TASKS } from './constants.js'
|
|
12
12
|
import { changeExtension, formatErrorContext, projectMeta, lookupKeys } from './utils.js'
|
|
13
|
-
import { reportRendered, reportSkipped, emitReport } from './report.js'
|
|
13
|
+
import { reportRendered, reportSkipped, reportError, renderErrorCount, emitReport } from './report.js'
|
|
14
14
|
import render from './render.js'
|
|
15
15
|
import postprocess, { loadPlugin as loadPostPlugin } from './postprocess.js'
|
|
16
16
|
import map from 'p-map'
|
|
@@ -374,7 +374,11 @@ export async function setup(options) {
|
|
|
374
374
|
logger.debug('Manifest skip: %s → %s', entity.name || entity.id, entity.destination)
|
|
375
375
|
return
|
|
376
376
|
}
|
|
377
|
-
|
|
377
|
+
// Reported on the way OUT, not here: `rendered` means the
|
|
378
|
+
// output moved, and a render that throws writes nothing. The
|
|
379
|
+
// decision is carried down to the success path so the reason
|
|
380
|
+
// and its detail still travel with it.
|
|
381
|
+
//
|
|
378
382
|
// Same detail at debug, for tailing a watch run. One line per
|
|
379
383
|
// render is too much for a build's normal output — the counts
|
|
380
384
|
// are the summary and --json is the record — but when you are
|
|
@@ -522,11 +526,22 @@ export async function setup(options) {
|
|
|
522
526
|
await updateEntry({ id, output: entry.output, deps: edges })
|
|
523
527
|
}
|
|
524
528
|
|
|
529
|
+
reportRendered(entity, decision.reason, decision)
|
|
525
530
|
logger.debug('Rendered: [%s] %s → %s', options.renderer, entity.name || entity.id, entity.destination)
|
|
526
531
|
} catch (err) {
|
|
527
532
|
if (!signal.aborted) {
|
|
528
533
|
await updateEntry({ id, output: { success: false } })
|
|
529
|
-
|
|
534
|
+
const context = formatErrorContext(entity, err, runtime.options)
|
|
535
|
+
logger.error('Render error: %s%s %s', entity.id, context, err.message)
|
|
536
|
+
// The machine-readable half of that same line. Without
|
|
537
|
+
// it a build that fails every page reports rendered:N,
|
|
538
|
+
// warnings:0 and exits 0 — three clean signals and only
|
|
539
|
+
// the human log knowing otherwise.
|
|
540
|
+
reportError(entity, err, {
|
|
541
|
+
renderer: options.renderer ?? null,
|
|
542
|
+
layout: entity.layout?.id ?? null,
|
|
543
|
+
context: context.trim() || null,
|
|
544
|
+
})
|
|
530
545
|
}
|
|
531
546
|
logger.debug('Render canceled')
|
|
532
547
|
}
|
|
@@ -537,8 +552,12 @@ export async function setup(options) {
|
|
|
537
552
|
concurrency: runtime.options.threads,
|
|
538
553
|
signal
|
|
539
554
|
})
|
|
540
|
-
|
|
555
|
+
// Jobs minus skips minus THROWS. Counting a failed render as
|
|
556
|
+
// rendered is the same overstatement the report used to make.
|
|
557
|
+
const failed = renderErrorCount()
|
|
558
|
+
renderJobs.size && logger.info('Rendered: %d', renderJobs.size - skipped - failed)
|
|
541
559
|
skipped && logger.info('Manifest skipped: %d', skipped)
|
|
560
|
+
failed && logger.error('Render errors: %d', failed)
|
|
542
561
|
})
|
|
543
562
|
|
|
544
563
|
onBeforePostprocess(async (signal) => {
|
|
@@ -765,11 +784,31 @@ export async function setup(options) {
|
|
|
765
784
|
}
|
|
766
785
|
}
|
|
767
786
|
}
|
|
768
|
-
|
|
787
|
+
// A cycle with failed renders is not a completed build, and the word
|
|
788
|
+
// people read is this one.
|
|
789
|
+
const failed = renderErrorCount()
|
|
790
|
+
if (failed) logger.error('Mikser completed with %d render error%s', failed, failed === 1 ? '' : 's')
|
|
791
|
+
else logger.notice('Mikser completed')
|
|
792
|
+
|
|
769
793
|
// After the cycle, and only under --json. stdout has been kept clear
|
|
770
794
|
// for exactly this (the logger writes to stderr under --json), so the
|
|
771
795
|
// document is the only thing on it and can be piped to jq.
|
|
772
796
|
emitReport()
|
|
797
|
+
|
|
798
|
+
// Non-zero for a one-shot build, so `mikser && mikser --verify` cannot
|
|
799
|
+
// pass with every page in the site stale. `exitCode` rather than
|
|
800
|
+
// process.exit so the report above is flushed and shutdown runs.
|
|
801
|
+
//
|
|
802
|
+
// Watch mode keeps going: a failed render there is a state to fix in
|
|
803
|
+
// the next cycle, not a reason to tear down the watcher. That is also
|
|
804
|
+
// what makes the failure self-concealing in watch — the errors scroll
|
|
805
|
+
// past between two green builds — so the exit code is precisely the
|
|
806
|
+
// signal CI needs and the one interactive use must not have.
|
|
807
|
+
//
|
|
808
|
+
// 1, not 2: --verify already uses 2 for output drift and --explain 3
|
|
809
|
+
// for not-found. "The build ran and some renders threw" is its own
|
|
810
|
+
// thing.
|
|
811
|
+
if (failed && !runtime.options.watch) process.exitCode = 1
|
|
773
812
|
})
|
|
774
813
|
|
|
775
814
|
onCancelled(async () => {
|
package/src/report.js
CHANGED
|
@@ -13,7 +13,7 @@ import runtime from './runtime.js'
|
|
|
13
13
|
|
|
14
14
|
function store() {
|
|
15
15
|
runtime.state ??= {}
|
|
16
|
-
runtime.state.report ??= { rendered: [], skipped: [], unchanged: [], warnings: [], gated: 0 }
|
|
16
|
+
runtime.state.report ??= { rendered: [], skipped: [], unchanged: [], errors: [], warnings: [], gated: 0 }
|
|
17
17
|
return runtime.state.report
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -77,15 +77,53 @@ export function reportWarning(code, fields = {}) {
|
|
|
77
77
|
store().warnings.push({ code, ...fields })
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
// A render that RAN and THREW. Recorded unconditionally — not gated on
|
|
81
|
+
// --json like the other buckets — because the exit code depends on the
|
|
82
|
+
// count, and a build that fails 12 renders must not exit 0 just because
|
|
83
|
+
// nobody asked for a report.
|
|
84
|
+
//
|
|
85
|
+
// Failed entities do NOT appear in `rendered`. That bucket means the output
|
|
86
|
+
// moved, and a throw writes nothing: the previous good bytes stay on disk,
|
|
87
|
+
// which is what makes the failure survivable and also what makes it
|
|
88
|
+
// invisible. `rendered: 12` beside zero written files is the misleading
|
|
89
|
+
// half, and summing buckets should not require knowing that.
|
|
90
|
+
export function reportError(entity, err, context = {}) {
|
|
91
|
+
const store = errorStore()
|
|
92
|
+
store.push({
|
|
93
|
+
id: entity?.id ?? null,
|
|
94
|
+
destination: entity?.destination ?? null,
|
|
95
|
+
error: err?.message ?? String(err),
|
|
96
|
+
...context,
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Errors are counted even without --json, so they need a store that does
|
|
101
|
+
// not depend on the report being requested.
|
|
102
|
+
function errorStore() {
|
|
103
|
+
runtime.state ??= {}
|
|
104
|
+
runtime.state.renderErrors ??= []
|
|
105
|
+
return runtime.state.renderErrors
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// How many renders threw this cycle. Read by the engine to decide the
|
|
109
|
+
// process exit code.
|
|
110
|
+
export function renderErrorCount() {
|
|
111
|
+
return errorStore().length
|
|
112
|
+
}
|
|
113
|
+
|
|
80
114
|
export function buildReport() {
|
|
81
115
|
const report = store()
|
|
82
116
|
return {
|
|
83
117
|
rendered: report.rendered,
|
|
84
118
|
skipped: report.skipped,
|
|
85
119
|
unchanged: report.unchanged,
|
|
120
|
+
// Renders that ran and threw. A build with a non-empty `errors` is a
|
|
121
|
+
// failed build, whatever the other counts say.
|
|
122
|
+
errors: errorStore(),
|
|
86
123
|
warnings: report.warnings,
|
|
87
124
|
summary: {
|
|
88
125
|
rendered: report.rendered.length,
|
|
126
|
+
errors: errorStore().length,
|
|
89
127
|
// Of those renders, how many produced bytes identical to
|
|
90
128
|
// what was already on disk — see reportUnchanged.
|
|
91
129
|
unchanged: report.unchanged.length,
|