mikser-io 9.81.0 → 9.83.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/app.js +10 -1
- package/docs/diagnostics.md +40 -5
- package/package.json +1 -1
- package/src/engine.js +51 -11
- package/src/instance.js +54 -10
- package/src/plugins/assets.js +95 -9
package/app.js
CHANGED
|
@@ -43,7 +43,16 @@ function locate(argv) {
|
|
|
43
43
|
: tool ? { type: 'report', tool, toolArgs: value('--tool-args'), json: has('--json') }
|
|
44
44
|
: explain ? { type: 'report', explain, json: has('--json') }
|
|
45
45
|
: has('--audit-output') ? { type: 'report', auditOutput: true, json: has('--json') }
|
|
46
|
-
: { type: 'build',
|
|
46
|
+
: { type: 'build',
|
|
47
|
+
clear: has('--clear'),
|
|
48
|
+
// Not a flag that happens to be set — the client's OUTPUT
|
|
49
|
+
// CONTRACT. `--json` promises the document on stdout and every
|
|
50
|
+
// log line on stderr, and BOTH halves are decided by the process
|
|
51
|
+
// that does the writing. Forwarded, that is the instance, which
|
|
52
|
+
// was started without the flag. So the contract has to travel
|
|
53
|
+
// with the request or it is not honoured at all.
|
|
54
|
+
json: has('--json'),
|
|
55
|
+
renderPresets: has('--render-presets') ? (value('--render-presets') ?? true) : undefined }
|
|
47
56
|
|
|
48
57
|
return {
|
|
49
58
|
longRunning,
|
package/docs/diagnostics.md
CHANGED
|
@@ -796,6 +796,19 @@ queue. A client that writes a file and immediately asks can beat the file
|
|
|
796
796
|
event, and draining would then build without the change that prompted the
|
|
797
797
|
request.
|
|
798
798
|
|
|
799
|
+
`--json`, `--tool` and `--tools` keep their stream contract across the socket:
|
|
800
|
+
the document on stdout, the log on stderr, so `mikser --json | jq` means the
|
|
801
|
+
same thing whether or not something is listening. Both halves of that contract
|
|
802
|
+
are decided by the process that writes, which when forwarding is the instance —
|
|
803
|
+
so the flag travels with the request, and each captured chunk is replayed on
|
|
804
|
+
the stream the instance actually used. The contract applies to that one request
|
|
805
|
+
and is restored afterwards: asking for a document does not put the instance
|
|
806
|
+
into json mode for its own output or for the next caller.
|
|
807
|
+
|
|
808
|
+
`--audit-output` is the exception, and it is the same with or without an
|
|
809
|
+
instance: it reports through the log and writes no document, so `--json` there
|
|
810
|
+
only moves its output to stderr.
|
|
811
|
+
|
|
799
812
|
## Faults
|
|
800
813
|
|
|
801
814
|
A **fault** is a subsystem saying it cannot do its job — as opposed to an
|
|
@@ -915,16 +928,38 @@ surfaces that turn silence into a statement:
|
|
|
915
928
|
- **The emitted output**, read back and resolved the way a browser
|
|
916
929
|
would — `src`, `href`, `poster`, `srcset` and CSS `url()` across
|
|
917
930
|
html and css. This one sees paths written by hand, not just helper
|
|
918
|
-
output, and it separates
|
|
919
|
-
`reference-
|
|
920
|
-
|
|
921
|
-
the
|
|
922
|
-
|
|
931
|
+
output, and it separates three problems that share a symptom:
|
|
932
|
+
`reference-no-derivative` when the assets plugin can say why the file
|
|
933
|
+
is not there, `reference-wrong-base` when the file exists elsewhere in
|
|
934
|
+
the output (the url was built from the wrong root, and the report names
|
|
935
|
+
where the file actually is), `reference-broken` when nothing produced
|
|
936
|
+
it at all.
|
|
923
937
|
- **The render track**, which records every `asset()` / `resource()`
|
|
924
938
|
call and tests the destination it built. This catches a url that
|
|
925
939
|
never reaches an html file at all — one emitted into a feed or a
|
|
926
940
|
sitemap — and warns under `asset-missing`. Where both can see the
|
|
927
941
|
same file, the output scan reports it and this one stays quiet.
|
|
942
|
+
|
|
943
|
+
A url under the assets folder gets a cause rather than a guess. Whether a
|
|
944
|
+
preset covers a file is decided by `match` against the entity id, which is
|
|
945
|
+
not visible from a url, so the assets plugin is asked and the answer is one
|
|
946
|
+
of four: the preset name is not configured; the preset does not cover this
|
|
947
|
+
file (with the `match` that decided it, and any preset that *does* cover it,
|
|
948
|
+
since the fix is then in the template); the preset covers it and the
|
|
949
|
+
derivative still is not there, so a render failed; or there is no source
|
|
950
|
+
file under that name at all. It travels into `--json` as `reason`.
|
|
951
|
+
|
|
952
|
+
This matters most where it looks least like itself. `files()` copies the
|
|
953
|
+
source into the output, so a derivative that was never produced leaves a
|
|
954
|
+
file of the same name sitting elsewhere — and the wrong-base search finds
|
|
955
|
+
it and reports a misplaced file with complete confidence. The base is
|
|
956
|
+
right; the derivative does not exist. A real answer wins over the
|
|
957
|
+
heuristic wherever there is one.
|
|
958
|
+
|
|
959
|
+
`asset()` looks nothing up. It takes a path rather than an entity and is
|
|
960
|
+
the hottest call in a render, so it stays a string operation; the question
|
|
961
|
+
is answered once per missing destination after the cycle, and only when
|
|
962
|
+
something is already wrong.
|
|
928
963
|
- **A link that works only by accident** — a url with one `..` too many
|
|
929
964
|
still loads, because a browser discards a climb above the origin root
|
|
930
965
|
rather than failing. Reported under `reference-over-deep`, separately
|
package/package.json
CHANGED
package/src/engine.js
CHANGED
|
@@ -10,7 +10,7 @@ import { instanceControl } from './instance.js'
|
|
|
10
10
|
import { useJournal, updateEntry } from './journal.js'
|
|
11
11
|
import { globby } from 'globby'
|
|
12
12
|
import { OPERATION, TASKS } from './constants.js'
|
|
13
|
-
import { changeExtension, formatErrorContext, projectMeta, lookupKeys } from './utils.js'
|
|
13
|
+
import { changeExtension, formatErrorContext, projectMeta, lookupKeys, siteRootFor } from './utils.js'
|
|
14
14
|
import { reportRendered, reportSkipped, reportError, renderErrorCount, emitReport, finishCycle, reportAssetUse, assetUse } from './report.js'
|
|
15
15
|
import { checkReferences } from './references.js'
|
|
16
16
|
import { toolSchemas, invokeTool, toolResultText, toolResultFailed } from './tools.js'
|
|
@@ -116,11 +116,41 @@ async function reportBrokenReferences(logger) {
|
|
|
116
116
|
const named = (files) =>
|
|
117
117
|
files.slice(0, 3).join(', ') + (files.length > 3 ? ` and ${files.length - 3} more` : '')
|
|
118
118
|
|
|
119
|
+
// Ask the assets plugin before guessing.
|
|
120
|
+
//
|
|
121
|
+
// A derivative that was never produced has its SOURCE sitting in the
|
|
122
|
+
// output — files() copies it there — so the same-name search finds it and
|
|
123
|
+
// "the file is at media/icons/logo.svg" reads as a base that is off by a
|
|
124
|
+
// folder. It is not: the base is right and the derivative does not exist,
|
|
125
|
+
// because the preset does not cover that file. Confidently naming the
|
|
126
|
+
// wrong cause is worse than naming none, so a real answer wins over the
|
|
127
|
+
// heuristic wherever there is one.
|
|
128
|
+
const explain = runtime.state?.assets?.explainMissing
|
|
129
|
+
const reasons = new Map()
|
|
130
|
+
if (explain) {
|
|
131
|
+
for (const { target } of broken.slice(0, SHOWN)) {
|
|
132
|
+
// Site-relative, because that is what the assets folder is named
|
|
133
|
+
// relative to. A build that deploys out/<lang> as its own domain
|
|
134
|
+
// root resolves this target to `a/derived/web/...`, and a plain
|
|
135
|
+
// prefix test on the assets folder sees `a` and gives up — the
|
|
136
|
+
// check goes quiet on exactly the multi-site builds where a
|
|
137
|
+
// derivative is shared into each root.
|
|
138
|
+
const root = siteRootFor(target, siteRoots)
|
|
139
|
+
const local = root ? target.slice(root.length).replace(/^\/+/, '') : target
|
|
140
|
+
try { reasons.set(target, await explain(local)) } catch { /* never break the report */ }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
119
144
|
for (const { url, target, files, elsewhere } of broken.slice(0, SHOWN)) {
|
|
145
|
+
const reason = reasons.get(target)
|
|
120
146
|
// Two different problems wear the same symptom. A target whose file
|
|
121
147
|
// exists elsewhere in the output is a base that is wrong, not an asset
|
|
122
148
|
// that is missing — and saying which one saves the reader the search.
|
|
123
|
-
if (
|
|
149
|
+
if (reason) {
|
|
150
|
+
logger.warn({ code: 'reference-no-derivative', url, target, files, reason },
|
|
151
|
+
'No derivative was produced: %s (from %s) — %s',
|
|
152
|
+
url, named(files), reason)
|
|
153
|
+
} else if (elsewhere?.length) {
|
|
124
154
|
logger.warn({ code: 'reference-wrong-base', url, target, files, elsewhere },
|
|
125
155
|
'Points at the wrong place: %s (from %s) — nothing at %s, but the file is at %s',
|
|
126
156
|
url, named(files), target, elsewhere.join(', '))
|
|
@@ -131,16 +161,17 @@ async function reportBrokenReferences(logger) {
|
|
|
131
161
|
}
|
|
132
162
|
}
|
|
133
163
|
if (broken.length) {
|
|
134
|
-
const
|
|
164
|
+
const explained = broken.slice(0, SHOWN).filter(b => reasons.get(b.target)).length
|
|
165
|
+
const misplaced = broken.filter(b => b.elsewhere?.length && !reasons.get(b.target)).length
|
|
135
166
|
logger.warn({
|
|
136
167
|
code: 'reference-broken-summary',
|
|
137
|
-
broken: broken.length, wrongBase: misplaced, checked,
|
|
168
|
+
broken: broken.length, wrongBase: misplaced, noDerivative: explained, checked,
|
|
138
169
|
},
|
|
139
|
-
'%d of %d reference(s) in the output resolve to nothing%s.
|
|
140
|
-
+ 'elsewhere): %d. Never produced: %d. A URL helper builds the
|
|
141
|
-
+ 'it up, so
|
|
170
|
+
'%d of %d reference(s) in the output resolve to nothing%s. No derivative produced: %d. '
|
|
171
|
+
+ 'Wrong base (the file exists elsewhere): %d. Never produced: %d. A URL helper builds the '
|
|
172
|
+
+ 'path rather than looking it up, so none of them can fail at the point it is written.',
|
|
142
173
|
broken.length, checked, broken.length > SHOWN ? `, ${SHOWN} shown` : '',
|
|
143
|
-
misplaced, broken.length - misplaced)
|
|
174
|
+
explained, misplaced, broken.length - misplaced - explained)
|
|
144
175
|
}
|
|
145
176
|
|
|
146
177
|
// Grouped by how FAR each climbed, because a site whose every over-deep url
|
|
@@ -215,10 +246,19 @@ async function reportMissingAssets(logger, alreadyReported = new Set()) {
|
|
|
215
246
|
// every page on the site, and a thousand lines of it buries whatever else
|
|
216
247
|
// the build said.
|
|
217
248
|
const SHOWN = 10
|
|
249
|
+
const explain = runtime.state?.assets?.explainMissing
|
|
218
250
|
for (const [destination, ids] of missing.slice(0, SHOWN)) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
251
|
+
// Same question, same answer, wherever the symptom surfaces. This path
|
|
252
|
+
// sees urls that never reach an html file at all — a sitemap, a feed —
|
|
253
|
+
// which the output scan cannot look at.
|
|
254
|
+
let reason = null
|
|
255
|
+
if (explain) {
|
|
256
|
+
try { reason = await explain(destination) } catch { /* never break the report */ }
|
|
257
|
+
}
|
|
258
|
+
logger.warn({ code: 'asset-missing', destination, referencedBy: ids, reason },
|
|
259
|
+
'Linked but not in the output: %s — referenced by %s%s', destination,
|
|
260
|
+
ids.slice(0, 3).join(', ') + (ids.length > 3 ? ` and ${ids.length - 3} more` : ''),
|
|
261
|
+
reason ? `. ${reason[0].toUpperCase()}${reason.slice(1)}` : '')
|
|
222
262
|
}
|
|
223
263
|
logger.warn({ code: 'asset-missing-summary', missing: missing.length, checked: used.length },
|
|
224
264
|
'%d of %d linked file(s) are not in the output%s. A URL helper builds the path rather than looking it '
|
package/src/instance.js
CHANGED
|
@@ -125,8 +125,11 @@ export function forward({ workingFolder, config, request }) {
|
|
|
125
125
|
readFrames(socket, (message) => {
|
|
126
126
|
if (message.type === 'log') {
|
|
127
127
|
// The instance's output for THIS request, on the stream it
|
|
128
|
-
// would have used locally
|
|
129
|
-
|
|
128
|
+
// would have used locally — which requires knowing which
|
|
129
|
+
// stream that was. An instance too old to say defaults to
|
|
130
|
+
// stderr, which is what every frame used to mean.
|
|
131
|
+
const out = message.stream === 'stdout' ? process.stdout : process.stderr
|
|
132
|
+
out.write(message.chunk)
|
|
130
133
|
} else if (message.type === 'refused') {
|
|
131
134
|
answered = true
|
|
132
135
|
process.stderr.write(`mikser: ${message.reason}\n`)
|
|
@@ -197,14 +200,22 @@ let server = null
|
|
|
197
200
|
// adding a log transport means the client sees precisely what it would have
|
|
198
201
|
// seen locally, formatting and all, with no second rendering of the same
|
|
199
202
|
// records to keep in step.
|
|
203
|
+
// WHICH stream, not merely that something was written.
|
|
204
|
+
//
|
|
205
|
+
// stdout and stderr are not two ways of saying the same thing: under --json,
|
|
206
|
+
// --tool and --tools, stdout carries a machine-readable document and stderr
|
|
207
|
+
// carries the log, and the split is the entire value of those flags. Capturing
|
|
208
|
+
// both into one undifferentiated stream throws that away, and the client can
|
|
209
|
+
// only guess — it guessed stderr, so every forwarded document landed where no
|
|
210
|
+
// consumer looks while the command exited 0.
|
|
200
211
|
function captureOutput(onChunk) {
|
|
201
212
|
const originals = [process.stdout.write, process.stderr.write]
|
|
202
|
-
const patch = (stream, original) => function (chunk, encoding, callback) {
|
|
203
|
-
try { onChunk(typeof chunk === 'string' ? chunk : chunk.toString()) } catch { /* client gone */ }
|
|
213
|
+
const patch = (stream, original, name) => function (chunk, encoding, callback) {
|
|
214
|
+
try { onChunk(typeof chunk === 'string' ? chunk : chunk.toString(), name) } catch { /* client gone */ }
|
|
204
215
|
return original.call(stream, chunk, encoding, callback)
|
|
205
216
|
}
|
|
206
|
-
process.stdout.write = patch(process.stdout, originals[0])
|
|
207
|
-
process.stderr.write = patch(process.stderr, originals[1])
|
|
217
|
+
process.stdout.write = patch(process.stdout, originals[0], 'stdout')
|
|
218
|
+
process.stderr.write = patch(process.stderr, originals[1], 'stderr')
|
|
208
219
|
return () => {
|
|
209
220
|
process.stdout.write = originals[0]
|
|
210
221
|
process.stderr.write = originals[1]
|
|
@@ -264,10 +275,10 @@ async function configStale() {
|
|
|
264
275
|
// the only process that can answer correctly. Same guards as a build: wrong
|
|
265
276
|
// config refuses, drifted config refuses.
|
|
266
277
|
async function serveReport(socket, request, logger) {
|
|
267
|
-
const restore = captureOutput((chunk) => frame(socket, { type: 'log', chunk }))
|
|
278
|
+
const restore = captureOutput((chunk, stream) => frame(socket, { type: 'log', chunk, stream }))
|
|
268
279
|
let code = 0
|
|
269
280
|
try {
|
|
270
|
-
code = await runReportOnly(request) ?? 0
|
|
281
|
+
code = await withRequestOutput(request, () => runReportOnly(request)) ?? 0
|
|
271
282
|
} catch (err) {
|
|
272
283
|
logger?.error('instance: forwarded report failed — %s', err.message)
|
|
273
284
|
code = 3
|
|
@@ -328,8 +339,37 @@ function refuseStale(socket, movedFile) {
|
|
|
328
339
|
})
|
|
329
340
|
}
|
|
330
341
|
|
|
342
|
+
// The client's output contract, applied to the instance for one request.
|
|
343
|
+
//
|
|
344
|
+
// `--json` is answered by two pieces of code that both read runtime.options:
|
|
345
|
+
// the logger picks its stream per line, and emitReport() writes nothing at all
|
|
346
|
+
// unless the option is set. The instance was started without the flag, so a
|
|
347
|
+
// forwarded `--json` was answered under the INSTANCE's contract — a build
|
|
348
|
+
// emitted no document whatsoever, and a report emitted one into the same
|
|
349
|
+
// stream as the log it was supposed to be separated from.
|
|
350
|
+
//
|
|
351
|
+
// Tighten only, never loosen. A client asking for a document states something
|
|
352
|
+
// the instance cannot know; a client not asking states nothing, and silencing
|
|
353
|
+
// an instance that was itself started under `--json` would take a document
|
|
354
|
+
// away from whatever is reading ITS stdout.
|
|
355
|
+
//
|
|
356
|
+
// Per request and restored after, like renderPresets: one client's flag does
|
|
357
|
+
// not put the instance into json mode for everyone.
|
|
358
|
+
async function withRequestOutput(request, run) {
|
|
359
|
+
const prior = {}
|
|
360
|
+
for (const key of ['json', 'tool', 'tools']) {
|
|
361
|
+
prior[key] = runtime.options[key]
|
|
362
|
+
if (request[key]) runtime.options[key] = request[key]
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
return await run()
|
|
366
|
+
} finally {
|
|
367
|
+
Object.assign(runtime.options, prior)
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
331
371
|
async function serveBuild(socket, request, logger) {
|
|
332
|
-
const restore = captureOutput((chunk) => frame(socket, { type: 'log', chunk }))
|
|
372
|
+
const restore = captureOutput((chunk, stream) => frame(socket, { type: 'log', chunk, stream }))
|
|
333
373
|
let code = 0
|
|
334
374
|
try {
|
|
335
375
|
// Fire the pending debounce rather than waiting it out.
|
|
@@ -356,7 +396,11 @@ async function serveBuild(socket, request, logger) {
|
|
|
356
396
|
const priorRenderPresets = runtime.options.renderPresets
|
|
357
397
|
if (request.renderPresets !== undefined) runtime.options.renderPresets = request.renderPresets
|
|
358
398
|
try {
|
|
359
|
-
|
|
399
|
+
// The report is emitted by the cycle itself, from inside
|
|
400
|
+
// rebuild() — the same call a one-shot makes. Nothing here
|
|
401
|
+
// re-implements it: the contract is what decides whether it
|
|
402
|
+
// writes, so setting the contract is the whole fix.
|
|
403
|
+
await withRequestOutput(request, () => runtime.rebuild())
|
|
360
404
|
} finally {
|
|
361
405
|
runtime.options.renderPresets = priorRenderPresets
|
|
362
406
|
}
|
package/src/plugins/assets.js
CHANGED
|
@@ -97,19 +97,100 @@ export function assets(options = {}) {
|
|
|
97
97
|
// reported once per process at onFinalize.
|
|
98
98
|
const matchTally = { evaluated: 0, matched: new Set(), reported: false }
|
|
99
99
|
|
|
100
|
+
// Which presets select this entity. No counters, no side effects.
|
|
101
|
+
//
|
|
102
|
+
// Split out because the same question gets asked twice for different
|
|
103
|
+
// reasons: once as files enter the catalog, where the answer drives the
|
|
104
|
+
// render and feeds the unmatched-preset tally, and once after the cycle to
|
|
105
|
+
// explain a derivative that is not there. The second must not move the
|
|
106
|
+
// counters the first reports on.
|
|
107
|
+
//
|
|
108
|
+
// `some`, not a push per matching pattern. Two patterns that both cover a
|
|
109
|
+
// file used to name their preset twice in the list; the second render was
|
|
110
|
+
// gated by the checksum, so nothing rendered twice — but "which presets
|
|
111
|
+
// cover this file" is a question with one answer, and the explainer below
|
|
112
|
+
// puts that answer in front of a reader.
|
|
113
|
+
function presetsSelecting(entity) {
|
|
114
|
+
const selected = []
|
|
115
|
+
for (const preset in (options.presets || {})) {
|
|
116
|
+
const { matches } = normalizePresetConfig(options.presets[preset])
|
|
117
|
+
if (matches.some(match => matchEntity(entity, match))) selected.push(preset)
|
|
118
|
+
}
|
|
119
|
+
return selected
|
|
120
|
+
}
|
|
121
|
+
|
|
100
122
|
async function getEntityPresets(entity) {
|
|
101
|
-
const entityPresets = []
|
|
102
123
|
matchTally.evaluated++
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
124
|
+
const selected = presetsSelecting(entity)
|
|
125
|
+
for (const preset of selected) matchTally.matched.add(preset)
|
|
126
|
+
return selected
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Why a linked derivative is not in the output.
|
|
130
|
+
//
|
|
131
|
+
// The engine detects the CONSEQUENCE — it knows what the output points at
|
|
132
|
+
// and what exists on disk — but it cannot name the cause, because whether
|
|
133
|
+
// a preset covers a file is decided by `match` against the entity id and
|
|
134
|
+
// none of that is visible from a url. So one sentence covered a mistyped
|
|
135
|
+
// preset name, a preset that did not run, and a file no preset was ever
|
|
136
|
+
// asked to cover. The last is the common one, the only one whose fix is in
|
|
137
|
+
// the config rather than in the template, and the one the reader is least
|
|
138
|
+
// likely to guess.
|
|
139
|
+
//
|
|
140
|
+
// Answered here rather than in the helper: `asset()` takes a path, not an
|
|
141
|
+
// entity, and it is the hottest call in a render. Nothing is looked up
|
|
142
|
+
// when the url is built. This runs at most a handful of times, after
|
|
143
|
+
// everything has settled, and only when something is already wrong.
|
|
144
|
+
let sourceIndex = null
|
|
145
|
+
let sourceIndexCycle
|
|
146
|
+
async function explainMissing(destination) {
|
|
147
|
+
const assetsName = runtime.options.assets
|
|
148
|
+
const parts = String(destination).replace(/^\/+/, '').split('/')
|
|
149
|
+
if (!assetsName || parts[0] !== assetsName || parts.length < 3) return null
|
|
150
|
+
const preset = parts[1]
|
|
151
|
+
const name = parts.slice(2).join('/')
|
|
152
|
+
|
|
153
|
+
const configured = Object.keys(options.presets || {}).sort()
|
|
154
|
+
if (!options.presets?.[preset]) {
|
|
155
|
+
return `no preset named '${preset}' is configured (configured: ${configured.join(', ') || 'none'})`
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// The derivative wears the PRESET's extension, so the source is found
|
|
159
|
+
// by stem — `media/hero.webp` came from `media/hero.jpg`.
|
|
160
|
+
const stem = (value) => value.slice(0, value.length - path.extname(value).length)
|
|
161
|
+
|
|
162
|
+
// Built once per cycle and only on this path: a build with nothing
|
|
163
|
+
// broken never walks the catalog for it.
|
|
164
|
+
const cycle = runtime.state?.cycle?.id ?? null
|
|
165
|
+
if (sourceIndexCycle !== cycle) { sourceIndex = null; sourceIndexCycle = cycle }
|
|
166
|
+
if (!sourceIndex) {
|
|
167
|
+
sourceIndex = new Map()
|
|
168
|
+
for await (const candidate of iterateEntities({ collection: { $ne: collection } })) {
|
|
169
|
+
if (typeof candidate.name !== 'string') continue
|
|
170
|
+
const key = stem(candidate.name)
|
|
171
|
+
if (!sourceIndex.has(key)) sourceIndex.set(key, [])
|
|
172
|
+
sourceIndex.get(key).push(candidate)
|
|
110
173
|
}
|
|
111
174
|
}
|
|
112
|
-
|
|
175
|
+
|
|
176
|
+
const source = (sourceIndex.get(stem(name)) ?? [])[0]
|
|
177
|
+
if (!source) {
|
|
178
|
+
return `no source file is named '${stem(name)}' — nothing would produce this derivative `
|
|
179
|
+
+ 'under any preset'
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const owns = presetsSelecting(source)
|
|
183
|
+
if (owns.includes(preset)) {
|
|
184
|
+
return `preset '${preset}' does cover ${source.id}, so the derivative should be there — it was `
|
|
185
|
+
+ 'not produced this cycle (the preset render failed, or the preset changed and only '
|
|
186
|
+
+ '--render-presets re-derives what is already in the catalog)'
|
|
187
|
+
}
|
|
188
|
+
const { matches } = normalizePresetConfig(options.presets[preset])
|
|
189
|
+
return `preset '${preset}' does not cover ${source.id} — its match is `
|
|
190
|
+
+ `${matches.join(', ') || '(none)'}`
|
|
191
|
+
+ (owns.length
|
|
192
|
+
? `. Presets that do cover it: ${owns.join(', ')}`
|
|
193
|
+
: '. No configured preset covers it')
|
|
113
194
|
}
|
|
114
195
|
|
|
115
196
|
// Report presets that matched none of the entities this run evaluated.
|
|
@@ -315,6 +396,11 @@ export function assets(options = {}) {
|
|
|
315
396
|
runtime.state.assets = {
|
|
316
397
|
presets: {},
|
|
317
398
|
assetsMap: {},
|
|
399
|
+
// The engine asks this when a linked derivative is not on disk.
|
|
400
|
+
// Published on state rather than imported, so the engine keeps
|
|
401
|
+
// knowing nothing about presets and says nothing when this plugin
|
|
402
|
+
// is not loaded.
|
|
403
|
+
explainMissing,
|
|
318
404
|
assetsFolder: options.outputFolder
|
|
319
405
|
? path.join(options.outputFolder, assetsName)
|
|
320
406
|
: assetsName,
|