mikser-io 6.0.6 → 6.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/package.json +4 -2
- package/src/engine.js +53 -10
- package/src/plugins/layouts.js +12 -4
- package/src/plugins/render/hbs.js +14 -1
- package/src/postprocess.js +10 -2
- package/src/render.js +34 -1
- 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.2.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
|
}
|
|
@@ -313,5 +356,5 @@ export async function setup(options) {
|
|
|
313
356
|
}
|
|
314
357
|
|
|
315
358
|
export function useLogger() {
|
|
316
|
-
return runtime.engine
|
|
359
|
+
return runtime.engine?.logger
|
|
317
360
|
}
|
package/src/plugins/layouts.js
CHANGED
|
@@ -59,18 +59,18 @@ export default ({
|
|
|
59
59
|
|
|
60
60
|
function removeFromSitemap(entity) {
|
|
61
61
|
const { sitemap } = runtime.state.layouts
|
|
62
|
+
const matches = (current) =>
|
|
63
|
+
current.id === entity.id || current.parent === entity.id
|
|
62
64
|
for (let href in sitemap) {
|
|
63
65
|
let entry = sitemap[href]
|
|
64
66
|
if (entry.id) {
|
|
65
|
-
if (entry
|
|
67
|
+
if (matches(entry)) {
|
|
66
68
|
delete sitemap[href]
|
|
67
|
-
return
|
|
68
69
|
}
|
|
69
70
|
} else {
|
|
70
71
|
for (let lang in entry) {
|
|
71
|
-
if (entry[lang]
|
|
72
|
+
if (matches(entry[lang])) {
|
|
72
73
|
delete entry[lang]
|
|
73
|
-
return
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
}
|
|
@@ -251,6 +251,11 @@ export default ({
|
|
|
251
251
|
}
|
|
252
252
|
break
|
|
253
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)
|
|
254
259
|
removePagesFromSitemap(entity)
|
|
255
260
|
break
|
|
256
261
|
}
|
|
@@ -292,6 +297,9 @@ export default ({
|
|
|
292
297
|
if (page) {
|
|
293
298
|
pageEntity.page = page + 1
|
|
294
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
|
|
295
303
|
if (entity.meta) {
|
|
296
304
|
if (entity.meta.href) {
|
|
297
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/render.js
CHANGED
|
@@ -2,6 +2,25 @@ import { readFileSync } from 'node:fs'
|
|
|
2
2
|
import { createRequire } from 'node:module'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import _ from 'lodash'
|
|
5
|
+
import { useLogger } from './engine.js'
|
|
6
|
+
|
|
7
|
+
// Flatten template-helper args into a single human-readable message.
|
|
8
|
+
// Handlebars helpers receive a trailing options object (it has a `.hash`
|
|
9
|
+
// property) which we drop.
|
|
10
|
+
function formatLogArgs(args) {
|
|
11
|
+
if (args.length && typeof args[args.length - 1] === 'object' && args[args.length - 1] !== null && 'hash' in args[args.length - 1]) {
|
|
12
|
+
args = args.slice(0, -1)
|
|
13
|
+
}
|
|
14
|
+
return args
|
|
15
|
+
.map(arg => {
|
|
16
|
+
if (arg == null) return String(arg)
|
|
17
|
+
if (typeof arg === 'object') {
|
|
18
|
+
try { return JSON.stringify(arg) } catch { return String(arg) }
|
|
19
|
+
}
|
|
20
|
+
return String(arg)
|
|
21
|
+
})
|
|
22
|
+
.join(' ')
|
|
23
|
+
}
|
|
5
24
|
|
|
6
25
|
export default async ({ entity, options, config, context, state, logger, port }) => {
|
|
7
26
|
logger = logger || {
|
|
@@ -65,7 +84,21 @@ export default async ({ entity, options, config, context, state, logger, port })
|
|
|
65
84
|
data: context.data,
|
|
66
85
|
content() {
|
|
67
86
|
return readFileSync(entity.source, { encoding: 'utf8' })
|
|
68
|
-
}
|
|
87
|
+
},
|
|
88
|
+
// Logger functions are exposed directly so each renderer's
|
|
89
|
+
// auto-helper loop picks them up are picked up; falls back to the local `logger` in
|
|
90
|
+
// worker contexts where the engine singleton isn't initialised.
|
|
91
|
+
//
|
|
92
|
+
// Args are flattened into a single space-separated message so
|
|
93
|
+
// every value the template passed shows up — pino otherwise drops
|
|
94
|
+
// trailing positional args unless the first contains %s/%d format
|
|
95
|
+
// specifiers. Handlebars appends an internal options object as
|
|
96
|
+
// the last arg, which we strip before joining.
|
|
97
|
+
log: (...args) => (useLogger() ?? logger).info(formatLogArgs(args)),
|
|
98
|
+
warn: (...args) => (useLogger() ?? logger).warn(formatLogArgs(args)),
|
|
99
|
+
error: (...args) => (useLogger() ?? logger).error(formatLogArgs(args)),
|
|
100
|
+
debug: (...args) => (useLogger() ?? logger).debug(formatLogArgs(args)),
|
|
101
|
+
trace: (...args) => (useLogger() ?? logger).trace(formatLogArgs(args)),
|
|
69
102
|
}
|
|
70
103
|
|
|
71
104
|
for (let pluginName of pluginsToLoad) {
|
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
|
}
|