mikser-io 6.0.5 → 6.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/package.json +4 -2
- package/src/engine.js +52 -9
- package/src/plugins/layouts.js +20 -8
- package/src/plugins/render/hbs.js +14 -1
- package/src/postprocess.js +10 -2
- package/src/utils.js +18 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mikser-io",
|
|
3
|
-
"version": "6.0
|
|
3
|
+
"version": "6.1.0",
|
|
4
4
|
"description": "<p align=\"center\"> <img src=\"mikser-lockup-stacked.svg\" alt=\"mikser\" width=\"198\" /> </p>",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -53,7 +53,9 @@
|
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"fluent-ffmpeg": "^2.1.3",
|
|
56
|
-
"mikser-io-
|
|
56
|
+
"mikser-io-post-mjml": "file:../mikser-io-post-mjml",
|
|
57
|
+
"mikser-io-render-liquid": "file:../mikser-io-render-liquid",
|
|
58
|
+
"mikser-io-render-markdown": "file:../mikser-io-render-markdown",
|
|
57
59
|
"sharp": "^0.34.5"
|
|
58
60
|
},
|
|
59
61
|
"directories": {
|
package/src/engine.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import pino from 'pino'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { Command } from 'commander'
|
|
4
|
-
import { rm, lstat, realpath, mkdir, writeFile } from 'fs/promises'
|
|
4
|
+
import { rm, lstat, realpath, mkdir, writeFile, readFile, unlink } from 'fs/promises'
|
|
5
5
|
import { existsSync } from 'fs'
|
|
6
6
|
import _ from 'lodash'
|
|
7
7
|
import Piscina from 'piscina'
|
|
@@ -10,7 +10,7 @@ import { onInitialize, onInitialized, onRender, onCancel, onCancelled, onFinaliz
|
|
|
10
10
|
import { useJournal, updateEntry } from './journal.js'
|
|
11
11
|
import { globby } from 'globby'
|
|
12
12
|
import { OPERATION, TASKS } from './constants.js'
|
|
13
|
-
import { changeExtension } from './utils.js'
|
|
13
|
+
import { changeExtension, formatErrorContext } from './utils.js'
|
|
14
14
|
import render from './render.js'
|
|
15
15
|
import postprocess, { loadPlugin as loadPostPlugin } from './postprocess.js'
|
|
16
16
|
import map from 'p-map'
|
|
@@ -89,6 +89,27 @@ export async function setup(options) {
|
|
|
89
89
|
onLoaded(async () => {
|
|
90
90
|
const logger = useLogger()
|
|
91
91
|
logger.debug(runtime.options, 'Mikser options')
|
|
92
|
+
|
|
93
|
+
// Cumulative render manifest — survives across watch cycles, used to
|
|
94
|
+
// unlink stale output files when their source entity is deleted.
|
|
95
|
+
// Keyed by "<entity.id>:<entity.destination>" so paginated outputs
|
|
96
|
+
// for the same id stay distinct.
|
|
97
|
+
runtime.state.manifest = new Map()
|
|
98
|
+
const manifestPath = path.join(runtime.options.runtimeFolder, 'render-details.json')
|
|
99
|
+
if (existsSync(manifestPath)) {
|
|
100
|
+
try {
|
|
101
|
+
const arr = JSON.parse(await readFile(manifestPath, 'utf8'))
|
|
102
|
+
if (Array.isArray(arr)) {
|
|
103
|
+
for (const entity of arr) {
|
|
104
|
+
if (entity?.id && entity?.destination) {
|
|
105
|
+
runtime.state.manifest.set(`${entity.id}:${entity.destination}`, entity)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} catch (err) {
|
|
110
|
+
logger.warn('Could not load render-details.json: %s', err.message)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
92
113
|
})
|
|
93
114
|
|
|
94
115
|
onRender(async (signal) => {
|
|
@@ -156,7 +177,7 @@ export async function setup(options) {
|
|
|
156
177
|
} catch (err) {
|
|
157
178
|
if (!signal.aborted) {
|
|
158
179
|
await updateEntry({ id, output: { success: false } })
|
|
159
|
-
logger.error('Render error: %s %s', entity.id, err.message)
|
|
180
|
+
logger.error('Render error: %s%s %s', entity.id, formatErrorContext(entity, err, runtime.options), err.message)
|
|
160
181
|
}
|
|
161
182
|
logger.debug('Render canceled')
|
|
162
183
|
}
|
|
@@ -171,15 +192,37 @@ export async function setup(options) {
|
|
|
171
192
|
})
|
|
172
193
|
|
|
173
194
|
onAfterRender(async () => {
|
|
174
|
-
const
|
|
195
|
+
const logger = useLogger()
|
|
196
|
+
const manifest = runtime.state.manifest
|
|
197
|
+
|
|
198
|
+
// Unlink stale output files for entities deleted in this cycle, and
|
|
199
|
+
// prune them from the manifest. Matches by `entity.id` for direct hits
|
|
200
|
+
// and by `entity.parent` so paginated children (whose id was rewritten
|
|
201
|
+
// via changeExtension) are reclaimed alongside their source.
|
|
202
|
+
for await (let { entity } of useJournal('Manifest cleanup', [OPERATION.DELETE])) {
|
|
203
|
+
for (const [key, value] of manifest) {
|
|
204
|
+
if (value.id === entity.id || value.parent === entity.id) {
|
|
205
|
+
const filePath = path.join(runtime.options.outputFolder, value.destination)
|
|
206
|
+
try {
|
|
207
|
+
await unlink(filePath)
|
|
208
|
+
logger.debug('Manifest unlinked stale output: %s', value.destination)
|
|
209
|
+
} catch { }
|
|
210
|
+
manifest.delete(key)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Merge this cycle's successful renders. New ids appear; re-rendered
|
|
216
|
+
// ids overwrite (same key); ids whose destination changed leave the
|
|
217
|
+
// old key as a stale entry — handled by the cleanup pass on next DELETE.
|
|
175
218
|
for await (let { output, entity } of useJournal('Output', [OPERATION.RENDER])) {
|
|
176
219
|
if (output?.success) {
|
|
177
|
-
|
|
178
|
-
results.set(jobId, entity)
|
|
220
|
+
manifest.set(`${entity.id}:${entity.destination}`, entity)
|
|
179
221
|
}
|
|
180
222
|
}
|
|
181
|
-
|
|
182
|
-
|
|
223
|
+
|
|
224
|
+
const manifestPath = path.join(runtime.options.runtimeFolder, 'render-details.json')
|
|
225
|
+
await writeFile(manifestPath, JSON.stringify(Array.from(manifest.values())), 'utf8')
|
|
183
226
|
})
|
|
184
227
|
|
|
185
228
|
onBeforePostprocess(async (signal) => {
|
|
@@ -259,7 +302,7 @@ export async function setup(options) {
|
|
|
259
302
|
} catch (err) {
|
|
260
303
|
if (!signal.aborted) {
|
|
261
304
|
await updateEntry({ id, output: { success: false } })
|
|
262
|
-
logger.error('Postprocess error: %s %s', entity.id, err.message)
|
|
305
|
+
logger.error('Postprocess error: %s%s %s', entity.id, formatErrorContext(entity, err, runtime.options), err.message)
|
|
263
306
|
}
|
|
264
307
|
logger.debug('Postprocess canceled')
|
|
265
308
|
}
|
package/src/plugins/layouts.js
CHANGED
|
@@ -20,6 +20,8 @@ export default ({
|
|
|
20
20
|
onSync,
|
|
21
21
|
matchEntity,
|
|
22
22
|
changeExtension,
|
|
23
|
+
findEntity,
|
|
24
|
+
findEntities,
|
|
23
25
|
constants: { ACTION, OPERATION, TASKS },
|
|
24
26
|
}) => {
|
|
25
27
|
const collection = 'layouts'
|
|
@@ -57,18 +59,18 @@ export default ({
|
|
|
57
59
|
|
|
58
60
|
function removeFromSitemap(entity) {
|
|
59
61
|
const { sitemap } = runtime.state.layouts
|
|
62
|
+
const matches = (current) =>
|
|
63
|
+
current.id === entity.id || current.parent === entity.id
|
|
60
64
|
for (let href in sitemap) {
|
|
61
65
|
let entry = sitemap[href]
|
|
62
66
|
if (entry.id) {
|
|
63
|
-
if (entry
|
|
67
|
+
if (matches(entry)) {
|
|
64
68
|
delete sitemap[href]
|
|
65
|
-
return
|
|
66
69
|
}
|
|
67
70
|
} else {
|
|
68
71
|
for (let lang in entry) {
|
|
69
|
-
if (entry[lang]
|
|
72
|
+
if (matches(entry[lang])) {
|
|
70
73
|
delete entry[lang]
|
|
71
|
-
return
|
|
72
74
|
}
|
|
73
75
|
}
|
|
74
76
|
}
|
|
@@ -206,9 +208,10 @@ export default ({
|
|
|
206
208
|
break
|
|
207
209
|
}
|
|
208
210
|
}
|
|
209
|
-
if (!entity.layout && runtime.config.layouts?.autoLayouts && entity.
|
|
210
|
-
const
|
|
211
|
-
const
|
|
211
|
+
if (!entity.layout && runtime.config.layouts?.autoLayouts && entity.id) {
|
|
212
|
+
const lookupBase = entity.id.replace(`/${entity.collection}/`,'')
|
|
213
|
+
const dir = path.dirname(lookupBase)
|
|
214
|
+
const base = path.basename(lookupBase)
|
|
212
215
|
const chunks = base.split('.')
|
|
213
216
|
const candidates = []
|
|
214
217
|
|
|
@@ -248,6 +251,11 @@ export default ({
|
|
|
248
251
|
}
|
|
249
252
|
break
|
|
250
253
|
case OPERATION.DELETE:
|
|
254
|
+
// DELETE journal entries are sparse (id/collection/type only),
|
|
255
|
+
// so the uri-based removePagesFromSitemap can't match. Walk
|
|
256
|
+
// the sitemap by id first; keep the uri-based sweep for any
|
|
257
|
+
// paginated children that match by uri.
|
|
258
|
+
removeFromSitemap(entity)
|
|
251
259
|
removePagesFromSitemap(entity)
|
|
252
260
|
break
|
|
253
261
|
}
|
|
@@ -256,6 +264,7 @@ export default ({
|
|
|
256
264
|
})
|
|
257
265
|
|
|
258
266
|
onBeforeRender(async (signal) => {
|
|
267
|
+
const logger = useLogger()
|
|
259
268
|
const tasks = []
|
|
260
269
|
const entities = Array.from(getSitemapEntities())
|
|
261
270
|
.filter(entity => entity.layout)
|
|
@@ -274,7 +283,7 @@ export default ({
|
|
|
274
283
|
try {
|
|
275
284
|
var { load, plugins = [] } = await import(`${path.join(runtime.options.layoutsFolder, entity.layout.name)}.js?stamp=${Date.now()}`)
|
|
276
285
|
if (load) {
|
|
277
|
-
data = await load(entity, signal)
|
|
286
|
+
data = await load({ entity, findEntity, findEntities, runtime, signal })
|
|
278
287
|
}
|
|
279
288
|
} catch (err) {
|
|
280
289
|
if (err.code != 'ERR_MODULE_NOT_FOUND') throw err
|
|
@@ -288,6 +297,9 @@ export default ({
|
|
|
288
297
|
if (page) {
|
|
289
298
|
pageEntity.page = page + 1
|
|
290
299
|
pageEntity.id = changeExtension(entity.id, `${pageEntity.page}.${entity.layout.format}`)
|
|
300
|
+
// Remember the source entity id so the render manifest
|
|
301
|
+
// can reclaim paginated outputs when the parent is deleted.
|
|
302
|
+
pageEntity.parent = entity.id
|
|
291
303
|
if (entity.meta) {
|
|
292
304
|
if (entity.meta.href) {
|
|
293
305
|
pageEntity.meta.href = `${entity.meta.href}.${pageEntity.page}`
|
|
@@ -53,5 +53,18 @@ export async function render({ entity, runtime }) {
|
|
|
53
53
|
sandbox[helper] = runtime[helper]
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
-
|
|
56
|
+
try {
|
|
57
|
+
return runtime.hbs(source, sandbox)
|
|
58
|
+
} catch (err) {
|
|
59
|
+
// Handlebars compile errors expose `.lineNumber`/`.column`; runtime
|
|
60
|
+
// errors (missing helper, etc.) don't, but we still know the layout.
|
|
61
|
+
// Parse errors put the line in the message as "Parse error on line N".
|
|
62
|
+
if (err.lineNumber != null && err.line == null) err.line = err.lineNumber
|
|
63
|
+
if (err.line == null && typeof err.message === 'string') {
|
|
64
|
+
const m = err.message.match(/on line (\d+)/i)
|
|
65
|
+
if (m) err.line = Number(m[1])
|
|
66
|
+
}
|
|
67
|
+
err.layoutUri = entity.layout.uri
|
|
68
|
+
throw err
|
|
69
|
+
}
|
|
57
70
|
}
|
package/src/postprocess.js
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
2
3
|
import _ from 'lodash'
|
|
3
4
|
|
|
4
5
|
export async function loadPlugin(pluginName, workingFolder) {
|
|
6
|
+
const require = createRequire(path.join(workingFolder, 'package.json'))
|
|
7
|
+
let nodeModulesResolved
|
|
8
|
+
try {
|
|
9
|
+
nodeModulesResolved = require.resolve(`mikser-io-${pluginName}`)
|
|
10
|
+
} catch { }
|
|
11
|
+
|
|
5
12
|
const resolveLocations = [
|
|
6
|
-
path.join(workingFolder, 'node_modules', `mikser-
|
|
13
|
+
path.join(workingFolder, 'node_modules', `mikser-io-${pluginName}/index.js`),
|
|
14
|
+
nodeModulesResolved,
|
|
7
15
|
path.join(workingFolder, 'plugins', `${pluginName}.js`),
|
|
8
16
|
path.join(path.dirname(import.meta.url), 'plugins', 'post', `${pluginName.replace('post-', '')}.js`)
|
|
9
|
-
]
|
|
17
|
+
].filter(Boolean)
|
|
10
18
|
for (let resolveLocation of resolveLocations) {
|
|
11
19
|
try {
|
|
12
20
|
return await import(resolveLocation)
|
package/src/utils.js
CHANGED
|
@@ -62,4 +62,22 @@ export function changeExtension(file, format) {
|
|
|
62
62
|
let extension = path.extname(file)
|
|
63
63
|
let result = file.substring(0, file.length - extension.length) + '.' + format
|
|
64
64
|
return result
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Build a compact "[layouts/foo.hbs:12:4]" suffix from whatever the
|
|
68
|
+
// underlying template engine attached to its thrown error. Renderer
|
|
69
|
+
// plugins are expected to set `err.layoutUri` (and optionally `err.line` /
|
|
70
|
+
// `err.column`) before rethrowing.
|
|
71
|
+
export function formatErrorContext(entity, err, options) {
|
|
72
|
+
const layoutUri = err?.layoutUri || entity?.layout?.uri || entity?.layout?.id
|
|
73
|
+
if (!layoutUri) return ''
|
|
74
|
+
const workingFolder = options?.workingFolder
|
|
75
|
+
const rel = workingFolder && layoutUri.startsWith(workingFolder + '/')
|
|
76
|
+
? layoutUri.slice(workingFolder.length + 1)
|
|
77
|
+
: layoutUri
|
|
78
|
+
const line = err?.line ?? err?.lineNumber
|
|
79
|
+
const column = err?.column ?? err?.col
|
|
80
|
+
let pos = ''
|
|
81
|
+
if (line) pos = `:${line}${column ? ':' + column : ''}`
|
|
82
|
+
return ` [${rel}${pos}]`
|
|
65
83
|
}
|