mikser-io 6.17.0 → 6.21.3
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/README.md +55 -15
- package/package.json +4 -2
- package/src/engine.js +69 -1
- package/src/journal.js +6 -6
- package/src/plugins/api.js +15 -19
- package/src/plugins/post/pdf.js +9 -2
- package/src/runtime.js +1 -1
- package/src/utils.js +1 -1
package/README.md
CHANGED
|
@@ -2,23 +2,43 @@
|
|
|
2
2
|
<img src="mikser-lockup-stacked.svg" alt="mikser" width="198" />
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
# Mikser
|
|
5
|
+
# Mikser
|
|
6
6
|
|
|
7
|
-
Mikser is a
|
|
7
|
+
Mikser is a content engine for Node.js built around a strict lifecycle, a composable plugin system, and direct control over every output. Every document, asset, and template flows through the same deterministic pipeline. Plugins hook in at any phase; nothing runs outside the cycle. It scales from a single markdown blog to a multi-language, multi-format publishing platform — and stays predictable in both directions.
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## Why mikser
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
11
|
+
**Your content stays yours.** Source files live on disk as `.md`, `.yml`, `.html` with YAML front-matter. The build output is plain static files. No database lock-in, no proprietary export format. The whole content tree is copyable, diffable, and version-controllable with git — your site is portable on day one and on year ten.
|
|
12
|
+
|
|
13
|
+
**Incremental builds that scale.** Mikser tracks every entity in a journal. When a file changes, only the affected entities re-process — not the whole site graph. On 10k+ documents this dramatically outpaces tools that rebuild more on every change.
|
|
14
|
+
|
|
15
|
+
**Concurrent rendering.** Renders fan out across a worker pool that keeps every CPU core hot. Multi-format outputs (HTML, PDF, MJML email, etc.) generate in parallel from the same source.
|
|
16
|
+
|
|
17
|
+
**One lifecycle, everything composes.** Plugins hook into 20+ named lifecycle phases. A search-indexing plugin shares the same journal iteration as a CMS plugin and an email-rendering plugin — no glue code, no orchestration layer.
|
|
18
|
+
|
|
19
|
+
**Run anywhere.** The same CLI handles one-shot builds, watch-mode dev loops, and a long-running HTTP server with a shared Express app. `npx mikser` ships a static site; `mikser --watch` is the dev loop; `mikser --server` exposes a live admin/API.
|
|
20
|
+
|
|
21
|
+
**Library mode.** Mikser is also a library. `useRenderer`, `useCollection`, `findSimilar`, and direct lifecycle hooks let you embed the engine inside an existing Node app instead of running it as a CLI.
|
|
22
|
+
|
|
23
|
+
**Open source.** MIT-licensed, on GitHub, no telemetry, no auth wall, no SaaS dependency. What you see is what runs.
|
|
24
|
+
|
|
25
|
+
## Plugin ecosystem
|
|
26
|
+
|
|
27
|
+
Plugins are independent npm packages — install only what a project actually uses.
|
|
28
|
+
|
|
29
|
+
| Plugin | What it does |
|
|
30
|
+
|---|---|
|
|
31
|
+
| `documents`, `files`, `resources`, `assets` | Content sources |
|
|
32
|
+
| `layouts` | Layout resolution with auto-matching |
|
|
33
|
+
| `render-hbs`, `render-eta`, `render-liquid`, `render-markdown` | Template engines |
|
|
34
|
+
| `render-resource`, `render-asset`, `render-href` | Resource / asset / link rewriting at render time |
|
|
35
|
+
| `post-pdf` | HTML → PDF via headless Chromium |
|
|
36
|
+
| `post-mjml` | MJML email markup → inbox-safe HTML |
|
|
37
|
+
| `data` | JSON snapshots of entities / context / catalog over HTTP |
|
|
38
|
+
| `api` | REST endpoints — list / get / create / update / delete / render |
|
|
39
|
+
| `vector` | OpenAI embeddings + semantic search (sqlite-vec or pgvector) |
|
|
40
|
+
| `decap` | [Decap CMS](https://decapcms.org/) mounted in the same process |
|
|
41
|
+
| `archive`, `mapper`, `live`, `aml` | Specialty integrations |
|
|
22
42
|
|
|
23
43
|
## Quick Start
|
|
24
44
|
|
|
@@ -37,7 +57,9 @@ export default {
|
|
|
37
57
|
```
|
|
38
58
|
|
|
39
59
|
```bash
|
|
40
|
-
npx mikser
|
|
60
|
+
npx mikser # one-shot build
|
|
61
|
+
npx mikser --watch # incremental dev loop
|
|
62
|
+
npx mikser --server # build + serve at :3001
|
|
41
63
|
```
|
|
42
64
|
|
|
43
65
|
## Core Concepts
|
|
@@ -47,3 +69,21 @@ npx mikser
|
|
|
47
69
|
- **Plugins** — Functionality is delivered via plugins. Built-in plugins handle common sources (documents, files, layouts, assets). Custom plugins can be added to any project.
|
|
48
70
|
- **Runtime Singleton** — A plain module-level object holds all global state and coordinates the lifecycle. The ES module cache guarantees every importer gets the same instance.
|
|
49
71
|
- **Watch Mode** — In watch mode, file changes trigger incremental re-processing without restarting.
|
|
72
|
+
|
|
73
|
+
## Documentation Index
|
|
74
|
+
|
|
75
|
+
| Document | Audience | Description |
|
|
76
|
+
| ----------------------------------------------------- | ------------------ | -------------------------------------------------- |
|
|
77
|
+
| [Getting Started](./documentation/getting-started.md) | Users | Installation, first project, basic usage |
|
|
78
|
+
| [Configuration](./documentation/configuration.md) | Users | All CLI options and config file reference |
|
|
79
|
+
| [Lifecycle](./documentation/lifecycle.md) | Users & Developers | Complete lifecycle phases and hook system |
|
|
80
|
+
| [Plugins](./documentation/plugins.md) | Users & Developers | Built-in plugins, writing custom plugins |
|
|
81
|
+
| [Entities](./documentation/entities.md) | Users & Developers | Entity model, operations, journal, catalog |
|
|
82
|
+
| [Rendering](./documentation/rendering.md) | Users & Developers | Render pipeline, render plugins, render modes |
|
|
83
|
+
| [Watch Mode](./documentation/watch-mode.md) | Users | File watching, scheduled tasks, incremental builds |
|
|
84
|
+
| [Architecture](./documentation/architecture.md) | Developers | System design, module structure, extension points |
|
|
85
|
+
| [API Reference](./documentation/api-reference.md) | Developers | Complete public API reference |
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
MIT — see [LICENSE](./LICENSE).
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mikser-io",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.21.3",
|
|
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": {
|
|
7
|
-
"debug": "node --no-warnings app.js --debug --working-folder test/fixture",
|
|
7
|
+
"debug": "node --no-warnings app.js --debug --server --watch --working-folder test/fixture",
|
|
8
8
|
"test:unit": "node --test --test-reporter=spec 'test/unit/**/*.test.js'",
|
|
9
9
|
"test:smoke": "node --no-warnings app.js --working-folder test/fixture",
|
|
10
10
|
"test": "npm run test:unit && npm run test:smoke"
|
|
@@ -56,10 +56,12 @@
|
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"fluent-ffmpeg": "^2.1.3",
|
|
59
|
+
"mikser-io-decap": "file:../mikser-io-decap",
|
|
59
60
|
"mikser-io-post-mjml": "file:../mikser-io-post-mjml",
|
|
60
61
|
"mikser-io-render-eta": "file:../mikser-io-render-eta",
|
|
61
62
|
"mikser-io-render-liquid": "file:../mikser-io-render-liquid",
|
|
62
63
|
"mikser-io-render-markdown": "file:../mikser-io-render-markdown",
|
|
64
|
+
"mikser-io-vector": "file:../mikser-io-vector",
|
|
63
65
|
"sharp": "^0.34.5"
|
|
64
66
|
},
|
|
65
67
|
"directories": {
|
package/src/engine.js
CHANGED
|
@@ -6,7 +6,7 @@ import { existsSync } from 'fs'
|
|
|
6
6
|
import _ from 'lodash'
|
|
7
7
|
import Piscina from 'piscina'
|
|
8
8
|
import runtime from './runtime.js'
|
|
9
|
-
import { onInitialize, onInitialized, onRender, onCancel, onCancelled, onFinalized, onLoaded, onAfterRender, onBeforePostprocess, onPostprocess, postprocessEntities } from './lifecycle.js'
|
|
9
|
+
import { onInitialize, onInitialized, onLoad, onRender, onCancel, onCancelled, onFinalized, onLoaded, onAfterRender, onBeforePostprocess, onPostprocess, postprocessEntities } from './lifecycle.js'
|
|
10
10
|
import { useJournal, updateEntry } from './journal.js'
|
|
11
11
|
import { globby } from 'globby'
|
|
12
12
|
import { OPERATION, TASKS } from './constants.js'
|
|
@@ -46,6 +46,7 @@ export async function setup(options) {
|
|
|
46
46
|
.option('-d --debug', 'display debug statements')
|
|
47
47
|
.option('-t --trace', 'display trace statements')
|
|
48
48
|
.option('-e --runtime-folder <folder>', 'set mikser runtime folder relative to working folder', 'runtime')
|
|
49
|
+
.option('-s --server [port]', 'start an Express server on the given port (defaults to 3001)')
|
|
49
50
|
|
|
50
51
|
Object.assign(runtime.options, options || runtime.engine.commander.parse(process.argv).opts())
|
|
51
52
|
runtime.options.info = true
|
|
@@ -84,6 +85,73 @@ export async function setup(options) {
|
|
|
84
85
|
}
|
|
85
86
|
}
|
|
86
87
|
await mkdir(runtime.options.runtimeFolder, { recursive: true })
|
|
88
|
+
|
|
89
|
+
// Server bring-up: two paths, controlled by which of these are set.
|
|
90
|
+
//
|
|
91
|
+
// runtime.options.app — pre-supplied Express app (e.g. mikser
|
|
92
|
+
// embedded inside an existing service).
|
|
93
|
+
// The caller owns the listen lifecycle
|
|
94
|
+
// and the static-route policy. Engine
|
|
95
|
+
// stays out of routing/listening so it
|
|
96
|
+
// doesn't clobber their setup; plugins
|
|
97
|
+
// still mount their own routers on it.
|
|
98
|
+
//
|
|
99
|
+
// --server [port] — engine creates the Express app, mounts
|
|
100
|
+
// (runtime.options.server) static for the output folder, and
|
|
101
|
+
// listens on the port. The actual
|
|
102
|
+
// listen() is deferred via the onLoad
|
|
103
|
+
// hook below so it runs LAST in the
|
|
104
|
+
// onLoaded phase — after every plugin
|
|
105
|
+
// has had a chance to register routes.
|
|
106
|
+
//
|
|
107
|
+
// If both are present, the externally-supplied app wins; --server
|
|
108
|
+
// becomes a no-op (the caller is in charge).
|
|
109
|
+
if (runtime.options.app) {
|
|
110
|
+
logger.info('Using externally-supplied Express app on runtime.options.app')
|
|
111
|
+
} else if (runtime.options.server) {
|
|
112
|
+
const { default: express } = await import('express').catch(() => {
|
|
113
|
+
throw new Error('Express is required for --server. Run: npm install express')
|
|
114
|
+
})
|
|
115
|
+
runtime.options.app = express()
|
|
116
|
+
runtime.options.port = runtime.options.server === true
|
|
117
|
+
? 3001
|
|
118
|
+
: Number(runtime.options.server) || 3001
|
|
119
|
+
logger.info('Server starting on port %d', runtime.options.port)
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
// Registered here (inside setup) so it runs AFTER plugins.js's onLoad
|
|
124
|
+
// (which is registered at module-import time). That ordering matters
|
|
125
|
+
// because plugins.js loads user plugins during its onLoad — each plugin
|
|
126
|
+
// factory may register onLoaded handlers that mount routes on
|
|
127
|
+
// runtime.options.app. We want our listen() to run LAST in the
|
|
128
|
+
// onLoaded phase, which is why we register it from inside another
|
|
129
|
+
// onLoad — by then plugins have already appended their handlers.
|
|
130
|
+
onLoad(() => {
|
|
131
|
+
// Only auto-mount static + auto-listen when the engine owns the
|
|
132
|
+
// app (created via --server). When an external app was supplied,
|
|
133
|
+
// `port` is unset and we stay out of the way — the caller manages
|
|
134
|
+
// both routing decisions and the listen lifecycle themselves.
|
|
135
|
+
if (!runtime.options.app || runtime.options.port == null) return
|
|
136
|
+
onLoaded(async () => {
|
|
137
|
+
const logger = useLogger()
|
|
138
|
+
const { default: express } = await import('express')
|
|
139
|
+
|
|
140
|
+
// Serve the output folder as the catch-all static route.
|
|
141
|
+
// Mounted LAST in the middleware chain so plugin routes
|
|
142
|
+
// (e.g. /api/*) match first; anything that didn't match a
|
|
143
|
+
// plugin's router falls through to the static handler and
|
|
144
|
+
// gets served from <outputFolder>.
|
|
145
|
+
runtime.options.app.use(express.static(runtime.options.outputFolder))
|
|
146
|
+
logger.info('Serving %s as /', runtime.options.outputFolder.replace(runtime.options.workingFolder + '/', ''))
|
|
147
|
+
|
|
148
|
+
await new Promise(resolve => {
|
|
149
|
+
runtime.options.app.listen(runtime.options.port, () => {
|
|
150
|
+
logger.info('Server listening on port %d', runtime.options.port)
|
|
151
|
+
resolve()
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
})
|
|
87
155
|
})
|
|
88
156
|
|
|
89
157
|
onLoaded(async () => {
|
package/src/journal.js
CHANGED
|
@@ -70,12 +70,12 @@ export async function clearJournal(aborted) {
|
|
|
70
70
|
await journal('operations').del()
|
|
71
71
|
if (!aborted) {
|
|
72
72
|
// Tear down the sqlite connection only when this is genuinely a
|
|
73
|
-
// one-shot run that's about to exit.
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
if (runtime.options.watch !== true && runtime.options.
|
|
73
|
+
// one-shot run that's about to exit. The two ways mikser stays
|
|
74
|
+
// alive across cycles are watch mode (chokidar handles keep the
|
|
75
|
+
// event loop ref'd) and an HTTP server (`runtime.options.app`,
|
|
76
|
+
// set either by --server or by a caller passing setup({ app }));
|
|
77
|
+
// both need the journal to survive subsequent cycles.
|
|
78
|
+
if (runtime.options.watch !== true && !runtime.options.app) {
|
|
79
79
|
journal.destroy()
|
|
80
80
|
}
|
|
81
81
|
}
|
package/src/plugins/api.js
CHANGED
|
@@ -73,13 +73,23 @@ export default ({
|
|
|
73
73
|
onLoaded(async () => {
|
|
74
74
|
const logger = useLogger()
|
|
75
75
|
|
|
76
|
+
// The api plugin no longer creates its own Express app. It mounts
|
|
77
|
+
// onto an existing app provided either by --server (engine
|
|
78
|
+
// creates one) or by a caller that programmatically passed
|
|
79
|
+
// setup({ app: ... }). Without an app there's nowhere to mount
|
|
80
|
+
// routes, so fail fast with a message that points at the fix.
|
|
81
|
+
const app = runtime.options.app
|
|
82
|
+
if (!app) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
'API plugin requires runtime.options.app — run mikser with --server, ' +
|
|
85
|
+
'or pass { app: yourExpressInstance } to setup() before loading the api plugin'
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
76
89
|
const { default: express } = await import('express').catch(() => {
|
|
77
|
-
throw new Error('
|
|
90
|
+
throw new Error('Express is required for the api plugin — run: npm install express')
|
|
78
91
|
})
|
|
79
92
|
|
|
80
|
-
const ownApp = !runtime.options.app
|
|
81
|
-
const app = runtime.options.app ?? express()
|
|
82
|
-
|
|
83
93
|
const router = express.Router()
|
|
84
94
|
router.use(express.json())
|
|
85
95
|
|
|
@@ -167,22 +177,8 @@ export default ({
|
|
|
167
177
|
}
|
|
168
178
|
})
|
|
169
179
|
|
|
170
|
-
// The HTTP server keeps the process alive across many process()
|
|
171
|
-
// cycles. Tell the journal layer not to tear down the sqlite
|
|
172
|
-
// connection at the end of each cycle.
|
|
173
|
-
runtime.options.persistent = true
|
|
174
|
-
|
|
175
180
|
const base = runtime.config.api?.base ?? '/api'
|
|
176
181
|
app.use(base, router)
|
|
177
|
-
|
|
178
|
-
if (ownApp) {
|
|
179
|
-
const port = runtime.config.api?.port ?? 3001
|
|
180
|
-
const url = runtime.config.api?.url ?? 'http://localhost'
|
|
181
|
-
app.listen(port, () => {
|
|
182
|
-
logger.info('Api listening: %s:%d%s', url, port, base)
|
|
183
|
-
})
|
|
184
|
-
} else {
|
|
185
|
-
logger.info('Api mounted: %s', base)
|
|
186
|
-
}
|
|
182
|
+
logger.info('Api mounted: %s', base)
|
|
187
183
|
})
|
|
188
184
|
}
|
package/src/plugins/post/pdf.js
CHANGED
|
@@ -14,11 +14,18 @@ export async function setup({ config, logger }) {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
const { default: puppeteer } = await import('puppeteer').catch(() => {
|
|
17
|
-
throw new Error('
|
|
17
|
+
throw new Error('Puppeteer is required for the pdf postprocessor — run: npm install puppeteer')
|
|
18
18
|
})
|
|
19
|
+
// --no-sandbox / --disable-setuid-sandbox are required on most
|
|
20
|
+
// headless Linux servers and Docker images where Chrome's sandbox
|
|
21
|
+
// can't be set up. Applied by default and merged (deduped) with any
|
|
22
|
+
// user-supplied launch.args so callers can add flags without losing
|
|
23
|
+
// these. To run WITH the sandbox, override launch.args explicitly.
|
|
24
|
+
const defaultArgs = ['--no-sandbox', '--disable-setuid-sandbox']
|
|
19
25
|
browser = await puppeteer.launch({
|
|
20
26
|
headless: true,
|
|
21
|
-
...config?.launch
|
|
27
|
+
...config?.launch,
|
|
28
|
+
args: [...new Set([...defaultArgs, ...(config?.launch?.args ?? [])])],
|
|
22
29
|
})
|
|
23
30
|
logger.debug('Puppeteer browser launched')
|
|
24
31
|
}
|
package/src/runtime.js
CHANGED
package/src/utils.js
CHANGED
|
@@ -55,7 +55,7 @@ export function matchEntity(entity, match) {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
else if (typeof match == 'object') return _.isMatch(entity, match)
|
|
58
|
-
throw new Error('
|
|
58
|
+
throw new Error('Invalid match type')
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
export function changeExtension(file, format) {
|