mikser-io-layouts 2.11.2 → 2.13.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 +72 -4
- package/package.json +1 -1
- package/test/unit.test.js +40 -5
package/index.js
CHANGED
|
@@ -123,6 +123,9 @@ export function layouts(userOptions = {}) {
|
|
|
123
123
|
// what makes their dependents re-render.
|
|
124
124
|
if (_.endsWith(relativePath, '.js') && isSidecarScript(relativePath)) {
|
|
125
125
|
logger.debug('Layouts sidecar changed (%s) — rescanning layouts', relativePath)
|
|
126
|
+
// The rescan re-catalogues the sidecar itself and re-emits
|
|
127
|
+
// whichever layouts its digest moved, so both the entity and
|
|
128
|
+
// its dependents follow from one call.
|
|
126
129
|
await rescanLayouts()
|
|
127
130
|
return
|
|
128
131
|
}
|
|
@@ -242,12 +245,13 @@ export function layouts(userOptions = {}) {
|
|
|
242
245
|
//
|
|
243
246
|
// Two components:
|
|
244
247
|
// own — the layout's own sidecar, `<name>.js`
|
|
245
|
-
// shared —
|
|
248
|
+
// shared — ALL sidecar scripts under the folder, as one digest
|
|
249
|
+
// (its own included; `own` just names it separately)
|
|
246
250
|
//
|
|
247
251
|
// `shared` is coarse on purpose: a sidecar's own imports
|
|
248
252
|
// (layouts/lib/context.js and friends) would otherwise need a module
|
|
249
|
-
// graph walk.
|
|
250
|
-
// layout is what the folder can afford — tens of files, not
|
|
253
|
+
// graph walk. One global digest means any sidecar change invalidates
|
|
254
|
+
// EVERY layout, which is what the folder can afford — tens of files, not
|
|
251
255
|
// thousands — and it is predictable, which a partial graph walk
|
|
252
256
|
// would not be.
|
|
253
257
|
// NOT every .js under the folder is a sidecar. `post.hbs.js` is a
|
|
@@ -292,6 +296,23 @@ export function layouts(userOptions = {}) {
|
|
|
292
296
|
const { layouts } = runtime.state.layouts
|
|
293
297
|
const logger = useLogger()
|
|
294
298
|
const paths = await globby('**/*', { cwd: runtime.options.layoutsFolder, ignore: ['**/*.js'] })
|
|
299
|
+
// Sidecars are catalogued too — as entities of their own, not as
|
|
300
|
+
// layouts. They are where a site's data layer lives (`data.children`
|
|
301
|
+
// on this project is an entire catalog listing), and until now they
|
|
302
|
+
// were the one source file no tool could read, search or resolve:
|
|
303
|
+
// an agent could see what a template does but not what feeds it.
|
|
304
|
+
//
|
|
305
|
+
// They are scanned in the SAME pass so the checksum gate and the
|
|
306
|
+
// delete sweep cover them; a separate pass would sweep them away.
|
|
307
|
+
//
|
|
308
|
+
// The original objection to giving them entities was the id
|
|
309
|
+
// stripping — `page.js` becoming a phantom `/layouts/page` beside
|
|
310
|
+
// the real `/layouts/page.liquid`. Keeping the `.js` removes that,
|
|
311
|
+
// and they stay out of the layouts NAME map so nothing resolves a
|
|
312
|
+
// template to one. Rendering is not a risk either: layout matching
|
|
313
|
+
// skips this collection outright.
|
|
314
|
+
const sidecarPaths = (await globby('**/*.js', { cwd: runtime.options.layoutsFolder }))
|
|
315
|
+
.filter(isSidecarScript)
|
|
295
316
|
const inputs = await sidecarInputs()
|
|
296
317
|
const scanned = new Set()
|
|
297
318
|
const stats = { emitted: 0, skipped: 0, deleted: 0 }
|
|
@@ -309,11 +330,29 @@ export function layouts(userOptions = {}) {
|
|
|
309
330
|
// before the loop; the gate hits a Map.get instead of a per-
|
|
310
331
|
// file SQL lookup.
|
|
311
332
|
const priorChecksums = checksumsByCollection(collection)
|
|
312
|
-
for (let relativePath of paths) {
|
|
333
|
+
for (let relativePath of [...paths, ...sidecarPaths]) {
|
|
313
334
|
const uri = path.join(runtime.options.layoutsFolder, relativePath)
|
|
314
335
|
const id = path.join('/layouts', relativePath)
|
|
315
336
|
scanned.add(id)
|
|
316
337
|
|
|
338
|
+
// A sidecar keeps its extension in both id and name, so it can
|
|
339
|
+
// never be mistaken for the layout it sits beside, and it is
|
|
340
|
+
// NOT put in the layouts map — that map answers "which template
|
|
341
|
+
// renders this", and a sidecar renders nothing.
|
|
342
|
+
if (sidecarPaths.includes(relativePath)) {
|
|
343
|
+
const chksum = await gateChecksum(uri, id, { priorChecksums })
|
|
344
|
+
if (chksum === null) { stats.skipped++; continue }
|
|
345
|
+
await createEntity({
|
|
346
|
+
id, uri, collection,
|
|
347
|
+
type: 'sidecar',
|
|
348
|
+
name: relativePath,
|
|
349
|
+
content: await readLayoutContent(uri),
|
|
350
|
+
checksum: chksum,
|
|
351
|
+
})
|
|
352
|
+
stats.emitted++
|
|
353
|
+
continue
|
|
354
|
+
}
|
|
355
|
+
|
|
317
356
|
const name = relativePath.replace(path.extname(relativePath), '')
|
|
318
357
|
const chksum = await gateChecksum(uri, id, {
|
|
319
358
|
priorChecksums,
|
|
@@ -342,6 +381,35 @@ export function layouts(userOptions = {}) {
|
|
|
342
381
|
},
|
|
343
382
|
}
|
|
344
383
|
Object.assign(layout, await getFormatInfo(relativePath))
|
|
384
|
+
|
|
385
|
+
// Two files can resolve to ONE layout name, and this map is
|
|
386
|
+
// keyed by name — so the second one silently replaced the
|
|
387
|
+
// first and which one won depended on scan order.
|
|
388
|
+
//
|
|
389
|
+
// getFormatInfo strips the whole compound extension:
|
|
390
|
+
// sitemap.xml.liquid, sitemap.liquid and sitemap.xml.js are
|
|
391
|
+
// all named `sitemap`. A document asking for `layout: sitemap`
|
|
392
|
+
// then got whichever landed last, and if that was a `js`
|
|
393
|
+
// template it rendered nothing — no error, no warning, the
|
|
394
|
+
// build green and the page absent. Reported from a real
|
|
395
|
+
// migration, where the same shape worked for one layout and
|
|
396
|
+
// not another and no rule could be found, because the rule was
|
|
397
|
+
// directory order.
|
|
398
|
+
//
|
|
399
|
+
// Not resolved here, because picking a winner would be
|
|
400
|
+
// guessing at intent. Said out loud instead, naming both
|
|
401
|
+
// files, which is the thing that was missing.
|
|
402
|
+
const clash = layouts[layout.name]
|
|
403
|
+
if (clash && clash.id !== layout.id) {
|
|
404
|
+
logger.warn(
|
|
405
|
+
{ code: 'layout-name-collision', name: layout.name, files: [clash.uri, layout.uri] },
|
|
406
|
+
'Layouts %j is claimed by two files — %s and %s. Both reduce to the same layout name, '
|
|
407
|
+
+ 'so one replaces the other and which one wins is directory order. A document asking '
|
|
408
|
+
+ 'for this layout may render with the wrong template, or with a `js` template that '
|
|
409
|
+
+ 'emits nothing at all. Rename one of them.',
|
|
410
|
+
layout.name, clash.uri, layout.uri)
|
|
411
|
+
}
|
|
412
|
+
|
|
345
413
|
layouts[layout.name] = layout
|
|
346
414
|
await createEntity(layout)
|
|
347
415
|
stats.emitted++
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mikser-io-layouts",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.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
|
@@ -1042,12 +1042,47 @@ describe('layouts plugin: a sidecar is a tracked input (layouts#4)', () => {
|
|
|
1042
1042
|
assert.notEqual(before.checksums.post, after.checksums.post)
|
|
1043
1043
|
})
|
|
1044
1044
|
|
|
1045
|
-
it('
|
|
1046
|
-
//
|
|
1047
|
-
//
|
|
1048
|
-
//
|
|
1045
|
+
it('catalogues a sidecar as an entity of its own', async () => {
|
|
1046
|
+
// A sidecar is where a site's data layer lives, and it used to be the
|
|
1047
|
+
// one source file no tool could read, search or resolve: an agent could
|
|
1048
|
+
// see what a template does but not what feeds it.
|
|
1049
1049
|
const { emitted } = await scan({ 'page.liquid': '<p>x</p>', 'page.js': 'export const load = () => ({})' })
|
|
1050
|
-
assert.deepEqual(emitted.map(e => e.id).sort(), ['/layouts/page.liquid'])
|
|
1050
|
+
assert.deepEqual(emitted.map(e => e.id).sort(), ['/layouts/page.js', '/layouts/page.liquid'])
|
|
1051
|
+
})
|
|
1052
|
+
|
|
1053
|
+
it('keeps the .js, so a sidecar cannot be mistaken for its layout', async () => {
|
|
1054
|
+
// The original objection to cataloguing it: stripping the extension
|
|
1055
|
+
// produces a phantom /layouts/page beside the real
|
|
1056
|
+
// /layouts/page.liquid, and `page` is how layouts are resolved.
|
|
1057
|
+
const { emitted } = await scan({ 'page.liquid': '<p>x</p>', 'page.js': 'export const load = () => ({})' })
|
|
1058
|
+
const sidecar = emitted.find(e => e.id === '/layouts/page.js')
|
|
1059
|
+
assert.equal(sidecar.name, 'page.js', 'the name keeps its extension too')
|
|
1060
|
+
assert.equal(sidecar.type, 'sidecar')
|
|
1061
|
+
assert.ok(!emitted.some(e => e.id === '/layouts/page'), 'no phantom')
|
|
1062
|
+
})
|
|
1063
|
+
|
|
1064
|
+
it('carries the source, so it can be read and searched', async () => {
|
|
1065
|
+
const source = 'export const load = () => ({ children: [] })'
|
|
1066
|
+
const { emitted } = await scan({ 'page.liquid': '<p>x</p>', 'page.js': source })
|
|
1067
|
+
assert.equal(emitted.find(e => e.id === '/layouts/page.js').content, source)
|
|
1068
|
+
})
|
|
1069
|
+
|
|
1070
|
+
it('does not put a sidecar in the layouts name map', async () => {
|
|
1071
|
+
// That map answers "which template renders this". A sidecar renders
|
|
1072
|
+
// nothing, and an entry there would shadow a real layout called `page`.
|
|
1073
|
+
await withTempWorking(async (workingFolder) => {
|
|
1074
|
+
const h = createHarness({ options: { workingFolder, outputFolder: path.join(workingFolder, 'out') } })
|
|
1075
|
+
await mkdir(path.join(workingFolder, 'layouts'), { recursive: true })
|
|
1076
|
+
await writeFile(path.join(workingFolder, 'layouts', 'page.liquid'), '<p>x</p>')
|
|
1077
|
+
await writeFile(path.join(workingFolder, 'layouts', 'page.js'), 'export const load = () => ({})')
|
|
1078
|
+
layouts()(h.core)
|
|
1079
|
+
await h.runHook('loaded')
|
|
1080
|
+
await h.runHook('import')
|
|
1081
|
+
const map = h.runtime.state.layouts.layouts
|
|
1082
|
+
assert.ok(map.page, 'the real layout is there')
|
|
1083
|
+
assert.equal(map.page.id, '/layouts/page.liquid')
|
|
1084
|
+
assert.equal(map['page.js'], undefined, 'the sidecar is not')
|
|
1085
|
+
})
|
|
1051
1086
|
})
|
|
1052
1087
|
|
|
1053
1088
|
it('still treats a JS-AUTHORED layout as a layout, not a sidecar', async () => {
|