mikser-io 9.0.3 → 9.0.5

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.
Files changed (3) hide show
  1. package/package.json +83 -83
  2. package/src/render.js +37 -1
  3. package/src/server.js +47 -25
package/package.json CHANGED
@@ -1,85 +1,85 @@
1
1
  {
2
- "name": "mikser-io",
3
- "version": "9.0.3",
4
- "description": "<p align=\"center\"> <img src=\"mikser-lockup-stacked.svg\" alt=\"mikser\" width=\"198\" /> </p>",
5
- "main": "index.js",
6
- "exports": {
7
- ".": "./index.js",
8
- "./package.json": "./package.json",
9
- "./*": "./*"
10
- },
11
- "scripts": {
12
- "debug": "node --no-warnings app.js --server --watch --working-folder test/fixture",
13
- "test:unit": "node --test --test-reporter=spec 'test/unit/**/*.test.js'",
14
- "test:smoke": "node --no-warnings app.js --working-folder test/fixture",
15
- "test:scenarios": "node --test --test-reporter=spec --test-timeout=60000 'test/scenarios/**/*.test.js'",
16
- "test:perf": "node test/perf/generate.js && node --no-warnings app.js --working-folder test/perf",
17
- "test": "npm run test:unit && npm run test:scenarios && npm run test:smoke"
18
- },
19
- "bin": {
20
- "mikser": "app.js"
21
- },
22
- "repository": {
23
- "type": "git",
24
- "url": "git+https://github.com/almero-digital-marketing/mikser-io.git"
25
- },
26
- "type": "module",
27
- "author": "",
28
- "license": "ISC",
29
- "dependencies": {
30
- "@budibase/handlebars-helpers": "^0.14.3",
31
- "await-semaphore": "^0.1.3",
32
- "axios": "^1.17.0",
33
- "better-sqlite3": "^12.10.0",
34
- "chokidar": "^5.0.0",
35
- "commander": "^15.0.0",
36
- "cors": "^2.8.5",
37
- "dayjs": "^1.11.21",
38
- "deepdash": "^5.3.9",
39
- "escape-string-regexp": "^5.0.0",
40
- "execa": "^9.6.1",
41
- "front-matter": "^4.0.2",
42
- "gauge": "^5.0.2",
43
- "globby": "^16.2.0",
44
- "handlebars": "^4.7.9",
45
- "hasha": "^7.0.0",
46
- "is-url": "^1.2.4",
47
- "line-reader": "^0.4.0",
48
- "lodash": "^4.18.1",
49
- "minimatch": "^10.2.5",
50
- "node-cron": "^4.2.1",
51
- "p-map": "^7.0.4",
52
- "p-queue": "^9.3.0",
53
- "pino": "^10.3.1",
54
- "pino-pretty": "^13.1.3",
55
- "piscina": "^5.1.4",
56
- "sift": "^17.1.3",
57
- "truncate-stream": "^1.0.2",
58
- "yaml": "^2.9.0"
59
- },
60
- "optionalDependencies": {
61
- "express": "^5.2.1"
62
- },
63
- "devDependencies": {
64
- "@ai-sdk/openai": "^3.0.71",
65
- "fluent-ffmpeg": "^2.1.3",
66
- "mikser-io-decap": "file:../mikser-io-decap",
67
- "mikser-io-post-mjml": "file:../mikser-io-post-mjml",
68
- "mikser-io-post-pdf": "file:../mikser-io-post-pdf",
69
- "mikser-io-render-eta": "file:../mikser-io-render-eta",
70
- "mikser-io-render-liquid": "file:../mikser-io-render-liquid",
71
- "mikser-io-render-markdown": "file:../mikser-io-render-markdown",
72
- "mikser-io-schemas": "file:../mikser-io-schemas",
73
- "mikser-io-vector": "file:../mikser-io-vector",
74
- "puppeteer": "^25.1.0",
75
- "sharp": "^0.34.5",
76
- "zod": "^4.4.3"
77
- },
78
- "directories": {
79
- "test": "test"
80
- },
81
- "bugs": {
82
- "url": "https://github.com/almero-digital-marketing/mikser-io/issues"
83
- },
84
- "homepage": "https://github.com/almero-digital-marketing/mikser-io#readme"
2
+ "name": "mikser-io",
3
+ "version": "9.0.5",
4
+ "description": "<p align=\"center\"> <img src=\"mikser-lockup-stacked.svg\" alt=\"mikser\" width=\"198\" /> </p>",
5
+ "main": "index.js",
6
+ "exports": {
7
+ ".": "./index.js",
8
+ "./package.json": "./package.json",
9
+ "./*": "./*"
10
+ },
11
+ "scripts": {
12
+ "debug": "node --no-warnings app.js --server --watch --working-folder test/fixture",
13
+ "test:unit": "node --test --test-reporter=spec 'test/unit/**/*.test.js'",
14
+ "test:smoke": "node --no-warnings app.js --working-folder test/fixture",
15
+ "test:scenarios": "node --test --test-reporter=spec --test-timeout=60000 'test/scenarios/**/*.test.js'",
16
+ "test:perf": "node test/perf/generate.js && node --no-warnings app.js --working-folder test/perf",
17
+ "test": "npm run test:unit && npm run test:scenarios && npm run test:smoke"
18
+ },
19
+ "bin": {
20
+ "mikser": "app.js"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/almero-digital-marketing/mikser-io.git"
25
+ },
26
+ "type": "module",
27
+ "author": "",
28
+ "license": "ISC",
29
+ "dependencies": {
30
+ "@budibase/handlebars-helpers": "^0.14.3",
31
+ "await-semaphore": "^0.1.3",
32
+ "axios": "^1.17.0",
33
+ "better-sqlite3": "^12.10.0",
34
+ "chokidar": "^5.0.0",
35
+ "commander": "^15.0.0",
36
+ "cors": "^2.8.5",
37
+ "dayjs": "^1.11.21",
38
+ "deepdash": "^5.3.9",
39
+ "escape-string-regexp": "^5.0.0",
40
+ "execa": "^9.6.1",
41
+ "front-matter": "^4.0.2",
42
+ "gauge": "^5.0.2",
43
+ "globby": "^16.2.0",
44
+ "handlebars": "^4.7.9",
45
+ "hasha": "^7.0.0",
46
+ "is-url": "^1.2.4",
47
+ "line-reader": "^0.4.0",
48
+ "lodash": "^4.18.1",
49
+ "minimatch": "^10.2.5",
50
+ "node-cron": "^4.2.1",
51
+ "p-map": "^7.0.4",
52
+ "p-queue": "^9.3.0",
53
+ "pino": "^10.3.1",
54
+ "pino-pretty": "^13.1.3",
55
+ "piscina": "^5.1.4",
56
+ "sift": "^17.1.3",
57
+ "truncate-stream": "^1.0.2",
58
+ "yaml": "^2.9.0"
59
+ },
60
+ "optionalDependencies": {
61
+ "express": "^5.2.1"
62
+ },
63
+ "devDependencies": {
64
+ "@ai-sdk/openai": "^3.0.71",
65
+ "fluent-ffmpeg": "^2.1.3",
66
+ "mikser-io-decap": "file:../mikser-io-decap",
67
+ "mikser-io-post-mjml": "file:../mikser-io-post-mjml",
68
+ "mikser-io-post-pdf": "file:../mikser-io-post-pdf",
69
+ "mikser-io-render-eta": "file:../mikser-io-render-eta",
70
+ "mikser-io-render-liquid": "file:../mikser-io-render-liquid",
71
+ "mikser-io-render-markdown": "file:../mikser-io-render-markdown",
72
+ "mikser-io-schemas": "file:../mikser-io-schemas",
73
+ "mikser-io-vector": "file:../mikser-io-vector",
74
+ "puppeteer": "^25.1.0",
75
+ "sharp": "^0.34.5",
76
+ "zod": "^4.4.3"
77
+ },
78
+ "directories": {
79
+ "test": "test"
80
+ },
81
+ "bugs": {
82
+ "url": "https://github.com/almero-digital-marketing/mikser-io/issues"
83
+ },
84
+ "homepage": "https://github.com/almero-digital-marketing/mikser-io#readme"
85
85
  }
