mikser-io 7.8.2 → 7.10.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/package.json +1 -1
- package/src/api.js +26 -1
- package/src/engine.js +2 -2
- package/src/plugins/api.js +3 -1
- package/src/plugins/assets.js +84 -43
- package/src/plugins/layouts.js +13 -0
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -7,6 +7,31 @@ import { writeFile, unlink, mkdir } from 'node:fs/promises'
|
|
|
7
7
|
import path from 'node:path'
|
|
8
8
|
import './lifecycle.js' // side-effect: attaches runtime.create / update / delete
|
|
9
9
|
|
|
10
|
+
// Build the error thrown when a submitted entity goes through a full
|
|
11
|
+
// process cycle but never renders. useRenderer can't see *why* — by the
|
|
12
|
+
// time it gives up, the journal is cleared — but it knows what it
|
|
13
|
+
// submitted, so it branches on whether a layout was even requested and
|
|
14
|
+
// points at the log line carrying the authoritative reason (the layouts
|
|
15
|
+
// plugin warns when a render-requested entity matches no layout).
|
|
16
|
+
//
|
|
17
|
+
// `.status = 422` (Unprocessable Entity): the request was well-formed
|
|
18
|
+
// but the entity can't be rendered as submitted. The API endpoint reads
|
|
19
|
+
// err.status (defaulting to 500) so a no-layout render returns a client
|
|
20
|
+
// error, not a server fault.
|
|
21
|
+
function incompleteRenderError(entity) {
|
|
22
|
+
const id = entity?.id ?? '<unknown>'
|
|
23
|
+
const msg = entity?.meta?.layout
|
|
24
|
+
? `Render did not complete for ${id}: it requested layout "${entity.meta.layout}" ` +
|
|
25
|
+
`but produced no output. Check the log for a "Layout not found" warning (the name ` +
|
|
26
|
+
`may not exist) or a "Render error" (the layout matched but threw).`
|
|
27
|
+
: `Render did not complete for ${id}: the entity has no meta.layout and matched no ` +
|
|
28
|
+
`layout, so nothing rendered it. Set meta.layout, add a layouts.match rule, or name ` +
|
|
29
|
+
`it to match a layout (auto-layout).`
|
|
30
|
+
const err = new Error(msg)
|
|
31
|
+
err.status = 422
|
|
32
|
+
return err
|
|
33
|
+
}
|
|
34
|
+
|
|
10
35
|
/**
|
|
11
36
|
* Bind to the runtime and return an on-demand renderer that pipelines
|
|
12
37
|
* concurrent calls into the minimum number of `runtime.process()` cycles.
|
|
@@ -79,7 +104,7 @@ export function useRenderer(runtime, { defaultTimeout = 30_000 } = {}) {
|
|
|
79
104
|
} finally {
|
|
80
105
|
for (const item of remaining.values()) {
|
|
81
106
|
clearTimeout(item.timer)
|
|
82
|
-
item.reject(
|
|
107
|
+
item.reject(incompleteRenderError(item.entity))
|
|
83
108
|
}
|
|
84
109
|
const idx = completedHooks.indexOf(hook)
|
|
85
110
|
if (idx >= 0) completedHooks.splice(idx, 1)
|
package/src/engine.js
CHANGED
|
@@ -131,7 +131,7 @@ export async function setup(options) {
|
|
|
131
131
|
runtime.options.port = runtime.options.server === true
|
|
132
132
|
? 3001
|
|
133
133
|
: Number(runtime.options.server) || 3001
|
|
134
|
-
logger.
|
|
134
|
+
logger.debug('Server starting on port %d', runtime.options.port)
|
|
135
135
|
|
|
136
136
|
// Trust-proxy: when mikser is behind a reverse proxy
|
|
137
137
|
// (nginx, Caddy, an Express app, ngrok with edge), the
|
|
@@ -211,7 +211,7 @@ export async function setup(options) {
|
|
|
211
211
|
// workers, useLogger consumers) gains the side-channel
|
|
212
212
|
// automatically — no second logger to thread through.
|
|
213
213
|
wireLoggerToMcp(runtime.engine.logger, runtime.options.mcp)
|
|
214
|
-
logger.
|
|
214
|
+
logger.debug('MCP substrate ready (mounts at %s when server is up)', runtime.options.mcpPath)
|
|
215
215
|
} catch (err) {
|
|
216
216
|
logger.error('Failed to enable MCP: %s', err.message)
|
|
217
217
|
}
|
package/src/plugins/api.js
CHANGED
|
@@ -627,7 +627,9 @@ export default ({
|
|
|
627
627
|
} catch (err) {
|
|
628
628
|
logger.error('Api[%s] render error: %s', name, err.message)
|
|
629
629
|
if (!res.headersSent) {
|
|
630
|
-
|
|
630
|
+
// useRenderer tags an unrenderable entity (no layout)
|
|
631
|
+
// with err.status = 422; everything else is a 500.
|
|
632
|
+
res.status(err.status ?? 500).json({ error: err.message })
|
|
631
633
|
}
|
|
632
634
|
}
|
|
633
635
|
})
|
package/src/plugins/assets.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
import { mkdir, writeFile, unlink, rm, readFile, symlink, } from 'fs/promises'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
|
+
import { createRequire } from 'node:module'
|
|
3
5
|
import { globby } from 'globby'
|
|
4
6
|
import _ from 'lodash'
|
|
5
7
|
import map from 'p-map'
|
|
@@ -42,6 +44,50 @@ export default ({
|
|
|
42
44
|
return entityPresets
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
// Resolve a preset name to an importable module location. Local
|
|
48
|
+
// files in presetsFolder win; names with no local file fall back to
|
|
49
|
+
// an npm package named `mikser-io-preset-<name>`, resolved from the
|
|
50
|
+
// project's node_modules. Mirrors how postprocess.js resolves
|
|
51
|
+
// post-* plugins — local override first, then the npm convention.
|
|
52
|
+
// Returns { uri, watchable } or null when neither exists.
|
|
53
|
+
//
|
|
54
|
+
// `watchable` distinguishes the two lifetimes: local presets are
|
|
55
|
+
// re-imported (cache-busted) on every load so watch-mode edits take
|
|
56
|
+
// effect; npm presets are versioned by their package, imported once.
|
|
57
|
+
function resolvePreset(name) {
|
|
58
|
+
const local = path.join(runtime.options.presetsFolder, `${name}.js`)
|
|
59
|
+
if (existsSync(local)) {
|
|
60
|
+
return { uri: local, watchable: true }
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const require = createRequire(path.join(runtime.options.workingFolder, 'package.json'))
|
|
64
|
+
const uri = require.resolve(`mikser-io-preset-${name}`)
|
|
65
|
+
return { uri, watchable: false }
|
|
66
|
+
} catch {
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Import a preset module and build its catalog entity. One place for
|
|
72
|
+
// the (revision, format, options) export contract so onImport and
|
|
73
|
+
// onSync stay in sync. Cache-busts local presets so watch-mode edits
|
|
74
|
+
// reload; npm presets import once (their version is the cache key).
|
|
75
|
+
async function buildPreset({ name, uri, watchable }) {
|
|
76
|
+
const cacheBust = watchable ? `?stamp=${Date.now()}` : ''
|
|
77
|
+
const { revision = 1, format, options } = await import(`${uri}${cacheBust}`)
|
|
78
|
+
return {
|
|
79
|
+
id: `/presets/${name}`,
|
|
80
|
+
collection,
|
|
81
|
+
type,
|
|
82
|
+
uri,
|
|
83
|
+
name,
|
|
84
|
+
source: uri,
|
|
85
|
+
format,
|
|
86
|
+
checksum: revision,
|
|
87
|
+
options,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
45
91
|
async function getRevisions(entity) {
|
|
46
92
|
let revisions = await globby(`${entity.destination.replaceAll('(', '\\(').replaceAll(')', '\\)')}.*.md5`, {
|
|
47
93
|
cwd: path.join(runtime.options.assetsFolder, entity.preset.name),
|
|
@@ -147,26 +193,16 @@ export default ({
|
|
|
147
193
|
const logger = useLogger()
|
|
148
194
|
const { presets } = runtime.state.assets
|
|
149
195
|
|
|
196
|
+
// Watch only fires for files in presetsFolder, so these are
|
|
197
|
+
// always local presets — watchable: true.
|
|
150
198
|
const name = relativePath.replace(path.extname(relativePath), '')
|
|
151
199
|
const uri = path.join(runtime.options.presetsFolder, relativePath)
|
|
152
|
-
const source = uri
|
|
153
200
|
|
|
154
201
|
let synced = true
|
|
155
202
|
switch (action) {
|
|
156
203
|
case ACTION.CREATE:
|
|
157
204
|
try {
|
|
158
|
-
const
|
|
159
|
-
const preset = {
|
|
160
|
-
id: path.join('/presets', relativePath),
|
|
161
|
-
collection,
|
|
162
|
-
type,
|
|
163
|
-
uri,
|
|
164
|
-
name: relativePath.replace(path.extname(relativePath), ''),
|
|
165
|
-
source,
|
|
166
|
-
format,
|
|
167
|
-
checksum: revision,
|
|
168
|
-
options
|
|
169
|
-
}
|
|
205
|
+
const preset = await buildPreset({ name, uri, watchable: true })
|
|
170
206
|
presets[name] = preset
|
|
171
207
|
await createEntity(preset)
|
|
172
208
|
} catch (err) {
|
|
@@ -176,19 +212,12 @@ export default ({
|
|
|
176
212
|
break
|
|
177
213
|
case ACTION.UPDATE:
|
|
178
214
|
try {
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
name: relativePath.replace(path.extname(relativePath), ''),
|
|
186
|
-
checksum: revision,
|
|
187
|
-
source,
|
|
188
|
-
format,
|
|
189
|
-
options
|
|
190
|
-
}
|
|
191
|
-
if (!preset[name]) {
|
|
215
|
+
const preset = await buildPreset({ name, uri, watchable: true })
|
|
216
|
+
// Was `!preset[name]` — a typo for `!presets[name]`
|
|
217
|
+
// that made every UPDATE take the create branch
|
|
218
|
+
// (preset[name] is always undefined). Fixed: only
|
|
219
|
+
// re-emit when the revision actually changed.
|
|
220
|
+
if (!presets[name]) {
|
|
192
221
|
presets[name] = preset
|
|
193
222
|
await createEntity(preset)
|
|
194
223
|
} else if (presets[name].checksum != preset.checksum) {
|
|
@@ -218,33 +247,45 @@ export default ({
|
|
|
218
247
|
const logger = useLogger()
|
|
219
248
|
const { presets } = runtime.state.assets
|
|
220
249
|
|
|
250
|
+
// Local presets: scan the presets folder. Loads every *.js
|
|
251
|
+
// there, keyed by filename. Local always wins.
|
|
221
252
|
const paths = await globby('*.js', { cwd: runtime.options.presetsFolder })
|
|
222
253
|
for (let relativePath of paths) {
|
|
254
|
+
const name = relativePath.replace(path.extname(relativePath), '')
|
|
223
255
|
const uri = path.join(runtime.options.presetsFolder, relativePath)
|
|
224
|
-
const source = uri
|
|
225
256
|
try {
|
|
226
|
-
const
|
|
227
|
-
const name = relativePath.replace(path.extname(relativePath), '')
|
|
228
|
-
|
|
229
|
-
const preset = {
|
|
230
|
-
id: path.join('/presets', relativePath),
|
|
231
|
-
collection,
|
|
232
|
-
type,
|
|
233
|
-
uri,
|
|
234
|
-
name: relativePath.replace(path.extname(relativePath), ''),
|
|
235
|
-
source,
|
|
236
|
-
format,
|
|
237
|
-
checksum: revision,
|
|
238
|
-
options,
|
|
239
|
-
options
|
|
240
|
-
}
|
|
241
|
-
|
|
257
|
+
const preset = await buildPreset({ name, uri, watchable: true })
|
|
242
258
|
await createEntity(preset)
|
|
243
259
|
presets[name] = preset
|
|
244
260
|
} catch (err) {
|
|
245
261
|
logger.error(err, 'Preset loading error: %s', uri)
|
|
246
262
|
}
|
|
247
263
|
}
|
|
264
|
+
|
|
265
|
+
// npm presets: any preset name referenced in config that no
|
|
266
|
+
// local file already provided resolves to an npm package
|
|
267
|
+
// `mikser-io-preset-<name>`. Config is the source of truth for
|
|
268
|
+
// which presets a project uses; this fills the names a folder
|
|
269
|
+
// scan can't (the code lives in node_modules, not presets/).
|
|
270
|
+
for (const name of Object.keys(runtime.config.assets?.presets || {})) {
|
|
271
|
+
if (presets[name]) continue // a local file already loaded it
|
|
272
|
+
const resolved = resolvePreset(name)
|
|
273
|
+
if (!resolved) {
|
|
274
|
+
logger.error(
|
|
275
|
+
'Preset not found: %s (no presets/%s.js, no npm package mikser-io-preset-%s)',
|
|
276
|
+
name, name, name,
|
|
277
|
+
)
|
|
278
|
+
continue
|
|
279
|
+
}
|
|
280
|
+
try {
|
|
281
|
+
const preset = await buildPreset({ name, uri: resolved.uri, watchable: resolved.watchable })
|
|
282
|
+
await createEntity(preset)
|
|
283
|
+
presets[name] = preset
|
|
284
|
+
logger.debug('Preset loaded from npm: mikser-io-preset-%s', name)
|
|
285
|
+
} catch (err) {
|
|
286
|
+
logger.error(err, 'Preset loading error (npm): mikser-io-preset-%s', name)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
248
289
|
})
|
|
249
290
|
|
|
250
291
|
onProcessed(async (signal) => {
|
package/src/plugins/layouts.js
CHANGED
|
@@ -304,6 +304,19 @@ export default ({
|
|
|
304
304
|
if (entity.meta?.layout && !entity.layout) {
|
|
305
305
|
logger.warn('Layout not found for %s: %s', entity.collection, entity.id)
|
|
306
306
|
}
|
|
307
|
+
// A render-requested entity (carries useRenderer's
|
|
308
|
+
// correlationId) that resolved to no layout will
|
|
309
|
+
// silently produce nothing — the caller just gets
|
|
310
|
+
// api.js's "did not complete". Surface the real reason
|
|
311
|
+
// here, where we authoritatively know no layout matched.
|
|
312
|
+
// Gated on correlationId so the thousands of normal
|
|
313
|
+
// layout-less content files stay quiet.
|
|
314
|
+
if (!entity.layout && entity.options?.correlationId) {
|
|
315
|
+
logger.warn(
|
|
316
|
+
'Render requested for %s but no layout matched — set meta.layout, add a layouts.match rule, or name it to match a layout (auto-layout). Entities without a layout are not rendered.',
|
|
317
|
+
entity.id,
|
|
318
|
+
)
|
|
319
|
+
}
|
|
307
320
|
|
|
308
321
|
if (entity.layout && entity.meta?.postprocessor) {
|
|
309
322
|
entity.layout.postprocessor = entity.meta.postprocessor
|