mikser-io-layouts 11.2.0 → 11.3.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 +5 -3
- package/lib/matching.js +51 -0
- package/package.json +1 -1
- package/test/unit.test.js +106 -0
package/index.js
CHANGED
|
@@ -112,9 +112,11 @@ export function layouts(userOptions = {}) {
|
|
|
112
112
|
// and a second copy of the stamping is a second chance for an
|
|
113
113
|
// edited sidecar to keep answering from cache.
|
|
114
114
|
//
|
|
115
|
-
//
|
|
116
|
-
// `
|
|
117
|
-
//
|
|
115
|
+
// Offered, not required of anyone: a consumer asks for the
|
|
116
|
+
// `layouts` service and uses it if present. The render's own use
|
|
117
|
+
// of the same loader reads `load` and `plugins`; a surface that
|
|
118
|
+
// gives layouts behaviour beyond rendering can read exports of
|
|
119
|
+
// its own from the same file.
|
|
118
120
|
sidecar: (layout) => loadSidecarModule(layout, {
|
|
119
121
|
layoutsFolder: layoutsFolderNow(),
|
|
120
122
|
logger: useLogger(),
|
package/lib/matching.js
CHANGED
|
@@ -203,12 +203,28 @@ export function createOnProcessed({
|
|
|
203
203
|
switch (operation) {
|
|
204
204
|
case OPERATION.CREATE:
|
|
205
205
|
case OPERATION.UPDATE:
|
|
206
|
+
let resolutionFailed = false
|
|
206
207
|
try {
|
|
207
208
|
entity.layouts = await resolveLayoutsForEntity(entity)
|
|
208
209
|
} catch (err) {
|
|
209
210
|
logger.error('Layout resolution for %s: %s', entity.id, err.message)
|
|
210
211
|
entity.layouts = []
|
|
212
|
+
resolutionFailed = true
|
|
211
213
|
}
|
|
214
|
+
|
|
215
|
+
// Whether the AUTHOR asked for a layout. Used further
|
|
216
|
+
// down to tell an abandoned page from a typo.
|
|
217
|
+
//
|
|
218
|
+
// Captured here, before the mirror below writes
|
|
219
|
+
// meta.layout for the pattern-matched and auto-layout
|
|
220
|
+
// paths, so the field still answers one question. Read
|
|
221
|
+
// after it, "the author named a layout" and "we recorded
|
|
222
|
+
// the one we chose" are the same value. That is a
|
|
223
|
+
// legibility choice, not a fix: the mirror only writes
|
|
224
|
+
// when a layout was found, so the `layouts.length === 0`
|
|
225
|
+
// term below already excludes every entity it touched.
|
|
226
|
+
const declaredALayout = Boolean(
|
|
227
|
+
entity.meta?.layout || entity.meta?.layouts?.length)
|
|
212
228
|
// Back-compat alias. Most existing downstream code
|
|
213
229
|
// reads `entity.layout`; keep it pointing at the
|
|
214
230
|
// first matched layout so it stays useful. The
|
|
@@ -275,6 +291,41 @@ export function createOnProcessed({
|
|
|
275
291
|
)
|
|
276
292
|
}
|
|
277
293
|
|
|
294
|
+
// Tell the manifest this entity produces nothing, so the
|
|
295
|
+
// snapshots it used to hold stop claiming files nobody
|
|
296
|
+
// writes any more.
|
|
297
|
+
//
|
|
298
|
+
// Take `layout:` out of a document's front matter and the
|
|
299
|
+
// page stayed on disk with a snapshot still vouching for
|
|
300
|
+
// it — and because the file still matched the hash its own
|
|
301
|
+
// render recorded, --audit-output read OK while the site
|
|
302
|
+
// served a page the source no longer asked for. The
|
|
303
|
+
// manifest cannot see this by itself: with no render task
|
|
304
|
+
// there is nothing to compare against, and an asset whose
|
|
305
|
+
// preset threw looks exactly the same from there. This
|
|
306
|
+
// hook is where it IS knowable, so this is where it is
|
|
307
|
+
// said.
|
|
308
|
+
//
|
|
309
|
+
// Only for an entity that declared nothing and matched
|
|
310
|
+
// nothing. The two excluded cases are both errors, and
|
|
311
|
+
// mikser keeps the last good output through an error
|
|
312
|
+
// rather than taking the page down:
|
|
313
|
+
//
|
|
314
|
+
// declared but unresolvable — `layout: page` with no
|
|
315
|
+
// `page` layout, already warned about by
|
|
316
|
+
// resolveLayout. Renaming a layout file would
|
|
317
|
+
// otherwise delete every page that names it, on the
|
|
318
|
+
// strength of a typo.
|
|
319
|
+
// resolution threw — meta.layout and meta.layouts both
|
|
320
|
+
// set, say. Nothing was decided, so nothing follows.
|
|
321
|
+
//
|
|
322
|
+
// Optional-called: the peer floor is core ^11.1.0 and
|
|
323
|
+
// recordNoOutput is newer than that, so on an older
|
|
324
|
+
// engine this is simply the behaviour that came before.
|
|
325
|
+
if (entity.layouts.length === 0 && !declaredALayout && !resolutionFailed) {
|
|
326
|
+
runtime.manifest?.recordNoOutput?.(entity.id)
|
|
327
|
+
}
|
|
328
|
+
|
|
278
329
|
// meta.postprocessor (string) / meta.postprocessors
|
|
279
330
|
// (array) override is read at task-build time (in
|
|
280
331
|
// onBeforeRender) because the layout entity is
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mikser-io-layouts",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.3.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",
|
package/test/unit.test.js
CHANGED
|
@@ -728,6 +728,112 @@ describe('layouts plugin: multi-layouts (matching)', () => {
|
|
|
728
728
|
})
|
|
729
729
|
})
|
|
730
730
|
|
|
731
|
+
describe('layouts plugin: reporting that an entity renders nothing', () => {
|
|
732
|
+
// The manifest keeps a snapshot per (entity, destination) — its claim that
|
|
733
|
+
// the entity produced that file. An entity that STOPS producing output
|
|
734
|
+
// leaves the claim behind, and the page with it: still on disk, still
|
|
735
|
+
// matching the hash its own render recorded, so --audit-output reads OK
|
|
736
|
+
// while the site serves something the source no longer asks for.
|
|
737
|
+
//
|
|
738
|
+
// The manifest cannot see this alone. With no render task there is nothing
|
|
739
|
+
// to compare a claim against, and an asset whose preset threw looks
|
|
740
|
+
// identical from there — so guessing deletes the good derivative a failed
|
|
741
|
+
// preset exists to keep. This hook knows, so this hook says.
|
|
742
|
+
//
|
|
743
|
+
// What must NOT be reported is the whole difficulty. Both exclusions are
|
|
744
|
+
// errors, and mikser's rule through an error is that the last good output
|
|
745
|
+
// survives rather than the page coming down.
|
|
746
|
+
async function report(files, doc, options = {}) {
|
|
747
|
+
return withTempWorking(async (workingFolder) => {
|
|
748
|
+
const h = createHarness({
|
|
749
|
+
options: { workingFolder, outputFolder: path.join(workingFolder, 'out') },
|
|
750
|
+
})
|
|
751
|
+
const reported = []
|
|
752
|
+
h.runtime.manifest = { recordNoOutput: (id) => reported.push(id) }
|
|
753
|
+
layouts(options)(h.core)
|
|
754
|
+
await h.runHook('loaded')
|
|
755
|
+
for (const relativePath of files) {
|
|
756
|
+
await h.runSync('layouts', { action: 'create', context: { relativePath } })
|
|
757
|
+
}
|
|
758
|
+
h.journal.push({ id: 1, entity: doc, operation: 'create', context: {}, options: {}, output: null })
|
|
759
|
+
await h.runHook('processed', { aborted: false })
|
|
760
|
+
return { reported, doc, logs: h.logs }
|
|
761
|
+
})
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const plainDoc = (meta) => ({
|
|
765
|
+
id: '/documents/blog/welcome.md',
|
|
766
|
+
collection: 'documents',
|
|
767
|
+
name: 'blog/welcome',
|
|
768
|
+
format: 'md',
|
|
769
|
+
meta,
|
|
770
|
+
})
|
|
771
|
+
|
|
772
|
+
it('reports an entity that declared nothing and matched nothing', async () => {
|
|
773
|
+
const { reported, doc } = await report(['post.hbs'], plainDoc({}))
|
|
774
|
+
assert.deepEqual(doc.layouts, [], 'precondition: nothing resolved')
|
|
775
|
+
assert.deepEqual(reported, ['/documents/blog/welcome.md'])
|
|
776
|
+
})
|
|
777
|
+
|
|
778
|
+
it('stays quiet when a declared layout merely cannot be found', async () => {
|
|
779
|
+
// A typo, or a layout file renamed. The entity still says which layout
|
|
780
|
+
// it wants, so this is an error to fix — not a page to withdraw.
|
|
781
|
+
// Reporting it would delete every page naming a layout the moment that
|
|
782
|
+
// layout's filename changed.
|
|
783
|
+
const { reported, doc, logs } = await report(['post.hbs'], plainDoc({ layout: 'pst' }))
|
|
784
|
+
assert.deepEqual(doc.layouts, [], 'precondition: nothing resolved')
|
|
785
|
+
assert.ok(logs.some(l => l.args.join(' ').includes('Layout not found')),
|
|
786
|
+
'precondition: it really did fail to resolve')
|
|
787
|
+
assert.deepEqual(reported, [])
|
|
788
|
+
})
|
|
789
|
+
|
|
790
|
+
it('stays quiet when resolution threw', async () => {
|
|
791
|
+
// meta.layout and meta.layouts both set. Nothing was decided, so
|
|
792
|
+
// nothing follows from it.
|
|
793
|
+
const { reported, doc } = await report(
|
|
794
|
+
['post.hbs'], plainDoc({ layout: 'post', layouts: ['post'] }))
|
|
795
|
+
assert.deepEqual(doc.layouts, [], 'precondition: resolution failed')
|
|
796
|
+
assert.deepEqual(reported, [])
|
|
797
|
+
})
|
|
798
|
+
|
|
799
|
+
it('stays quiet when a layout did match', async () => {
|
|
800
|
+
const { reported, doc } = await report(['post.hbs'], plainDoc({ layout: 'post' }))
|
|
801
|
+
assert.equal(doc.layouts.length, 1, 'precondition: it resolved')
|
|
802
|
+
assert.deepEqual(reported, [])
|
|
803
|
+
})
|
|
804
|
+
|
|
805
|
+
it('stays quiet when a CONFIG PATTERN matched and the author declared nothing', async () => {
|
|
806
|
+
// The case that pins down which question `declaredALayout` answers.
|
|
807
|
+
// The mirror block writes meta.layout for pattern-matched and
|
|
808
|
+
// auto-layout entities, so if it were read after that, "the author
|
|
809
|
+
// named a layout" and "we recorded the one we chose" would be the same
|
|
810
|
+
// field — and an entity that rendered perfectly well would be reported
|
|
811
|
+
// as producing nothing, taking its own fresh output off disk.
|
|
812
|
+
const { reported, doc } = await report(
|
|
813
|
+
['post.hbs'], plainDoc({}), { match: { '@/blog/*': 'post' } })
|
|
814
|
+
assert.equal(doc.layouts.length, 1, 'precondition: the pattern matched')
|
|
815
|
+
assert.equal(doc.meta.layout, 'post', 'precondition: and the mirror recorded it')
|
|
816
|
+
assert.deepEqual(reported, [])
|
|
817
|
+
})
|
|
818
|
+
|
|
819
|
+
it('does not require the engine to offer recordNoOutput at all', async () => {
|
|
820
|
+
// The peer floor is core ^11.1.0 and recordNoOutput is newer, so an
|
|
821
|
+
// entity that renders nothing on an older engine has to be a quiet
|
|
822
|
+
// no-op rather than a crash mid-cycle.
|
|
823
|
+
await withTempWorking(async (workingFolder) => {
|
|
824
|
+
const h = createHarness({
|
|
825
|
+
options: { workingFolder, outputFolder: path.join(workingFolder, 'out') },
|
|
826
|
+
})
|
|
827
|
+
assert.equal(h.runtime.manifest, undefined, 'precondition: no manifest on the runtime')
|
|
828
|
+
layouts()(h.core)
|
|
829
|
+
await h.runHook('loaded')
|
|
830
|
+
await h.runSync('layouts', { action: 'create', context: { relativePath: 'post.hbs' } })
|
|
831
|
+
h.journal.push({ id: 1, entity: plainDoc({}), operation: 'create', context: {}, options: {}, output: null })
|
|
832
|
+
await h.runHook('processed', { aborted: false })
|
|
833
|
+
})
|
|
834
|
+
})
|
|
835
|
+
})
|
|
836
|
+
|
|
731
837
|
describe('layouts plugin: multi-layouts (collision)', () => {
|
|
732
838
|
it('drops both render tasks when two layouts produce the same destination', async () => {
|
|
733
839
|
await withTempWorking(async (workingFolder) => {
|