package/src/render.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { readFileSync, existsSync } from 'node:fs'
2
+ import { readFile, unlink } from 'node:fs/promises'
2
3
  import { createRequire } from 'node:module'
3
4
  import { randomUUID } from 'node:crypto'
4
5
  import path from 'node:path'
@@ -290,7 +291,42 @@ export function useRenderer(runtime, { defaultTimeout = 30_000 } = {}) {
290
291
  if (!isFinal) return
291
292
  remaining.delete(cid)
292
293
  clearTimeout(item.timer)
293
- item.resolve({ output: entry.output, entity: entry.entity })
294
+
295
+ // The postprocess chain writes the final bytes straight to
296
+ // disk and its dispatcher (src/postprocess.js) returns
297
+ // undefined, so a postprocessed completion arrives with
298
+ // output.result empty — even though the bytes exist on disk.
299
+ // Our contract (see the render() JSDoc below) is to hand back
300
+ // the FINAL pipeline output, so read it from where the chain
301
+ // wrote it: <destination> under outputFolder, or under
302
+ // previewFolder when the caller passed save:false (the engine
303
+ // swaps the postprocess outputFolder to previewFolder in that
304
+ // case — see engine.js onBeforePostprocess). Render-only
305
+ // entities already carry their bytes in output.result and
306
+ // skip this entirely.
307
+ let output = entry.output
308
+ if (hasPostprocessor && output?.result == null && entry.entity?.destination) {
309
+ const saveOff = entry.entity.options?.save === false
310
+ const folder = saveOff
311
+ ? runtime.options.previewFolder
312
+ : runtime.options.outputFolder
313
+ const file = path.join(folder, entry.entity.destination.replace(/^\/+/, ''))
314
+ try {
315
+ const bytes = await readFile(file)
316
+ output = { success: true, result: bytes }
317
+ // save:false → that previewFolder file is engine
318
+ // scratch the caller explicitly opted out of
319
+ // persisting. Hand back the bytes, leave nothing on
320
+ // disk.
321
+ if (saveOff) await unlink(file).catch(() => {})
322
+ } catch (err) {
323
+ useLogger()?.warn?.(
324
+ 'useRenderer: postprocessed output for %s not found at %s — returning empty result: %s',
325
+ entry.entity.id, file, err.message,
326
+ )
327
+ }
328
+ }
329
+ item.resolve({ output, entity: entry.entity })
294
330
  }
