mikser-io 9.80.0 → 9.81.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 CHANGED
@@ -47,6 +47,12 @@ function locate(argv) {
47
47
 
48
48
  return {
49
49
  longRunning,
50
+ // The flags this process was invoked with, forwarded so the INSTANCE
51
+ // can reject an unknown one. This pre-parser reads the few options it
52
+ // needs and passes the rest along, so commander never runs on a
53
+ // forwarded invocation — and a typo was accepted in silence, built,
54
+ // and reported success.
55
+ argv,
50
56
  workingFolder: value('--working-folder', '-i') ?? '.',
51
57
  config: value('--config', '-c') ?? 'mikser.config.js',
52
58
  request,
@@ -74,7 +80,7 @@ async function main() {
74
80
  const code = await forward({
75
81
  workingFolder,
76
82
  config: path.resolve(where.workingFolder, where.config),
77
- request: where.request,
83
+ request: { ...where.request, argv: where.argv },
78
84
  })
79
85
  // null means nobody was listening — carry on exactly as before.
80
86
  if (code !== null) process.exit(code)
@@ -914,9 +914,12 @@ surfaces that turn silence into a statement:
914
914
  evidence:
915
915
  - **The emitted output**, read back and resolved the way a browser
916
916
  would — `src`, `href`, `poster`, `srcset` and CSS `url()` across
917
- html and css. Anything resolving to no file warns under
918
- `reference-broken`, naming the url and the pages carrying it. This
919
- one sees paths written by hand, not just helper output.
917
+ html and css. This one sees paths written by hand, not just helper
918
+ output, and it separates two problems that share a symptom:
919
+ `reference-wrong-base` when the file exists elsewhere in the output
920
+ (the url was built from the wrong root, and the report names where
921
+ the file actually is), `reference-broken` when nothing produced it
922
+ at all.
920
923
  - **The render track**, which records every `asset()` / `resource()`
921
924
  call and tests the destination it built. This catches a url that
922
925
  never reaches an html file at all — one emitted into a feed or a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mikser-io",
3
- "version": "9.80.0",
3
+ "version": "9.81.0",
4
4
  "files": [
5
5
  "app.js",
6
6
  "index.js",
package/src/engine.js CHANGED
@@ -116,15 +116,31 @@ 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
- for (const { url, target, files } of broken.slice(0, SHOWN)) {
120
- logger.warn({ code: 'reference-broken', url, target, files },
121
- 'Resolves to nothing: %s (from %s) %s', url, named(files), target)
119
+ for (const { url, target, files, elsewhere } of broken.slice(0, SHOWN)) {
120
+ // Two different problems wear the same symptom. A target whose file
121
+ // exists elsewhere in the output is a base that is wrong, not an asset
122
+ // that is missing — and saying which one saves the reader the search.
123
+ if (elsewhere?.length) {
124
+ logger.warn({ code: 'reference-wrong-base', url, target, files, elsewhere },
125
+ 'Points at the wrong place: %s (from %s) — nothing at %s, but the file is at %s',
126
+ url, named(files), target, elsewhere.join(', '))
127
+ } else {
128
+ logger.warn({ code: 'reference-broken', url, target, files },
129
+ 'Resolves to nothing, and nothing produced it: %s (from %s) — %s',
130
+ url, named(files), target)
131
+ }
122
132
  }
123
133
  if (broken.length) {
124
- logger.warn({ code: 'reference-broken-summary', broken: broken.length, checked },
125
- '%d of %d reference(s) in the output resolve to nothing%s. A URL helper builds the '
126
- + 'path rather than looking it up, so these are links to files nothing produced.',
127
- broken.length, checked, broken.length > SHOWN ? `, ${SHOWN} shown` : '')
134
+ const misplaced = broken.filter(b => b.elsewhere?.length).length
135
+ logger.warn({
136
+ code: 'reference-broken-summary',
137
+ broken: broken.length, wrongBase: misplaced, checked,
138
+ },
139
+ '%d of %d reference(s) in the output resolve to nothing%s. Wrong base (the file exists '
140
+ + 'elsewhere): %d. Never produced: %d. A URL helper builds the path rather than looking '
141
+ + 'it up, so neither kind can fail at the point it is written.',
142
+ broken.length, checked, broken.length > SHOWN ? `, ${SHOWN} shown` : '',
143
+ misplaced, broken.length - misplaced)
128
144
  }
129
145
 
130
146
  // Grouped by how FAR each climbed, because a site whose every over-deep url
package/src/instance.js CHANGED
@@ -286,6 +286,39 @@ function refuseConfig(socket, request, wrongConfig) {
286
286
  })
287
287
  }
288
288
 
289
+ // An option the instance's own commander does not recognise.
290
+ //
291
+ // A forwarded invocation never reaches commander: app.js pre-parses the few
292
+ // flags it needs and forwards the rest, so `mikser --bogus` against a running
293
+ // watcher built normally and exited 0, while the same command with nothing
294
+ // listening was rejected outright. Same words, two answers, and the quiet one
295
+ // is the one that looks like it worked.
296
+ //
297
+ // parseOptions REPORTS unknowns without applying anything, which is what makes
298
+ // this safe to run against the live instance's option table.
299
+ function refuseUnknownFlags(socket, request) {
300
+ const argv = Array.isArray(request.argv) ? request.argv : null
301
+ if (!argv?.length) return false
302
+ const commander = runtime.engine?.commander
303
+ if (!commander) return false
304
+ let unknown = []
305
+ try {
306
+ unknown = commander.parseOptions([...argv]).unknown ?? []
307
+ } catch {
308
+ return false // never let a probe refuse a legitimate build
309
+ }
310
+ unknown = unknown.filter(a => a.startsWith('-'))
311
+ if (!unknown.length) return false
312
+ frame(socket, {
313
+ type: 'refused',
314
+ reason: `unknown option ${unknown.map(u => `'${u}'`).join(', ')}.`,
315
+ detail: 'Forwarded to the instance running in this folder, which does not recognise it. '
316
+ + 'The same command with nothing listening would have been rejected too — this says so '
317
+ + 'rather than building as though the flag had been understood.',
318
+ })
319
+ return true
320
+ }
321
+
289
322
  function refuseStale(socket, movedFile) {
290
323
  frame(socket, {
291
324
  type: 'refused',
@@ -373,6 +406,7 @@ export function serveInstance() {
373
406
  // instance's — a report against the wrong config is the
374
407
  // original incident, and it is wrong whether or not it
375
408
  // writes anything.
409
+ if (refuseUnknownFlags(socket, request)) return
376
410
  const wrongConfig = configMismatch(request.config)
377
411
  if (wrongConfig) return refuseConfig(socket, request, wrongConfig)
378
412
  const movedFile = await configStale()
package/src/references.js CHANGED
@@ -173,5 +173,31 @@ export async function checkReferences(outputFolder, { siteRoots = [] } = {}) {
173
173
  }
174
174
  }
175
175
 
176
+ // "It was never written" and "it is written somewhere else" are different
177
+ // problems with one symptom, and they were reported with one sentence.
178
+ //
179
+ // A missing target whose FILE exists elsewhere in the output is almost
180
+ // always a base problem: the url was built from the wrong root, or with a
181
+ // segment too many. Naming where the file actually is turns "resolves to
182
+ // nothing" into the answer. A target that exists nowhere really was never
183
+ // produced — a preset that did not run, an extension nothing emits.
184
+ //
185
+ // Indexed only when something is broken, so a clean build pays nothing.
186
+ if (broken.size) {
187
+ const byName = new Map()
188
+ for (const file of await globby('**/*', {
189
+ cwd: outputFolder, followSymbolicLinks: true, onlyFiles: true, suppressErrors: true,
190
+ })) {
191
+ const name = path.basename(file)
192
+ if (!byName.has(name)) byName.set(name, [])
193
+ byName.get(name).push(file)
194
+ }
195
+ for (const entry of broken.values()) {
196
+ const elsewhere = (byName.get(path.basename(entry.target)) ?? [])
197
+ .filter(f => f !== entry.target)
198
+ if (elsewhere.length) entry.elsewhere = elsewhere.slice(0, 3)
199
+ }
200
+ }
201
+
176
202
  return { broken: [...broken.values()], overDeep: [...overDeepRefs.values()], checked }
177
203
  }