mikser-io 6.2.0 → 6.3.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mikser-io",
3
- "version": "6.2.0",
3
+ "version": "6.3.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": {
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, options.postprocessor)
255
+ destination: changeExtension(entity.destination, ext)
237
256
  },
238
257
  options: { postprocessor: options.postprocessor, tasks: options.tasks },
239
258
  context
@@ -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
- try {
284
- var { load, plugins = [] } = await import(`${path.join(runtime.options.layoutsFolder, entity.layout.name)}.js?stamp=${Date.now()}`)
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
- data = await load({ entity, findEntity, findEntities, runtime, signal })
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
- logger.error('Plugin load error: [%s] %s', pluginName, err.message)
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 loaded: %s', pluginName)
54
+ logger.error('Plugin %s not found. Searched: %s', pluginName, resolveLocations.join(', '))
45
55
  }
46
56
 
47
57
  onLoad(async () => {
@@ -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 != 'ERR_MODULE_NOT_FOUND') throw err
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. Searched: %s', pluginName, resolveLocations.join(', '))
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?.load) await plugin.load({ entity, options, config: config[pluginName], context, runtime, state, logger })
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
- return await postprocessorPlugin?.postprocess({ entity, options, config, context, plugins, runtime, state, logger })
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 != 'ERR_MODULE_NOT_FOUND') {
62
- logger.error('Redner plugin error:', resolveLocation, err)
63
- throw err
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. Searched: %s', pluginName, resolveLocations.join(', '))
67
77
  }
68
78
 
69
79
  const { renderer } = options