mikser-io-layouts 2.15.0 → 3.1.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 +34 -19
- package/lib/mcp.js +68 -62
- package/package.json +2 -2
- package/test/_harness.js +5 -0
- package/test/check-entity.test.js +42 -27
- package/test/unit.test.js +37 -5
package/index.js
CHANGED
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
checksum as fileChecksum, checksumOf,
|
|
23
23
|
useDatabase,
|
|
24
24
|
writeOutput, reportUnchanged,
|
|
25
|
+
provideService,
|
|
26
|
+
cliOption,
|
|
25
27
|
} from 'mikser-io'
|
|
26
28
|
|
|
27
29
|
import { createInspect } from './lib/inspect.js'
|
|
@@ -87,20 +89,28 @@ export function layouts(userOptions = {}) {
|
|
|
87
89
|
// The old `runtime.state.layouts.sitemap` in-memory map + `uriIndex`
|
|
88
90
|
// are gone: every entity's "sitemap presence" IS its catalog row.
|
|
89
91
|
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
|
|
92
|
+
// Offer the inspection surface as a service. A consumer asks core
|
|
93
|
+
// for 'layouts' and never names this package; before, it read
|
|
94
|
+
// runtime.options.layouts directly, which coupled the two through an
|
|
95
|
+
// object neither owns and cost the `--layouts` flag its name.
|
|
96
|
+
//
|
|
97
|
+
// Provided at factory-eval time — before any hook runs — so a
|
|
98
|
+
// consumer's onLoaded sees it whatever the plugin order.
|
|
99
|
+
// The flag this package could not have until runtime.options.layouts
|
|
100
|
+
// stopped holding an API object: commander derives the property from
|
|
101
|
+
// the long flag, so `--layouts` would have landed a folder string on
|
|
102
|
+
// top of inspect(), after the load phase, breaking a consumer three
|
|
103
|
+
// plugins away.
|
|
104
|
+
cliOption('--layouts <folder>',
|
|
105
|
+
'folder holding the layout templates, relative to the working folder (default: layouts)')
|
|
106
|
+
|
|
107
|
+
provideService('layouts', {
|
|
96
108
|
inspect: createInspect({ runtime, findEntity, findEntities, useDatabase, collection }),
|
|
97
|
-
}
|
|
109
|
+
}, { plugin: 'mikser-io-layouts' })
|
|
98
110
|
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
// in the plugins array for this to fire; that constraint is
|
|
103
|
-
// documented in mikser-io's CLAUDE.md.
|
|
111
|
+
// Tool registration. Registers against core's tool registry, which
|
|
112
|
+
// exists whether or not the mcp plugin is installed — so there is no
|
|
113
|
+
// gate to write and no ordering rule to obey.
|
|
104
114
|
onLoaded(async () => {
|
|
105
115
|
registerMcpTools({ runtime, useLogger, findEntity, findEntities, useDatabase, collection })
|
|
106
116
|
})
|
|
@@ -199,13 +209,18 @@ export function layouts(userOptions = {}) {
|
|
|
199
209
|
layouts: {},
|
|
200
210
|
}
|
|
201
211
|
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
|
|
212
|
+
// --layouts wins, then the config, then the collection name.
|
|
213
|
+
//
|
|
214
|
+
// Read HERE rather than at construction: the option table is not
|
|
215
|
+
// parsed until every plugin has been constructed and had its
|
|
216
|
+
// chance to declare, so a value read in the factory is always
|
|
217
|
+
// undefined. An absolute value is taken as given; anything else
|
|
218
|
+
// is relative to the working folder, like every other folder flag.
|
|
219
|
+
const layoutsFolderName =
|
|
220
|
+
runtime.options.layouts ?? options.layoutsFolder ?? collection
|
|
221
|
+
runtime.options.layoutsFolder = path.isAbsolute(layoutsFolderName)
|
|
222
|
+
? layoutsFolderName
|
|
223
|
+
: path.join(runtime.options.workingFolder, layoutsFolderName)
|
|
209
224
|
runtime.options.layoutsStateFolder = path.join(runtime.options.outputFolder, 'state')
|
|
210
225
|
|
|
211
226
|
logger.debug('Layouts folder: %s', runtime.options.layoutsFolder)
|
package/lib/mcp.js
CHANGED
|
@@ -1,44 +1,47 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// The tool surface for layouts. Registered against the ENGINE's tool
|
|
2
|
+
// registry, which exists whether or not the mcp plugin is installed — so
|
|
3
|
+
// there is no `if (!runtime.options.mcp) return` gate to write, and no rule
|
|
4
|
+
// about where mcp sits in the plugins array.
|
|
5
5
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
6
|
+
// The engine stores BARE names; the mcp plugin prefixes on the way out, so a
|
|
7
|
+
// session still sees `mikser_layouts_inspect` and the CLI sees
|
|
8
|
+
// `layouts_inspect`. One registration, both surfaces.
|
|
9
|
+
//
|
|
10
|
+
// Surfaces the inspect() primitive this package offers as the `layouts`
|
|
11
|
+
// service. The schema and author-facing description live here because
|
|
12
|
+
// they're MCP-flavor; the underlying data shape is owned by lib/inspect.js.
|
|
10
13
|
|
|
11
14
|
import { z } from 'zod'
|
|
15
|
+
import { registerTool, useService } from 'mikser-io'
|
|
12
16
|
|
|
13
17
|
export function registerMcpTools({ runtime, useLogger, findEntity, findEntities, useDatabase, collection }) {
|
|
14
|
-
const
|
|
15
|
-
if (!
|
|
16
|
-
if (!runtime.options.layouts?.inspect) return // shouldn't happen if we run after our own inspect setup, but defensive
|
|
18
|
+
const layouts = useService('layouts')
|
|
19
|
+
if (!layouts?.inspect) return // our own factory has not run yet
|
|
17
20
|
|
|
18
21
|
const logger = useLogger()
|
|
19
|
-
|
|
20
|
-
mcp.simpleTool(
|
|
21
|
-
'mikser_layouts_inspect',
|
|
22
|
-
'Inspect a layout and everything it pulls in. Answers "what does this layout need from a document" '
|
|
23
|
-
+ 'before you write one — which saves a guess-and-render-empty cycle, and is the only way to catch a '
|
|
24
|
-
+ 'mistyped key before it ships a page with a section silently missing.\n\n'
|
|
25
|
-
+ 'START WITH references.contract.meta. It is the list of document meta keys the WHOLE layout tree '
|
|
26
|
-
+ 'consumes, resolved through includes and renamings into the vocabulary a document actually writes — '
|
|
27
|
-
+ 'so `data.meta.hero.tags` in a partial three files down comes back as `hero.tags`. Compare it against '
|
|
28
|
-
+ 'the meta you are about to write: a key in the contract with no value is a gap, and a key you wrote '
|
|
29
|
-
+ 'that appears nowhere in it is usually a typo.\n\n'
|
|
30
|
-
+ 'Check references.contract.complete before trusting a comparison. When false, some branch could not be '
|
|
31
|
-
+ 'read and references.contract.incomplete names each one with a reason.\n\n'
|
|
32
|
-
+ 'Also returns the template source, sample entities targeting the layout, and both reference views: '
|
|
33
|
-
+ 'runtime (what recent renders actually touched) and static (what the source mentions, including '
|
|
34
|
-
+ 'branches no render has taken).',
|
|
22
|
+
registerTool('layouts_inspect',
|
|
35
23
|
{
|
|
36
|
-
|
|
37
|
-
|
|
24
|
+
description: 'Inspect a layout and everything it pulls in. Answers "what does this layout need from a document" '
|
|
25
|
+
+ 'before you write one — which saves a guess-and-render-empty cycle, and is the only way to catch a '
|
|
26
|
+
+ 'mistyped key before it ships a page with a section silently missing.\n\n'
|
|
27
|
+
+ 'START WITH references.contract.meta. It is the list of document meta keys the WHOLE layout tree '
|
|
28
|
+
+ 'consumes, resolved through includes and renamings into the vocabulary a document actually writes — '
|
|
29
|
+
+ 'so `data.meta.hero.tags` in a partial three files down comes back as `hero.tags`. Compare it against '
|
|
30
|
+
+ 'the meta you are about to write: a key in the contract with no value is a gap, and a key you wrote '
|
|
31
|
+
+ 'that appears nowhere in it is usually a typo.\n\n'
|
|
32
|
+
+ 'Check references.contract.complete before trusting a comparison. When false, some branch could not be '
|
|
33
|
+
+ 'read and references.contract.incomplete names each one with a reason.\n\n'
|
|
34
|
+
+ 'Also returns the template source, sample entities targeting the layout, and both reference views: '
|
|
35
|
+
+ 'runtime (what recent renders actually touched) and static (what the source mentions, including '
|
|
36
|
+
+ 'branches no render has taken).',
|
|
37
|
+
inputSchema: {
|
|
38
|
+
id: z.string().describe('Layout id, e.g. "/layouts/reports/royalty.html-pdf.liquid". Use mikser_query_entities with { collection: "layouts" } to discover ids.'),
|
|
39
|
+
samples: z.number().int().min(0).max(10).optional().describe('How many existing entities currently using this layout to include as data-shape examples. Default 3. Only entities with explicit meta.layout match; auto-matched layouts are not surfaced.'),
|
|
40
|
+
},
|
|
38
41
|
},
|
|
39
42
|
async ({ id, samples = 3 }) => {
|
|
40
43
|
try {
|
|
41
|
-
const result = await
|
|
44
|
+
const result = await layouts.inspect(id, { samples })
|
|
42
45
|
return {
|
|
43
46
|
content: [{
|
|
44
47
|
type: 'text',
|
|
@@ -127,7 +130,10 @@ function distance(a, b) {
|
|
|
127
130
|
}
|
|
128
131
|
|
|
129
132
|
export function registerCheckTool({ runtime, findEntity, findEntities, useDatabase, collection, logger }) {
|
|
130
|
-
|
|
133
|
+
// Resolved here rather than in the handlers: this runs from onLoaded, by
|
|
134
|
+
// which point every factory — including our own, which provides it — has
|
|
135
|
+
// run.
|
|
136
|
+
const layouts = useService('layouts')
|
|
131
137
|
|
|
132
138
|
const ok = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] })
|
|
133
139
|
|
|
@@ -258,7 +264,7 @@ export function registerCheckTool({ runtime, findEntity, findEntities, useDataba
|
|
|
258
264
|
// the entity names one. `schemaKey` is the plugin's own config — commonly
|
|
259
265
|
// `meta.layout`, so a layout and its schema share a name.
|
|
260
266
|
const schemaFor = (entity) => {
|
|
261
|
-
const api =
|
|
267
|
+
const api = useService('schemas')
|
|
262
268
|
if (!api?.lookup) return null
|
|
263
269
|
const key = runtime.options.schemasKey ?? runtime.options.schemaKey ?? 'meta.schema'
|
|
264
270
|
const name = key.split('.').reduce((o, k) => o?.[k], entity)
|
|
@@ -382,35 +388,35 @@ export function registerCheckTool({ runtime, findEntity, findEntities, useDataba
|
|
|
382
388
|
return [...(affected ?? [])].filter(cid => cid !== entity.id).sort()
|
|
383
389
|
} catch { return [] }
|
|
384
390
|
}
|
|
385
|
-
|
|
386
|
-
mcp.simpleTool(
|
|
387
|
-
'mikser_check_entity',
|
|
388
|
-
'Check a document against the contract of the layout that renders it, BEFORE the mistake ships.\n\n'
|
|
389
|
-
+ 'A mistyped key does not fail a build. The section it named simply does not render, the page goes out '
|
|
390
|
-
+ 'with a hole in it, and every signal reads clean. This is the check that catches it.\n\n'
|
|
391
|
-
+ 'It classifies the entity first, because not every document is a page. `kind` is one of:\n'
|
|
392
|
-
+ ' page — renders through a layout; checked against that layout\'s contract.\n'
|
|
393
|
-
+ ' data — never renders, but other entities pull it in through a query. Reports who consumes it. '
|
|
394
|
-
+ 'A missing key here surfaces as a hole in THOSE pages, not in this file, and no amount of building '
|
|
395
|
-
+ 'will make it render.\n'
|
|
396
|
-
+ ' unreferenced — never renders and nothing in the catalog reads it. Not an error: the catalog is '
|
|
397
|
-
+ 'readable over the API, so an external consumer may depend on it.\n\n'
|
|
398
|
-
+ 'For a page, read `missing` first — keys the layout consumes UNCONDITIONALLY that this document has '
|
|
399
|
-
+ 'no value for. That is the only list meaning "this is probably wrong". Then `likelyTypos`, which '
|
|
400
|
-
+ 'pairs a key you wrote against a key the layout wanted within two edits; a typo produces both at '
|
|
401
|
-
+ 'once, which is far stronger than either alone.\n\n'
|
|
402
|
-
+ 'Everything else means "you may want to look", never "this is broken": `missingOptional` is guarded '
|
|
403
|
-
+ 'by the layout and safe to omit, `unused` may be consumed by a different layout, and '
|
|
404
|
-
+ '`unresolvedSections` names a section that matched no template under this layout.\n\n'
|
|
405
|
-
+ 'The contract is scoped to the partials this entity ACTUALLY rendered with. Unscoped, a page using '
|
|
406
|
-
+ 'two of twenty sections would be checked against all twenty and a real omission would be one line in '
|
|
407
|
-
+ 'three hundred.\n\n'
|
|
408
|
-
+ 'Check `reliable` before trusting an absence.',
|
|
391
|
+
registerTool('check_entity',
|
|
409
392
|
{
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
393
|
+
description: 'Check a document against the contract of the layout that renders it, BEFORE the mistake ships.\n\n'
|
|
394
|
+
+ 'A mistyped key does not fail a build. The section it named simply does not render, the page goes out '
|
|
395
|
+
+ 'with a hole in it, and every signal reads clean. This is the check that catches it.\n\n'
|
|
396
|
+
+ 'It classifies the entity first, because not every document is a page. `kind` is one of:\n'
|
|
397
|
+
+ ' page — renders through a layout; checked against that layout\'s contract.\n'
|
|
398
|
+
+ ' data — never renders, but other entities pull it in through a query. Reports who consumes it. '
|
|
399
|
+
+ 'A missing key here surfaces as a hole in THOSE pages, not in this file, and no amount of building '
|
|
400
|
+
+ 'will make it render.\n'
|
|
401
|
+
+ ' unreferenced — never renders and nothing in the catalog reads it. Not an error: the catalog is '
|
|
402
|
+
+ 'readable over the API, so an external consumer may depend on it.\n\n'
|
|
403
|
+
+ 'For a page, read `missing` first — keys the layout consumes UNCONDITIONALLY that this document has '
|
|
404
|
+
+ 'no value for. That is the only list meaning "this is probably wrong". Then `likelyTypos`, which '
|
|
405
|
+
+ 'pairs a key you wrote against a key the layout wanted within two edits; a typo produces both at '
|
|
406
|
+
+ 'once, which is far stronger than either alone.\n\n'
|
|
407
|
+
+ 'Everything else means "you may want to look", never "this is broken": `missingOptional` is guarded '
|
|
408
|
+
+ 'by the layout and safe to omit, `unused` may be consumed by a different layout, and '
|
|
409
|
+
+ '`unresolvedSections` names a section that matched no template under this layout.\n\n'
|
|
410
|
+
+ 'The contract is scoped to the partials this entity ACTUALLY rendered with. Unscoped, a page using '
|
|
411
|
+
+ 'two of twenty sections would be checked against all twenty and a real omission would be one line in '
|
|
412
|
+
+ 'three hundred.\n\n'
|
|
413
|
+
+ 'Check `reliable` before trusting an absence.',
|
|
414
|
+
inputSchema: {
|
|
415
|
+
id: z.string().describe('Entity id, e.g. "/documents/en/index.md".'),
|
|
416
|
+
layout: z.string().optional()
|
|
417
|
+
.describe('Layout id to check against. Omit to use the layout this entity last rendered with, '
|
|
418
|
+
+ 'which is what the manifest recorded rather than a prediction.'),
|
|
419
|
+
},
|
|
414
420
|
},
|
|
415
421
|
async ({ id, layout }) => {
|
|
416
422
|
const entity = await findEntity({ id })
|
|
@@ -527,7 +533,7 @@ export function registerCheckTool({ runtime, findEntity, findEntities, useDataba
|
|
|
527
533
|
// statically to every section in the project, so an unscoped
|
|
528
534
|
// contract for a two-section page is the union of the catalogue and
|
|
529
535
|
// a real omission is one line in three hundred.
|
|
530
|
-
const inspected = await
|
|
536
|
+
const inspected = await layouts.inspect(layoutId, {
|
|
531
537
|
samples: 0,
|
|
532
538
|
partials: usedPartials && usedPartials.length ? usedPartials : null,
|
|
533
539
|
})
|
|
@@ -576,7 +582,7 @@ export function registerCheckTool({ runtime, findEntity, findEntities, useDataba
|
|
|
576
582
|
// section look dead, and one such page reported 118 of them
|
|
577
583
|
// while its longer sibling reported none. The document's only
|
|
578
584
|
// difference was using fewer sections.
|
|
579
|
-
const wide = await
|
|
585
|
+
const wide = await layouts.inspect(layoutId, { samples: 0 })
|
|
580
586
|
const anyLayout = new Set([
|
|
581
587
|
...(wide.references?.contract?.meta ?? []),
|
|
582
588
|
...observed,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mikser-io-layouts",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.1.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",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"homepage": "https://github.com/almero-digital-marketing/mikser-io-layouts#readme",
|
|
20
20
|
"peerDependencies": {
|
|
21
|
-
"mikser-io": "^
|
|
21
|
+
"mikser-io": "^10.0.0",
|
|
22
22
|
"zod": "^4.0.0"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
package/test/_harness.js
CHANGED
|
@@ -9,6 +9,7 @@ import _ from 'lodash'
|
|
|
9
9
|
import {
|
|
10
10
|
runtime as realRuntime,
|
|
11
11
|
matchEntity, normalize, changeExtension, getFormatInfo, checksum, AbortError,
|
|
12
|
+
resetServices,
|
|
12
13
|
} from 'mikser-io'
|
|
13
14
|
|
|
14
15
|
const OPERATION = {
|
|
@@ -33,6 +34,10 @@ export function createHarness({
|
|
|
33
34
|
entities = [],
|
|
34
35
|
journal = [],
|
|
35
36
|
} = {}) {
|
|
37
|
+
// A fresh harness is a fresh world, and the service registry is module
|
|
38
|
+
// state that would otherwise carry the previous test's provider into this
|
|
39
|
+
// one — where providing it again is (correctly) an error.
|
|
40
|
+
resetServices()
|
|
36
41
|
const hookNames = [
|
|
37
42
|
'load', 'loaded', 'import', 'imported',
|
|
38
43
|
'process', 'processed', 'persist', 'persisted',
|
|
@@ -16,6 +16,7 @@ import path from 'node:path'
|
|
|
16
16
|
import { createInspect } from '../lib/inspect.js'
|
|
17
17
|
import { registerCheckTool } from '../lib/mcp.js'
|
|
18
18
|
import * as liquid from 'mikser-io-render-liquid'
|
|
19
|
+
import { provideService, resetServices, invokeTool } from 'mikser-io'
|
|
19
20
|
|
|
20
21
|
// Shaped like a real project: a page shell with chrome, and sections dispatched
|
|
21
22
|
// through a registry. The registry is the reason scoping matters — statically it
|
|
@@ -129,15 +130,22 @@ before(async () => {
|
|
|
129
130
|
: new Set()),
|
|
130
131
|
},
|
|
131
132
|
}
|
|
132
|
-
|
|
133
|
-
|
|
133
|
+
// Each fixture activates itself immediately before it is used. The tool
|
|
134
|
+
// registry is one global list now, so a later describe's registration
|
|
135
|
+
// replaces an earlier one — and one test here deliberately reaches back
|
|
136
|
+
// to the no-schema fixture from inside the schema describe.
|
|
137
|
+
const activate = () => {
|
|
138
|
+
resetServices()
|
|
139
|
+
provideService('layouts', {
|
|
140
|
+
inspect: createInspect({ runtime, findEntity, findEntities, useDatabase, collection: 'layouts' }),
|
|
141
|
+
})
|
|
142
|
+
registerCheckTool({ runtime, findEntity, findEntities, useDatabase,
|
|
143
|
+
collection: 'layouts', logger: { debug() {} } })
|
|
144
|
+
}
|
|
145
|
+
check = async (id) => {
|
|
146
|
+
activate()
|
|
147
|
+
return JSON.parse((await invokeTool('check_entity', { id })).content[0].text)
|
|
134
148
|
}
|
|
135
|
-
const registry = new Map()
|
|
136
|
-
runtime.options.mcp = { simpleTool: (name, _d, _s, handler) => registry.set(name, handler) }
|
|
137
|
-
registerCheckTool({ runtime, findEntity, findEntities, useDatabase,
|
|
138
|
-
collection: 'layouts', logger: { debug() {} } })
|
|
139
|
-
|
|
140
|
-
check = async (id) => JSON.parse((await registry.get('mikser_check_entity')({ id })).content[0].text)
|
|
141
149
|
})
|
|
142
150
|
|
|
143
151
|
after(async () => { if (dir) await rm(dir, { recursive: true, force: true }) })
|
|
@@ -346,15 +354,19 @@ describe('mikser_check_entity: with a declared schema', () => {
|
|
|
346
354
|
options: { schemas: { lookup: (n) => (n === 'article' ? schema : undefined), names: () => ['article'] } },
|
|
347
355
|
renderers: new Map([['liquid', { parseReferences: liquid.parseReferences }]]),
|
|
348
356
|
}
|
|
349
|
-
|
|
350
|
-
|
|
357
|
+
const activate = () => {
|
|
358
|
+
resetServices()
|
|
359
|
+
provideService('layouts', {
|
|
360
|
+
inspect: createInspect({ runtime, findEntity, findEntities, useDatabase, collection: 'layouts' }),
|
|
361
|
+
})
|
|
362
|
+
provideService('schemas', runtime.options.schemas)
|
|
363
|
+
registerCheckTool({ runtime, findEntity, findEntities, useDatabase,
|
|
364
|
+
collection: 'layouts', logger: { debug() {} } })
|
|
365
|
+
}
|
|
366
|
+
checkWithSchema = async (id) => {
|
|
367
|
+
activate()
|
|
368
|
+
return JSON.parse((await invokeTool('check_entity', { id })).content[0].text)
|
|
351
369
|
}
|
|
352
|
-
const registry = new Map()
|
|
353
|
-
runtime.options.mcp = { simpleTool: (n, _d, _s, h) => registry.set(n, h) }
|
|
354
|
-
registerCheckTool({ runtime, findEntity, findEntities, useDatabase,
|
|
355
|
-
collection: 'layouts', logger: { debug() {} } })
|
|
356
|
-
checkWithSchema = async (id) =>
|
|
357
|
-
JSON.parse((await registry.get('mikser_check_entity')({ id })).content[0].text)
|
|
358
370
|
})
|
|
359
371
|
|
|
360
372
|
after(async () => { if (dir2) await rm(dir2, { recursive: true, force: true }) })
|
|
@@ -442,17 +454,20 @@ describe('mikser_check_entity: peer comparison', () => {
|
|
|
442
454
|
const findEntities = async (q) => entities.filter(e =>
|
|
443
455
|
Object.entries(q ?? {}).every(([k, v]) => k.split('.').reduce((o, x) => o?.[x], e) === v))
|
|
444
456
|
const runtime = { options: {}, renderers: new Map() }
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
457
|
+
const activate = () => {
|
|
458
|
+
resetServices()
|
|
459
|
+
provideService('layouts', { inspect: async () => ({ references: {} }) })
|
|
460
|
+
registerCheckTool({
|
|
461
|
+
runtime, findEntity, findEntities,
|
|
462
|
+
// No snapshots: these never render, so they are data documents.
|
|
463
|
+
useDatabase: () => ({ handle: { prepare: () => ({ all: () => [], get: () => undefined }) } }),
|
|
464
|
+
collection: 'layouts', logger: { debug() {} },
|
|
465
|
+
})
|
|
466
|
+
}
|
|
467
|
+
peerCheck = async (id) => {
|
|
468
|
+
activate()
|
|
469
|
+
return JSON.parse((await invokeTool('check_entity', { id })).content[0].text)
|
|
470
|
+
}
|
|
456
471
|
})
|
|
457
472
|
|
|
458
473
|
it('reports a key most siblings carry and this record lacks', async () => {
|
package/test/unit.test.js
CHANGED
|
@@ -6,7 +6,7 @@ import { tmpdir } from 'node:os'
|
|
|
6
6
|
import path from 'node:path'
|
|
7
7
|
|
|
8
8
|
import { layouts } from '../index.js'
|
|
9
|
-
import { frontMatter } from 'mikser-io'
|
|
9
|
+
import { frontMatter, useService } from 'mikser-io'
|
|
10
10
|
import { createHarness } from './_harness.js'
|
|
11
11
|
|
|
12
12
|
async function withTempWorking(fn) {
|
|
@@ -30,10 +30,42 @@ describe('layouts plugin', () => {
|
|
|
30
30
|
assert.equal(h.hooks.beforeRender.length, 1)
|
|
31
31
|
assert.equal(h.hooks.complete.length, 1)
|
|
32
32
|
assert.ok(h.sync.has('layouts'))
|
|
33
|
-
// The inspect primitive is
|
|
34
|
-
// onLoaded fires — so
|
|
35
|
-
//
|
|
36
|
-
assert.equal(typeof
|
|
33
|
+
// The inspect primitive is offered at factory-eval — before any
|
|
34
|
+
// onLoaded fires — so a consumer sees it during its own onLoaded
|
|
35
|
+
// whatever the plugin order.
|
|
36
|
+
assert.equal(typeof useService('layouts')?.inspect, 'function')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('puts the layouts folder where --layouts says', async () => {
|
|
40
|
+
// The flag could not exist while runtime.options.layouts held the
|
|
41
|
+
// inspect API: commander names an option after its long flag, so this
|
|
42
|
+
// would have overwritten that object after the load phase and broken
|
|
43
|
+
// a consumer somewhere else entirely.
|
|
44
|
+
await withTempWorking(async (workingFolder) => {
|
|
45
|
+
const h = createHarness({ options: {
|
|
46
|
+
workingFolder, outputFolder: path.join(workingFolder, 'out'), layouts: 'tmpl' } })
|
|
47
|
+
layouts()(h.core)
|
|
48
|
+
await h.runHook('loaded')
|
|
49
|
+
assert.equal(h.runtime.options.layoutsFolder, path.join(workingFolder, 'tmpl'))
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('lets --layouts win over the config, and takes an absolute path as given', async () => {
|
|
54
|
+
await withTempWorking(async (workingFolder) => {
|
|
55
|
+
const h = createHarness({ options: {
|
|
56
|
+
workingFolder, outputFolder: path.join(workingFolder, 'out'), layouts: 'from-cli' } })
|
|
57
|
+
layouts({ layoutsFolder: 'from-config' })(h.core)
|
|
58
|
+
await h.runHook('loaded')
|
|
59
|
+
assert.equal(h.runtime.options.layoutsFolder, path.join(workingFolder, 'from-cli'))
|
|
60
|
+
})
|
|
61
|
+
await withTempWorking(async (workingFolder) => {
|
|
62
|
+
const absolute = path.join(workingFolder, 'elsewhere')
|
|
63
|
+
const h = createHarness({ options: {
|
|
64
|
+
workingFolder, outputFolder: path.join(workingFolder, 'out'), layouts: absolute } })
|
|
65
|
+
layouts()(h.core)
|
|
66
|
+
await h.runHook('loaded')
|
|
67
|
+
assert.equal(h.runtime.options.layoutsFolder, absolute)
|
|
68
|
+
})
|
|
37
69
|
})
|
|
38
70
|
|
|
39
71
|
it('initializes runtime.state.layouts on onLoaded with empty maps', async () => {
|