mikser-io-layouts 2.13.0 → 2.15.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/index.js CHANGED
@@ -311,8 +311,43 @@ export function layouts(userOptions = {}) {
311
311
  // and they stay out of the layouts NAME map so nothing resolves a
312
312
  // template to one. Rendering is not a risk either: layout matching
313
313
  // skips this collection outright.
314
- const sidecarPaths = (await globby('**/*.js', { cwd: runtime.options.layoutsFolder }))
315
- .filter(isSidecarScript)
314
+ const allScripts = await globby('**/*.js', { cwd: runtime.options.layoutsFolder })
315
+ const sidecarPaths = allScripts.filter(isSidecarScript)
316
+
317
+ // A `.js` that nothing will ever read.
318
+ //
319
+ // A sidecar is named after the LAYOUT, and a layout's name drops
320
+ // BOTH extensions: `robots.txt.liquid` is the layout `robots`, so
321
+ // its sidecar is `robots.js`. Write `robots.txt.js` and it is
322
+ // claimed by nothing — isSidecarScript rejects it because
323
+ // stripping `.js` leaves `.txt`, and the layout glob ignores every
324
+ // `.js` outright. The file is simply invisible: it does not fail,
325
+ // it never runs, and the layout renders with empty `data`.
326
+ //
327
+ // Downstream that shipped `Sitemap: /sitemap.xml` as a relative
328
+ // reference in a robots.txt — which in a robots.txt means nothing
329
+ // — on a green build.
330
+ //
331
+ // Only when a layout sits beside it under the same stem, which is
332
+ // what makes this a misnaming rather than a stray file. A dotted
333
+ // module a sidecar imports (`lib/data.json.js`) is claimed by
334
+ // nothing either, and is nobody's mistake; flagging it would be
335
+ // the noise that gets this line filtered out.
336
+ for (const script of allScripts) {
337
+ if (isSidecarScript(script)) continue
338
+ const stem = script.replace(/\.js$/, '')
339
+ const layout = paths.find(candidate =>
340
+ candidate === stem || candidate.startsWith(`${stem}.`))
341
+ if (!layout) continue
342
+ const name = path.basename(stem, path.extname(stem))
343
+ logger.warn({ code: 'layout-sidecar-misnamed', script, layout },
344
+ 'layouts/%s is not loaded as a sidecar and nothing else reads it. A sidecar is '
345
+ + 'named after the LAYOUT, and %s is the layout `%s` — both extensions are '
346
+ + 'dropped — so its sidecar has to be %s.js. The layout renders with empty data '
347
+ + 'until it is renamed.',
348
+ script, layout, name, path.join(path.dirname(stem) === '.' ? '' : path.dirname(stem), name))
349
+ }
350
+
316
351
  const inputs = await sidecarInputs()
317
352
  const scanned = new Set()
318
353
  const stats = { emitted: 0, skipped: 0, deleted: 0 }
package/lib/assembly.js CHANGED
@@ -27,10 +27,21 @@ import {
27
27
  observeConsumed,
28
28
  queryContext,
29
29
  useDatabase,
30
+ reportError,
30
31
  } from 'mikser-io'
31
32
  import { compileDestinationTemplate, sanitizeDestination, primaryDestination } from './destination.js'
32
33
  import { setSidecarStamp, sidecarHookInstalled } from './sidecar-modules.js'
33
34
 
35
+ // What the sidecar actually returned, for a message a reader can act on.
36
+ // `[object Object]` and `` are the two least useful things to print here.
37
+ function describePages(value) {
38
+ if (Array.isArray(value)) return `an array of ${value.length}`
39
+ if (value === null) return 'null'
40
+ if (typeof value === 'object') return `an object with keys ${Object.keys(value).join(', ') || '(none)'}`
41
+ if (typeof value === 'number') return `the number ${value}`
42
+ return `a ${typeof value} (${JSON.stringify(value)})`
43
+ }
44
+
34
45
  export function createOnBeforeRender({
35
46
  runtime, useLogger, useJournal,
36
47
  findById, findEntity, findEntities,
@@ -350,6 +361,56 @@ export function createOnBeforeRender({
350
361
  : null
351
362
 
352
363
  if (data?.pages) {
364
+ // `pages` is the pagination COUNT, and only a count.
365
+ //
366
+ // The guard above is truthiness and the loop below is
367
+ // `page < data.pages`, so a sidecar returning anything
368
+ // truthy that is not a positive number passed the first
369
+ // and ran zero times in the second. `{ pages: [] }`
370
+ // coerces to 0; `{ pages: [{…}] }` coerces to NaN. Both
371
+ // produced NO page: no file, no error, no warning, not
372
+ // counted in `Rendered:`, no manifest snapshot — a green
373
+ // build with the page simply absent, and --audit-output
374
+ // cannot see it either because an entity that never
375
+ // rendered has no snapshot to be missing.
376
+ //
377
+ // `pages` is also the obvious name for the list in a
378
+ // sitemap, which is exactly where it lands.
379
+ //
380
+ // Thrown rather than reinterpreted. The key has one
381
+ // meaning; guessing a second from the value's type is how
382
+ // a name comes to mean two things depending on what you
383
+ // put in it. This fails the render for this entity, which
384
+ // is counted, recorded and named.
385
+ if (!Number.isInteger(data.pages) || data.pages < 1) {
386
+ // Reported, not thrown. This runs in onBeforeRender,
387
+ // a lifecycle hook rather than a render, so an
388
+ // exception here tears the whole build down with a
389
+ // stack trace — one mistyped sidecar stopping every
390
+ // other page, which trades a silent failure for a
391
+ // total one.
392
+ //
393
+ // reportError, not logger.error. This entity could
394
+ // not be turned into output, which is what a render
395
+ // error IS — so it is counted in `errors`, recorded in
396
+ // mikser_failures, and makes a one-shot build exit 1.
397
+ // A coded logger.error would register as a fault, and
398
+ // a fault leaves the build green: "🟢 Mikser completed"
399
+ // beside a red line and exit 0 is the same green build
400
+ // the report was about.
401
+ const message =
402
+ `sidecar returned \`pages\` as ${describePages(data.pages)}. `
403
+ + '`pages` is the pagination COUNT — a positive integer — and is reserved '
404
+ + 'in the data a layout receives, so a list cannot be passed under it. '
405
+ + 'Rename it (`items`, `entries`, `urls`) and the layout renders once '
406
+ + 'with that list available.'
407
+ reportError(entity, new Error(message), { layout: entity.layout?.name ?? null })
408
+ logger.error({ code: 'layout-pages-not-a-count', id: entity.id,
409
+ layout: entity.layout?.name ?? null },
410
+ 'Layout %s: %s — %s produced no output.',
411
+ entity.layout?.name ?? '?', message, entity.id)
412
+ continue
413
+ }
353
414
  if (!_.endsWith(entity.name, entity.format)) {
354
415
  // Loop bound is `< data.pages` (not `data.pages - 1`).
355
416
  // With 4 pages and the old bound, iteration only ran
@@ -409,6 +470,19 @@ export function createOnBeforeRender({
409
470
  sidecarMetaReads: [...(sidecarTrack.metaReads ?? [])],
410
471
  sidecarConsumedReads: [...(sidecarTrack.consumedReads ?? [])].map(([k, v]) => [k, [...v]]) })
411
472
  }
473
+ } else {
474
+ // Pagination was asked for, and nothing was queued.
475
+ //
476
+ // The guard exists so an entity whose name already
477
+ // carries its format is not expanded twice, but it
478
+ // shares the shape that made the type confusion above
479
+ // so expensive: no task, no message, output absent on
480
+ // a green build. Whatever the right handling is, it is
481
+ // not silence.
482
+ logger.warn({ code: 'layout-pagination-skipped', id: entity.id },
483
+ 'Pagination was requested for %s (pages: %d) but its name already ends '
484
+ + 'with its format (%s), so no page was produced.',
485
+ entity.id, data.pages, entity.format)
412
486
  }
413
487
  } else {
414
488
  // Same derivation onProcessed persists onto the catalog
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mikser-io-layouts",
3
- "version": "2.13.0",
3
+ "version": "2.15.0",
4
4
  "description": "SSG-flavor task-production policy for mikser-io. Multi-match layout assignment, per-layout destinations, postprocess chain attachment, layout inspection.",
5
5
  "main": "index.js",
6
6
  "type": "module",