mikser-io 6.2.0 → 6.3.1
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/engine.js +20 -1
- package/src/plugins/layouts.js +25 -6
- package/src/plugins.js +12 -2
- package/src/postprocess.js +33 -4
- package/src/render.js +15 -5
package/package.json
CHANGED
package/src/engine.js
CHANGED
|
@@ -226,14 +226,33 @@ export async function setup(options) {
|
|
|
226
226
|
})
|
|
227
227
|
|
|
228
228
|
onBeforePostprocess(async (signal) => {
|
|
229
|
+
// Resolve the output extension for each postprocessor exactly once
|
|
230
|
+
// per cycle. A plugin can declare `export const output = '...'` to
|
|
231
|
+
// separate its name from the produced file extension (e.g. post-mjml
|
|
232
|
+
// names itself "mjml" but emits ".html"). Falls back to the
|
|
233
|
+
// postprocessor name for plugins that don't declare it — preserves
|
|
234
|
+
// the current behavior for post-pdf and any other existing plugin.
|
|
235
|
+
const outputExtCache = new Map()
|
|
236
|
+
const resolveOutputExt = async (postprocessor) => {
|
|
237
|
+
if (outputExtCache.has(postprocessor)) return outputExtCache.get(postprocessor)
|
|
238
|
+
let ext = postprocessor
|
|
239
|
+
try {
|
|
240
|
+
const plugin = await loadPostPlugin(`post-${postprocessor}`, runtime.options.workingFolder)
|
|
241
|
+
if (plugin?.output) ext = plugin.output
|
|
242
|
+
} catch { /* loadPostPlugin already logged */ }
|
|
243
|
+
outputExtCache.set(postprocessor, ext)
|
|
244
|
+
return ext
|
|
245
|
+
}
|
|
246
|
+
|
|
229
247
|
const tasks = []
|
|
230
248
|
for await (const { entity, options, context, output } of useJournal('Queuing postprocess', [OPERATION.RENDER], signal)) {
|
|
231
249
|
if (output?.success && options.postprocessor) {
|
|
250
|
+
const ext = await resolveOutputExt(options.postprocessor)
|
|
232
251
|
tasks.push({
|
|
233
252
|
entity: {
|
|
234
253
|
...entity,
|
|
235
254
|
origin: entity.destination,
|
|
236
|
-
destination: changeExtension(entity.destination,
|
|
255
|
+
destination: changeExtension(entity.destination, ext)
|
|
237
256
|
},
|
|
238
257
|
options: { postprocessor: options.postprocessor, tasks: options.tasks },
|
|
239
258
|
context
|
package/src/plugins/layouts.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
import { mkdir, writeFile, unlink } from 'node:fs/promises'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
3
4
|
import { globby } from 'globby'
|
|
4
5
|
import _ from 'lodash'
|
|
5
6
|
|
|
@@ -280,13 +281,27 @@ export default ({
|
|
|
280
281
|
const entity = _.cloneDeep(original)
|
|
281
282
|
entity.destination = '/' + entity.name
|
|
282
283
|
let data
|
|
283
|
-
|
|
284
|
-
|
|
284
|
+
let load
|
|
285
|
+
let plugins = []
|
|
286
|
+
const sidecarPath = `${path.join(runtime.options.layoutsFolder, entity.layout.name)}.js`
|
|
287
|
+
// Existence-check first so a real ERR_MODULE_NOT_FOUND inside the
|
|
288
|
+
// sidecar (e.g. it imports a missing package) doesn't get swallowed
|
|
289
|
+
// as "sidecar doesn't exist".
|
|
290
|
+
if (existsSync(sidecarPath)) {
|
|
291
|
+
try {
|
|
292
|
+
({ load, plugins = [] } = await import(`${sidecarPath}?stamp=${Date.now()}`))
|
|
293
|
+
} catch (err) {
|
|
294
|
+
logger.error('Layout sidecar %s failed to load: %s', sidecarPath.replace(runtime.options.workingFolder + '/', ''), err.message)
|
|
295
|
+
throw err
|
|
296
|
+
}
|
|
285
297
|
if (load) {
|
|
286
|
-
|
|
298
|
+
try {
|
|
299
|
+
data = await load({ entity, findEntity, findEntities, runtime, signal })
|
|
300
|
+
} catch (err) {
|
|
301
|
+
logger.error('Layout sidecar %s load() threw: %s', sidecarPath.replace(runtime.options.workingFolder + '/', ''), err.message)
|
|
302
|
+
throw err
|
|
303
|
+
}
|
|
287
304
|
}
|
|
288
|
-
} catch (err) {
|
|
289
|
-
if (err.code != 'ERR_MODULE_NOT_FOUND') throw err
|
|
290
305
|
}
|
|
291
306
|
|
|
292
307
|
if (data?.pages) {
|
|
@@ -370,7 +385,11 @@ export default ({
|
|
|
370
385
|
} catch { }
|
|
371
386
|
await writeFile(destinationFile, output.result)
|
|
372
387
|
logger.debug('Layout render finished: %s', entity.destination.replace(runtime.options.workingFolder, ''))
|
|
373
|
-
if (entity.origin) {
|
|
388
|
+
if (entity.origin && entity.origin !== entity.destination) {
|
|
389
|
+
// Don't unlink the origin if it was the same path we just
|
|
390
|
+
// wrote to (post plugins that produce the same extension as
|
|
391
|
+
// the renderer's output — e.g. MJML→HTML on `*.html-mjml.*`
|
|
392
|
+
// layouts). Otherwise we'd delete our own final file.
|
|
374
393
|
const originFile = path.join(runtime.options.outputFolder, entity.origin)
|
|
375
394
|
try {
|
|
376
395
|
await unlink(originFile)
|
package/src/plugins.js
CHANGED
|
@@ -27,6 +27,10 @@ export async function loadPlugin(pluginName) {
|
|
|
27
27
|
if (fs.existsSync(resolveLocation.replace('file:', ''))) {
|
|
28
28
|
try {
|
|
29
29
|
const plugin = await import(resolveLocation)
|
|
30
|
+
if (typeof plugin.default !== 'function') {
|
|
31
|
+
logger.error('Plugin %s loaded from %s but does not export a default factory function', pluginName, resolveLocation)
|
|
32
|
+
return
|
|
33
|
+
}
|
|
30
34
|
const pluginRuntime = plugin.default(core)
|
|
31
35
|
runtime.engine[pluginName] = pluginRuntime
|
|
32
36
|
if (pluginRuntime) {
|
|
@@ -36,12 +40,18 @@ export async function loadPlugin(pluginName) {
|
|
|
36
40
|
}
|
|
37
41
|
return
|
|
38
42
|
} catch (err) {
|
|
39
|
-
|
|
43
|
+
// ERR_MODULE_NOT_FOUND here means the plugin file resolved
|
|
44
|
+
// but one of its imports didn't — a real bug worth surfacing.
|
|
45
|
+
if (err.code === 'ERR_MODULE_NOT_FOUND') {
|
|
46
|
+
logger.error('Plugin %s found at %s but its dependencies are missing: %s', pluginName, resolveLocation, err.message)
|
|
47
|
+
} else {
|
|
48
|
+
logger.error('Plugin %s failed to load (%s): %s', pluginName, resolveLocation, err.message)
|
|
49
|
+
}
|
|
40
50
|
return
|
|
41
51
|
}
|
|
42
52
|
}
|
|
43
53
|
}
|
|
44
|
-
logger.error('Plugin not
|
|
54
|
+
logger.error('Plugin %s not found.', pluginName)
|
|
45
55
|
}
|
|
46
56
|
|
|
47
57
|
onLoad(async () => {
|
package/src/postprocess.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
import { createRequire } from 'node:module'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
3
4
|
import _ from 'lodash'
|
|
5
|
+
import { useLogger } from './engine.js'
|
|
4
6
|
|
|
5
7
|
export async function loadPlugin(pluginName, workingFolder) {
|
|
8
|
+
const logger = useLogger()
|
|
6
9
|
const require = createRequire(path.join(workingFolder, 'package.json'))
|
|
7
10
|
let nodeModulesResolved
|
|
8
11
|
try {
|
|
9
12
|
nodeModulesResolved = require.resolve(`mikser-io-${pluginName}`)
|
|
10
|
-
} catch { }
|
|
13
|
+
} catch { /* package not installed at this level — fine, try next */ }
|
|
11
14
|
|
|
12
15
|
const resolveLocations = [
|
|
13
16
|
path.join(workingFolder, 'node_modules', `mikser-io-${pluginName}/index.js`),
|
|
@@ -15,13 +18,25 @@ export async function loadPlugin(pluginName, workingFolder) {
|
|
|
15
18
|
path.join(workingFolder, 'plugins', `${pluginName}.js`),
|
|
16
19
|
path.join(path.dirname(import.meta.url), 'plugins', 'post', `${pluginName.replace('post-', '')}.js`)
|
|
17
20
|
].filter(Boolean)
|
|
21
|
+
|
|
18
22
|
for (let resolveLocation of resolveLocations) {
|
|
23
|
+
// See render.js loadPlugin — existence-check first so we can
|
|
24
|
+
// distinguish "plugin not at this path" from "plugin found but its
|
|
25
|
+
// transitive deps are missing".
|
|
26
|
+
if (!existsSync(resolveLocation.replace(/^file:/, ''))) continue
|
|
19
27
|
try {
|
|
20
28
|
return await import(resolveLocation)
|
|
21
29
|
} catch (err) {
|
|
22
|
-
if (err.code
|
|
30
|
+
if (err.code === 'ERR_MODULE_NOT_FOUND') {
|
|
31
|
+
logger?.error('Postprocess plugin %s found at %s but its dependencies are missing: %s', pluginName, resolveLocation, err.message)
|
|
32
|
+
} else {
|
|
33
|
+
logger?.error('Postprocess plugin %s failed to load (%s): %s', pluginName, resolveLocation, err.message)
|
|
34
|
+
}
|
|
35
|
+
throw err
|
|
23
36
|
}
|
|
24
37
|
}
|
|
38
|
+
|
|
39
|
+
logger?.error('Postprocess plugin %s not found.', pluginName)
|
|
25
40
|
}
|
|
26
41
|
|
|
27
42
|
export default async ({ entity, options, config, context, state, logger }) => {
|
|
@@ -46,10 +61,24 @@ export default async ({ entity, options, config, context, state, logger }) => {
|
|
|
46
61
|
|
|
47
62
|
for (let pluginName of pluginsToLoad) {
|
|
48
63
|
const plugin = await loadPlugin(pluginName, options.workingFolder)
|
|
64
|
+
if (!plugin) continue // loadPlugin already logged the "not found" path
|
|
49
65
|
plugins[pluginName] = plugin
|
|
50
|
-
if (plugin
|
|
66
|
+
if (plugin.load) {
|
|
67
|
+
try {
|
|
68
|
+
await plugin.load({ entity, options, config: config[pluginName], context, runtime, state, logger })
|
|
69
|
+
} catch (err) {
|
|
70
|
+
logger.error('Postprocess plugin %s load() failed: %s', pluginName, err.message)
|
|
71
|
+
throw err
|
|
72
|
+
}
|
|
73
|
+
}
|
|
51
74
|
}
|
|
52
75
|
|
|
53
76
|
const postprocessorPlugin = plugins[`post-${postprocessor}`]
|
|
54
|
-
|
|
77
|
+
if (!postprocessorPlugin) {
|
|
78
|
+
throw new Error(`Postprocessor "${postprocessor}" was requested but plugin "post-${postprocessor}" is not loaded`)
|
|
79
|
+
}
|
|
80
|
+
if (typeof postprocessorPlugin.postprocess !== 'function') {
|
|
81
|
+
throw new Error(`Plugin "post-${postprocessor}" does not export a postprocess() function`)
|
|
82
|
+
}
|
|
83
|
+
return await postprocessorPlugin.postprocess({ entity, options, config, context, plugins, runtime, state, logger })
|
|
55
84
|
}
|
package/src/render.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs'
|
|
1
|
+
import { readFileSync, existsSync } from 'node:fs'
|
|
2
2
|
import { createRequire } from 'node:module'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import _ from 'lodash'
|
|
@@ -46,7 +46,7 @@ export default async ({ entity, options, config, context, state, logger, port })
|
|
|
46
46
|
let nodeModulesResolved
|
|
47
47
|
try {
|
|
48
48
|
nodeModulesResolved = require.resolve(`mikser-io-${pluginName}`)
|
|
49
|
-
} catch { }
|
|
49
|
+
} catch { /* package not installed at this level — fine, try next */ }
|
|
50
50
|
|
|
51
51
|
const resolveLocations = [
|
|
52
52
|
path.join(options.workingFolder, 'node_modules', `mikser-io-${pluginName}/index.js`),
|
|
@@ -54,16 +54,26 @@ export default async ({ entity, options, config, context, state, logger, port })
|
|
|
54
54
|
path.join(options.workingFolder, 'plugins', `${pluginName}.js`),
|
|
55
55
|
path.join(path.dirname(import.meta.url), 'plugins', 'render', `${pluginName.replace('render-', '')}.js`)
|
|
56
56
|
].filter(Boolean)
|
|
57
|
+
|
|
57
58
|
for (let resolveLocation of resolveLocations) {
|
|
59
|
+
// Existence-check first: once we know the plugin file is there,
|
|
60
|
+
// any subsequent ERR_MODULE_NOT_FOUND is a *transitive* dep
|
|
61
|
+
// missing (e.g. plugin imports a package that isn't installed),
|
|
62
|
+
// not a "this plugin isn't here, try the next path" signal.
|
|
63
|
+
if (!existsSync(resolveLocation.replace(/^file:/, ''))) continue
|
|
58
64
|
try {
|
|
59
65
|
return await import(resolveLocation)
|
|
60
66
|
} catch (err) {
|
|
61
|
-
if (err.code
|
|
62
|
-
logger.error('
|
|
63
|
-
|
|
67
|
+
if (err.code === 'ERR_MODULE_NOT_FOUND') {
|
|
68
|
+
logger.error('Render plugin %s found at %s but its dependencies are missing: %s', pluginName, resolveLocation, err.message)
|
|
69
|
+
} else {
|
|
70
|
+
logger.error('Render plugin %s failed to load (%s): %s', pluginName, resolveLocation, err.message)
|
|
64
71
|
}
|
|
72
|
+
throw err
|
|
65
73
|
}
|
|
66
74
|
}
|
|
75
|
+
|
|
76
|
+
logger.error('Render plugin %s not found.', pluginName)
|
|
67
77
|
}
|
|
68
78
|
|
|
69
79
|
const { renderer } = options
|