mikser-io-layouts 11.1.0 → 11.2.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 +14 -1
- package/lib/assembly.js +12 -11
- package/lib/sidecar-modules.js +31 -0
- package/package.json +1 -1
- package/test/sidecar-modules.test.js +49 -1
package/index.js
CHANGED
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
import { createInspect } from './lib/inspect.js'
|
|
30
30
|
import { createOnProcessed } from './lib/matching.js'
|
|
31
31
|
import { createOnBeforeRender } from './lib/assembly.js'
|
|
32
|
-
import { installSidecarModuleHook } from './lib/sidecar-modules.js'
|
|
32
|
+
import { installSidecarModuleHook, loadSidecarModule } from './lib/sidecar-modules.js'
|
|
33
33
|
import { registerMcpTools } from './lib/mcp.js'
|
|
34
34
|
|
|
35
35
|
export function layouts(userOptions = {}) {
|
|
@@ -106,6 +106,19 @@ export function layouts(userOptions = {}) {
|
|
|
106
106
|
|
|
107
107
|
provideService('layouts', {
|
|
108
108
|
inspect: createInspect({ runtime, findEntity, findEntities, useDatabase, collection }),
|
|
109
|
+
// A layout's sidecar, loaded the way the render loads it —
|
|
110
|
+
// same path rule, same digest stamp. Offered as a service because
|
|
111
|
+
// the alternative is another package importing this one's lib/,
|
|
112
|
+
// and a second copy of the stamping is a second chance for an
|
|
113
|
+
// edited sidecar to keep answering from cache.
|
|
114
|
+
//
|
|
115
|
+
// Consumers: mikser-io-mcp-app, which invokes a sidecar's `call`,
|
|
116
|
+
// `read` and `list` exports for an app's actions and data. The
|
|
117
|
+
// render's own use of the same loader reads `load` and `plugins`.
|
|
118
|
+
sidecar: (layout) => loadSidecarModule(layout, {
|
|
119
|
+
layoutsFolder: layoutsFolderNow(),
|
|
120
|
+
logger: useLogger(),
|
|
121
|
+
}),
|
|
109
122
|
}, { plugin: 'mikser-io-layouts' })
|
|
110
123
|
|
|
111
124
|
// Tool registration. Registers against core's tool registry, which
|
package/lib/assembly.js
CHANGED
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
reportError,
|
|
31
31
|
} from 'mikser-io'
|
|
32
32
|
import { compileDestinationTemplate, sanitizeDestination, primaryDestination } from './destination.js'
|
|
33
|
-
import {
|
|
33
|
+
import { loadSidecarModule } from './sidecar-modules.js'
|
|
34
34
|
|
|
35
35
|
// What the sidecar actually returned, for a message a reader can act on.
|
|
36
36
|
// `[object Object]` and `` are the two least useful things to print here.
|
|
@@ -270,19 +270,20 @@ export function createOnBeforeRender({
|
|
|
270
270
|
const sidecarTrack = createTrack({ partial: false, meta: true, consumed: true })
|
|
271
271
|
if (existsSync(sidecarPath)) {
|
|
272
272
|
try {
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
273
|
+
// Path resolution and stamping live in
|
|
274
|
+
// sidecar-modules.js, shared with the apps surface
|
|
275
|
+
// that invokes a sidecar's action handlers — one
|
|
276
|
+
// implementation, so neither can drift from the
|
|
277
|
+
// digest rule that keeps an edited sidecar from
|
|
278
|
+
// answering out of cache.
|
|
279
|
+
const sidecarModule = await loadSidecarModule(entity.layout, {
|
|
280
|
+
layoutsFolder: runtime.options.layoutsFolder,
|
|
281
|
+
logger,
|
|
282
|
+
});
|
|
282
283
|
// Semicolon above is load-bearing: without it the
|
|
283
284
|
// parenthesised destructuring assignment below reads
|
|
284
285
|
// as a CALL of the preceding expression.
|
|
285
|
-
({ load, plugins = [] } =
|
|
286
|
+
({ load, plugins = [] } = sidecarModule ?? {})
|
|
286
287
|
} catch (err) {
|
|
287
288
|
logger.error('Layout sidecar %s failed to load: %s', sidecarPath.replace(runtime.options.workingFolder + '/', ''), err.message)
|
|
288
289
|
throw err
|
package/lib/sidecar-modules.js
CHANGED
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
// once rather than left to be discovered.
|
|
32
32
|
|
|
33
33
|
import module from 'node:module'
|
|
34
|
+
import path from 'node:path'
|
|
35
|
+
import { existsSync } from 'node:fs'
|
|
34
36
|
import { pathToFileURL } from 'node:url'
|
|
35
37
|
|
|
36
38
|
// Feature-detected rather than imported by name: `import { registerHooks }`
|
|
@@ -80,3 +82,32 @@ export function setSidecarStamp(next) {
|
|
|
80
82
|
export function sidecarHookInstalled() {
|
|
81
83
|
return installed
|
|
82
84
|
}
|
|
85
|
+
|
|
86
|
+
// Load a layout's sidecar module, or null when it has none.
|
|
87
|
+
//
|
|
88
|
+
// One implementation, two callers: the render (which then tracks what the
|
|
89
|
+
// sidecar reads) and the apps surface (which invokes its action handlers).
|
|
90
|
+
// The stamping is the whole reason this is shared — a second copy would drift
|
|
91
|
+
// from the digest rule above and reintroduce the failure that rule exists for:
|
|
92
|
+
// an edited sidecar that keeps answering from cache.
|
|
93
|
+
export async function loadSidecarModule(layout, { layoutsFolder, logger } = {}) {
|
|
94
|
+
if (!layout?.name || !layoutsFolder) return null
|
|
95
|
+
const sidecarPath = path.join(layoutsFolder, `${layout.name}.js`)
|
|
96
|
+
// Existence first, so a real ERR_MODULE_NOT_FOUND from INSIDE the sidecar
|
|
97
|
+
// — it imports a package that is not installed — is not swallowed as
|
|
98
|
+
// "this layout has no sidecar".
|
|
99
|
+
if (!existsSync(sidecarPath)) return null
|
|
100
|
+
|
|
101
|
+
const shared = layout.inputs?.shared || ''
|
|
102
|
+
setSidecarStamp(shared)
|
|
103
|
+
// With the resolve hook installed the digest reaches the sidecar's own
|
|
104
|
+
// imports too; without it only the entry reloads, so fall back to a
|
|
105
|
+
// per-call stamp to keep at least that much.
|
|
106
|
+
const entryStamp = sidecarHookInstalled() ? shared : Date.now()
|
|
107
|
+
try {
|
|
108
|
+
return await import(`${pathToFileURL(sidecarPath).href}?stamp=${entryStamp}`)
|
|
109
|
+
} catch (err) {
|
|
110
|
+
logger?.error('Layout sidecar %s failed to load: %s', sidecarPath, err.message)
|
|
111
|
+
throw err
|
|
112
|
+
}
|
|
113
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mikser-io-layouts",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.2.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",
|
|
@@ -23,7 +23,7 @@ import { mkdtemp, rm, mkdir, writeFile } from 'node:fs/promises'
|
|
|
23
23
|
import { tmpdir } from 'node:os'
|
|
24
24
|
import path from 'node:path'
|
|
25
25
|
|
|
26
|
-
import { installSidecarModuleHook, setSidecarStamp, sidecarHookInstalled } from '../lib/sidecar-modules.js'
|
|
26
|
+
import { installSidecarModuleHook, setSidecarStamp, sidecarHookInstalled, loadSidecarModule } from '../lib/sidecar-modules.js'
|
|
27
27
|
|
|
28
28
|
describe('sidecar module identity', () => {
|
|
29
29
|
let root, layoutsFolder
|
|
@@ -98,3 +98,51 @@ describe('sidecar module identity', () => {
|
|
|
98
98
|
"export const icon = 'filter.svg'\n")
|
|
99
99
|
})
|
|
100
100
|
})
|
|
101
|
+
|
|
102
|
+
// loadSidecarModule — the shared loader. The render path and the apps surface
|
|
103
|
+
// both go through it; a second copy of the path rule or the stamping is a
|
|
104
|
+
// second chance for an edited sidecar to keep answering from cache, which is
|
|
105
|
+
// the failure the whole file above exists for.
|
|
106
|
+
describe('loadSidecarModule', () => {
|
|
107
|
+
let root, layoutsFolder
|
|
108
|
+
|
|
109
|
+
before(async () => {
|
|
110
|
+
root = await mkdtemp(path.join(tmpdir(), 'sidecar-loader-'))
|
|
111
|
+
layoutsFolder = path.join(root, 'layouts')
|
|
112
|
+
await mkdir(layoutsFolder, { recursive: true })
|
|
113
|
+
await writeFile(path.join(layoutsFolder, 'order.js'),
|
|
114
|
+
'export const load = () => ({ page: 1 })\n'
|
|
115
|
+
+ 'export const call = async ({ action }) => ({ handled: action })\n'
|
|
116
|
+
+ 'export const read = async ({ uri }) => ({ uri })\n'
|
|
117
|
+
+ 'export const list = async () => [{ uri: "app://order/rows" }]\n')
|
|
118
|
+
await writeFile(path.join(layoutsFolder, 'plain.js'), 'export const load = () => ({})\n')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
after(async () => { await rm(root, { recursive: true, force: true }) })
|
|
122
|
+
|
|
123
|
+
it('returns the module with every named export a sidecar declares', async () => {
|
|
124
|
+
const sidecar = await loadSidecarModule({ name: 'order', inputs: { shared: 'd1' } }, { layoutsFolder })
|
|
125
|
+
assert.equal(typeof sidecar.load, 'function', 'the render reads load')
|
|
126
|
+
for (const name of ['call', 'read', 'list']) {
|
|
127
|
+
assert.equal(typeof sidecar[name], 'function', `the apps surface reads ${name}`)
|
|
128
|
+
}
|
|
129
|
+
assert.deepEqual(await sidecar.call({ action: 'approve' }), { handled: 'approve' })
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('returns null for a layout with no sidecar, rather than throwing', async () => {
|
|
133
|
+
assert.equal(await loadSidecarModule({ name: 'absent', inputs: {} }, { layoutsFolder }), null)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('returns null when it is asked without a layout or a folder', async () => {
|
|
137
|
+
assert.equal(await loadSidecarModule(null, { layoutsFolder }), null)
|
|
138
|
+
assert.equal(await loadSidecarModule({ name: 'order' }, {}), null)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('leaves a sidecar without the apps exports alone', async () => {
|
|
142
|
+
// Every existing sidecar is this shape. Reading `call` off one must be
|
|
143
|
+
// undefined, not an error — the apps surface decides what to do.
|
|
144
|
+
const sidecar = await loadSidecarModule({ name: 'plain', inputs: { shared: 'd1' } }, { layoutsFolder })
|
|
145
|
+
assert.equal(typeof sidecar.load, 'function')
|
|
146
|
+
assert.equal(sidecar.call, undefined)
|
|
147
|
+
})
|
|
148
|
+
})
|