295
331
  completedHooks.push(hook)
296
332
 
package/src/server.js CHANGED
@@ -39,6 +39,10 @@ export function attachServerCliOptions(commander) {
39
39
  // Wire the server lifecycle hooks. Called by engine.js's setup() AFTER
40
40
  // engine's own onInitialized/onLoad registrations so the log-line order
41
41
  // stays "engine folder logs → server bring-up" rather than the reverse.
42
+ // True when this module created the Express app. The late hook applies config-derived
43
+ // settings only then — a caller-supplied app owns its own configuration.
44
+ let ownsApp = false
45
+
42
46
  export function setupServer() {
43
47
  onInitialized(async () => {
44
48
  const logger = useLogger()
@@ -53,6 +57,7 @@ export function setupServer() {
53
57
  throw new Error('Express is required for --server. Run: npm install express')
54
58
  })
55
59
  runtime.options.app = express()
60
+ ownsApp = true
56
61
  runtime.options.port = runtime.options.server === true
57
62
  ? 3001
58
63
  : Number(runtime.options.server) || 3001
@@ -93,9 +98,8 @@ export function setupServer() {
93
98
  // DIFFERENT host fails closed (non-loopback peer → XFF ignored →
94
99
  // loopback endpoints 403) until the operator sets 'uniquelocal'
95
100
  // or the specific subnet.
96
- const trustProxy = runtime.config.server?.trustProxy ?? 'loopback'
97
- runtime.options.app.set('trust proxy', trustProxy)
98
- logger.info('Server trust proxy: %s', String(trustProxy))
101
+ // `trust proxy` is applied in the late hook at the bottom of this file: runtime.config
102
+ // is not populated during the initialized phase.
99
103
 
100
104
  // CORS — a server exists to be fetched, and in dev the frontend
101
105
  // is almost always on a different origin (a dev server on
@@ -110,28 +114,30 @@ export function setupServer() {
110
114
  // Default on (?? true) so programmatic setup({ server }) — which
111
115
  // bypasses commander's --no-cors default — matches the CLI.
112
116
  // Explicit false (config or --no-cors) still disables.
113
- const corsOrigin = runtime.config.server?.cors ?? runtime.options.cors ?? true
114
- if (corsOrigin) {
115
- const origin = corsOrigin === true ? '*' : String(corsOrigin)
116
- // Extensible header arrays. Plugins push values into these
117
- // at factory time to teach CORS about headers they care
118
- // about e.g. the mikser-io-mcp plugin adds mcp-session-id,
119
- // mcp-protocol-version, last-event-id so browser-side MCP
120
- // clients can complete the Streamable HTTP handshake.
121
- // Default to the minimum every server needs.
122
- runtime.options.corsAllowHeaders = ['Content-Type', 'Authorization']
123
- runtime.options.corsExposeHeaders = []
124
- const { default: cors } = await import('cors')
125
- runtime.options.app.use(cors((req, callback) => {
126
- callback(null, {
127
- origin,
128
- methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
129
- allowedHeaders: runtime.options.corsAllowHeaders,
130
- exposedHeaders: runtime.options.corsExposeHeaders,
131
- })
132
- }))
133
- logger.debug('CORS enabled: %s', origin)
134
- }
117
+ // Registered unconditionally, decided per request. The middleware's position is fixed
118
+ // here — ahead of static and every plugin router — while its setting lives in
119
+ // runtime.config, which is only populated in the load phase. The per-request callback
120
+ // is what lets both hold.
121
+ //
122
+ // Extensible header arrays. Plugins push values into these at factory time to teach
123
+ // CORS about headers they care about — e.g. the mikser-io-mcp plugin adds
124
+ // mcp-session-id, mcp-protocol-version, last-event-id so browser-side MCP clients can
125
+ // complete the Streamable HTTP handshake. Default to the minimum every server needs.
126
+ runtime.options.corsAllowHeaders = ['Content-Type', 'Authorization']
127
+ runtime.options.corsExposeHeaders = []
128
+ const { default: cors } = await import('cors')
129
+ runtime.options.app.use(cors((req, callback) => {
130
+ const configured = runtime.config.server?.cors ?? runtime.options.cors ?? true
131
+ // `origin: false` is how the cors package emits no CORS headers at all, which is
132
+ // what --no-cors / config.server.cors:false asks for.
133
+ if (!configured) return callback(null, { origin: false })
134
+ callback(null, {
135
+ origin: configured === true ? '*' : String(configured),
136
+ methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
137
+ allowedHeaders: runtime.options.corsAllowHeaders,
138
+ exposedHeaders: runtime.options.corsExposeHeaders,
139
+ })
140
+ }))
135
141
  })
136
142
 
137
143
  // Late-binding static + listen. Registered here (inside another
@@ -150,6 +156,22 @@ export function setupServer() {
150
156
  const logger = useLogger()
151
157
  const { default: express } = await import('express')
152
158
 
159
+ // Config-derived settings belong here rather than in the onInitialized block that
160
+ // creates the app. Phases run initialize -> initialized -> load -> loaded, and
161
+ // config.js populates runtime.config from a `load` hook — so anything read during
162
+ // `initialized` sees an empty object and takes its default. `loaded` runs after
163
+ // every `load`, so runtime.config is complete here whatever the import order.
164
+ //
165
+ // Express evaluates `trust proxy` per request, so setting it before listen is
166
+ // equivalent to setting it at creation.
167
+ if (ownsApp) {
168
+ const trustProxy = runtime.config.server?.trustProxy ?? 'loopback'
169
+ runtime.options.app.set('trust proxy', trustProxy)
170
+ logger.info('Server trust proxy: %s', String(trustProxy))
171
+ const corsOrigin = runtime.config.server?.cors ?? runtime.options.cors ?? true
172
+ logger.debug('CORS: %s', corsOrigin === false ? 'disabled' : (corsOrigin === true ? '*' : String(corsOrigin)))
173
+ }
174
+
153
175
  runtime.options.app.use(express.static(runtime.options.outputFolder))
154
176
 
155
177
  await new Promise(resolve => {