mikser-io 8.3.9 → 9.0.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/10.0-PLAN.md +1 -1
- package/CLAUDE.md +266 -38
- package/README.md +27 -2
- package/index.js +32 -1
- package/package.json +7 -1
- package/src/config.js +20 -29
- package/src/engine.js +91 -40
- package/src/lifecycle.js +14 -10
- package/src/logger.js +103 -16
- package/src/plugins/api.js +24 -19
- package/src/plugins/assets.js +37 -35
- package/src/plugins/commands.js +61 -59
- package/src/plugins/data.js +21 -19
- package/src/plugins/documents.js +13 -12
- package/src/plugins/files.js +145 -143
- package/src/plugins/front-matter.js +20 -18
- package/src/plugins/json.js +18 -16
- package/src/plugins/mapper.js +22 -23
- package/src/plugins/observer.js +26 -24
- package/src/plugins/preview.js +15 -14
- package/src/plugins/providers/http.js +179 -0
- package/src/plugins/render/asset.js +4 -0
- package/src/plugins/render/file.js +4 -0
- package/src/plugins/render/hbs.js +125 -0
- package/src/plugins/render/href.js +4 -0
- package/src/plugins/render/preset.js +4 -0
- package/src/plugins/render/resource.js +4 -0
- package/src/plugins/resources.js +28 -26
- package/src/plugins/shares.js +34 -32
- package/src/plugins/validator.js +18 -19
- package/src/plugins/yaml.js +21 -19
- package/src/plugins.js +92 -53
- package/src/postprocess.js +189 -20
- package/src/render.js +29 -3
- package/src/runtime.js +9 -1
- package/src/server.js +7 -1
- package/src/source.js +44 -12
- package/src/utils.js +116 -31
- package/9.0-PLAN.md +0 -438
- package/src/plugins/layouts.js +0 -803
package/10.0-PLAN.md
CHANGED
|
@@ -37,7 +37,7 @@ When the working signals dominate over a sustained period of real use, v10 desig
|
|
|
37
37
|
|
|
38
38
|
## Why v10 (not 9.x)
|
|
39
39
|
|
|
40
|
-
The 9.0
|
|
40
|
+
The 9.0 design deliberately deferred mikser-to-mikser composition to v10. The reasoning:
|
|
41
41
|
|
|
42
42
|
> Cross-instance concerns (discovery, peer identity, routing, cross-mikser refs, watch propagation, liveness, partition behavior, peer auth) belong to a distributed-systems substrate that iterates at "we hit a production partition bug" speed, not at engine-substrate speed. Folding any of it into `mikser-io` would re-fail ADR-0006 test #5 (release cadence) the moment it ships.
|
|
43
43
|
|
package/CLAUDE.md
CHANGED
|
@@ -10,22 +10,51 @@ paths. No migration markdown. No `task: pool` legacy aliasing for
|
|
|
10
10
|
`task: inline`. Update READMEs and ADRs in place as source-of-truth
|
|
11
11
|
changes; rewrite, don't supersede. The freedom is the point.
|
|
12
12
|
|
|
13
|
-
**v9
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
13
|
+
**v9 shipped. The gate now is daily-use payoff, not completion.**
|
|
14
|
+
The engine, every sibling plugin, the agency template — all live.
|
|
15
|
+
The next step isn't "finish v9," it's **use v9 in real daily work
|
|
16
|
+
until the workflow earns the v10 layer**. The v10 architecture
|
|
17
|
+
(party-mikser-io) is more fun to design than v9 dogfooding is to
|
|
18
|
+
sustain — the gate exists because that asymmetry kills projects.
|
|
19
|
+
Concrete pass/fail signals in `10.0-PLAN.md` under "The gate." If
|
|
20
|
+
a session brings up v10 design work, check the gate first; if v9
|
|
21
|
+
isn't paying out yet, the question to answer is "what about v9
|
|
22
|
+
isn't working in daily use?" — not "what should v10 look like?"
|
|
23
|
+
|
|
24
|
+
**mikser is a mixer.** The name is the architecture. It combines
|
|
25
|
+
inputs (entities — from files, forms, queries, anywhere) through
|
|
26
|
+
configurable rendering pipelines (renderer plugins + task-production
|
|
27
|
+
policies) into outputs of any kind. Static-site generation is the
|
|
28
|
+
canonical recipe — what mikser ships with and what most projects
|
|
29
|
+
use today — but it's a recipe, not the definition. The same
|
|
30
|
+
substrate runs:
|
|
31
|
+
|
|
32
|
+
- HTML SSG via `layouts` + `renderHbs` (the recipe everything else
|
|
33
|
+
is currently shaped around)
|
|
34
|
+
- PDFs via `renderHbs` + `post-pdf` (puppeteer-driven)
|
|
35
|
+
- Emails via `renderHbs` → `post-mjml` → `post-email` (chain)
|
|
36
|
+
- 3D renders by composing a hypothetical `renderBlender` against
|
|
37
|
+
`.blend` entities — same engine, different renderer + recipe
|
|
38
|
+
- Video / audio / archives / data-export — same shape
|
|
39
|
+
|
|
40
|
+
The engine has no opinion on what a renderer produces. `render.js`
|
|
41
|
+
and `postprocess.js` are abstract worker entries; `manifest.js`
|
|
42
|
+
tracks snapshots of *any* output; refs invalidation re-runs
|
|
43
|
+
*any* render task. SSG is the dominant case in v9 because it's
|
|
44
|
+
where the dogfooding lives, not because the architecture only
|
|
45
|
+
fits there. When proposing changes, hold the broader view: a
|
|
46
|
+
change that helps SSG but blocks the Blender/video/audio/archive
|
|
47
|
+
cases is constraining the engine's actual reach.
|
|
48
|
+
|
|
49
|
+
**Position mikser by what it is, not by speed.** Hugo wins the
|
|
50
|
+
speed race for SSG specifically; mikser doesn't compete there
|
|
51
|
+
(and "fastest mixer" isn't a useful claim because the bottleneck
|
|
52
|
+
is the renderer, not the engine). Secondary attributes that
|
|
53
|
+
follow from the mixer framing: AI-native, lifecycle-observable,
|
|
54
|
+
files-as-source-of-truth, full-cycle introspection. Speed claim
|
|
55
|
+
cap: "fast enough that watch-mode rebuilds feel instant." Honest
|
|
56
|
+
range: 200–800 docs/sec on the SSG-flavor pipeline depending on
|
|
57
|
+
corpus size (see `test/perf/`).
|
|
29
58
|
|
|
30
59
|
**Direct critique preferred.** Skip "great question," skip "solid but,"
|
|
31
60
|
skip softening. When something didn't pay off, say so. Match user
|
|
@@ -81,7 +110,10 @@ brevity.
|
|
|
81
110
|
(`minThreads: 0` + `idleTimeout: 30_000`) so INLINE-only workloads
|
|
82
111
|
pay no worker overhead. `workerSafeOptions(runtime.options)`
|
|
83
112
|
strips plugin-surface functions before TASKS.WORKER dispatch so
|
|
84
|
-
Piscina's structured clone doesn't choke.
|
|
113
|
+
Piscina's structured clone doesn't choke. Renderer / postprocessor
|
|
114
|
+
descriptors in `runtime.options.plugins` are projected to their
|
|
115
|
+
`render-${name}` / `post-${name}` identifier so workers can
|
|
116
|
+
resolve them via dynamic import.
|
|
85
117
|
- `database/` — `createSqliteDatabase()`, `registerSchema()`,
|
|
86
118
|
`useDatabase()` (the `mikser_meta` table stamps schema_version).
|
|
87
119
|
`sift-to-sql.js` translates sift filters to SQL WHERE clauses
|
|
@@ -97,7 +129,16 @@ brevity.
|
|
|
97
129
|
plugins to push onto), late-binding static mount + listen.
|
|
98
130
|
- `logger.js` — pino + pino-pretty (inline) + gauge progress + custom
|
|
99
131
|
Writable for progress coordination + `pino.multistream` for
|
|
100
|
-
third-party shipping
|
|
132
|
+
third-party shipping. Two registration surfaces for transports:
|
|
133
|
+
- `runtime.config.logging.transports` — declarative; user config.
|
|
134
|
+
- `addLogTransport({ target, options?, level? })` — public export;
|
|
135
|
+
plugins call this from their factory (queues until logger build)
|
|
136
|
+
or any later hook (live-rebuilds the multistream + swaps
|
|
137
|
+
`runtime.engine.logger`). `useLogger()` reads the swap-target
|
|
138
|
+
fresh on each call so new transports start receiving records
|
|
139
|
+
immediately. Both surfaces compose; same `{level, target, options}`
|
|
140
|
+
shape. Enables Better Stack / Datadog / Loki / Axiom / Sentry as
|
|
141
|
+
standard sibling plugins without engine changes per vendor.
|
|
101
142
|
- `utils.js` — shared pure helpers: `mimeForEntity`, `isLoopback`,
|
|
102
143
|
`expandEntity`, `projectMeta`, `useCollection`, `useRenderer` (via
|
|
103
144
|
render.js), `isTextEntity`, `readEntityContent`, `extractRefs`,
|
|
@@ -111,21 +152,62 @@ brevity.
|
|
|
111
152
|
the engine's logger. Each worker opens its own read-only sqlite
|
|
112
153
|
handle on first task (`ensureWorkerDb` in render.js) so template
|
|
113
154
|
helpers like `runtime.lookupHref` stay sync. Never touch the journal
|
|
114
|
-
directly.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
155
|
+
directly. Plugin loading: main-thread INLINE dispatch reads
|
|
156
|
+
`runtime.renderers` / `runtime.postprocessors` first (populated by
|
|
157
|
+
plugins.js from descriptor returns); workers see empty registries
|
|
158
|
+
in their separate-process runtime and fall through to dynamic-import
|
|
159
|
+
by `mikser-io-${name}` package name. Per-plugin options flow through
|
|
160
|
+
`descriptor.options` and arrive as the `config` arg to
|
|
161
|
+
`load`/`render`/`setup`/`postprocess`/`teardown`.
|
|
162
|
+
- `config.js` — loads `mikser.config.js` at `onLoad` into
|
|
163
|
+
`runtime.config`. v9 holds only engine-level keys (`server`,
|
|
164
|
+
`logging`, `catalog` if tuned) plus the `plugins` array — all
|
|
165
|
+
plugin options moved to the factory call site (ADR-0010).
|
|
166
|
+
- `plugins.js` — dispatches v9 plugin entries at `onLoad`. Each
|
|
167
|
+
entry in `plugins: []` is a factory call return; the dispatcher
|
|
168
|
+
duck-types on shape:
|
|
169
|
+
- function → lifecycle plugin; called with `core` so it can
|
|
170
|
+
register hooks.
|
|
171
|
+
- `{ name, options, load?, render? }` → renderer descriptor;
|
|
172
|
+
stored in `runtime.renderers`.
|
|
173
|
+
- `{ name, options, postprocess, output?, setup?, teardown? }` →
|
|
174
|
+
postprocessor descriptor; stored in `runtime.postprocessors`.
|
|
175
|
+
Strings produce a v9 migration error pointing at the new shape.
|
|
118
176
|
- `manager.js` — file watching (chokidar) and cron scheduling.
|
|
119
177
|
- `source.js` — `useSource` codifies the folder-of-files pattern.
|
|
120
178
|
- `constants.js` — `OPERATION` (CREATE/UPDATE/DELETE/RENDER/
|
|
121
179
|
POSTPROCESS), `ACTION` (sync action types), `TASKS` (`INLINE`/
|
|
122
180
|
`SERIAL`/`WORKER` — dispatch modes).
|
|
123
181
|
|
|
182
|
+
## Canonical sibling plugins
|
|
183
|
+
|
|
184
|
+
These ship as separate repos but most projects pull them in. They're
|
|
185
|
+
not bundled because the engine is renderer-agnostic — each plugin
|
|
186
|
+
encodes a particular recipe (SSG, MJML, PDF, email) rather than
|
|
187
|
+
substrate.
|
|
188
|
+
|
|
189
|
+
- **`mikser-io-layouts`** — the canonical SSG-flavor task-production
|
|
190
|
+
policy. Owns multi-match layout assignment (`meta.layout` /
|
|
191
|
+
`meta.layouts` dual key + `autoLayouts` peel ladder), per-layout
|
|
192
|
+
destination Handlebars templates, collision detection, postprocess
|
|
193
|
+
chain parsing (filename `name.html-mjml-email.hbs` → `postprocessors:
|
|
194
|
+
['mjml','email']` + frontmatter `postprocessor`/`postprocessors`),
|
|
195
|
+
`inspect()` primitive at `runtime.options.layouts.inspect`. The
|
|
196
|
+
engine has no opinion on layouts; this plugin is one way of producing
|
|
197
|
+
render tasks. Other domains (3D rendering, video) would have their
|
|
198
|
+
own task-production plugins.
|
|
199
|
+
- **`mikser-io-render-{hbs,liquid,eta,markdown,metatext}`** — renderer
|
|
200
|
+
plugins. `renderHbs` is bundled in core; the rest are siblings.
|
|
201
|
+
- **`mikser-io-post-{mjml,pdf,email}`** — postprocessor plugins,
|
|
202
|
+
composable in chains (see `documentation/rendering.md#postprocess`).
|
|
203
|
+
- **`mikser-io-mcp`** — AI tooling surface (ADR-0006 test #5).
|
|
204
|
+
- **`mikser-io-vector`** — vector index over the catalog.
|
|
205
|
+
- **`mikser-io-forms`** — HTTP form receivers → entities.
|
|
206
|
+
- **`mikser-io-schemas`** — TypeScript type generation from entity meta.
|
|
207
|
+
|
|
124
208
|
## Plugin map (`src/plugins/`)
|
|
125
209
|
|
|
126
210
|
- `documents` — file→entity sync for the documents collection
|
|
127
|
-
- `layouts` — layout matching + sitemap + `inspect()` primitive
|
|
128
|
-
(exposed at `runtime.options.layouts.inspect`)
|
|
129
211
|
- `files` — file→entity sync for the files collection
|
|
130
212
|
- `assets` — asset references and copy
|
|
131
213
|
- `resources` — resource references
|
|
@@ -140,16 +222,83 @@ brevity.
|
|
|
140
222
|
- `observer` / `mapper` / `validator` / `commands` / `shares` —
|
|
141
223
|
utility plugins
|
|
142
224
|
|
|
225
|
+
## Built-in content providers (`src/plugins/providers/`)
|
|
226
|
+
|
|
227
|
+
The substrate-side `readEntityContent(entity)` dispatches by URI
|
|
228
|
+
scheme. Two schemes are handled inline by built-in providers
|
|
229
|
+
(no separate package required, ships with mikser-io):
|
|
230
|
+
|
|
231
|
+
- **No scheme / plain path / `file://`** — filesystem read. Lives
|
|
232
|
+
inline in `src/utils.js readEntityContent` because the engine
|
|
233
|
+
already touches the filesystem everywhere.
|
|
234
|
+
- **`http://` / `https://`** — `src/plugins/providers/http.js`.
|
|
235
|
+
Conditional GET with ETag + Last-Modified, in-memory response
|
|
236
|
+
cache, inflight coalescing on the same URL, binary mirror to
|
|
237
|
+
`runtime/http-cache/<sha-of-url>.<ext>`, operator-supplied
|
|
238
|
+
headers via `entity.meta.httpHeaders`, configurable timeout via
|
|
239
|
+
`entity.meta.httpTimeoutMs`. Test affordance:
|
|
240
|
+
`__resetHttpCacheForTests()` clears the module-level caches
|
|
241
|
+
between unit tests.
|
|
242
|
+
|
|
243
|
+
Any other scheme (`gdrive://`, `notion://`, `s3://`, …) routes
|
|
244
|
+
through the dynamic-import-by-name path — `mikser-io-provider-<scheme>`
|
|
245
|
+
must be installed as a sibling package and export a top-level
|
|
246
|
+
`read(entity)` function. See ADR-0010 / the gdrive provider for
|
|
247
|
+
the convention.
|
|
248
|
+
|
|
249
|
+
## Multi-emitter collections (sweep ownership)
|
|
250
|
+
|
|
251
|
+
Collections like `documents` / `files` / `assets` aren't owned by a
|
|
252
|
+
single plugin. The file source (via `useSource`) emits into them
|
|
253
|
+
from its scanned folder, but other plugins legitimately emit into
|
|
254
|
+
the same collection: `mikser-io-csv` fans CSV rows into `documents`,
|
|
255
|
+
a gdrive sync would emit there too, an API endpoint can push into
|
|
256
|
+
`files`. The catalog has no opinion on emitter identity — only on
|
|
257
|
+
collection.
|
|
258
|
+
|
|
259
|
+
The delete sweep MUST respect that. `sweepDeleted(collection,
|
|
260
|
+
scanned, onDelete, ownerPrefix)` requires `ownerPrefix` (typically
|
|
261
|
+
`absFolder` for file sources, `layoutsFolder` for layouts) and only
|
|
262
|
+
considers entities whose `entity.uri` is rooted under that prefix.
|
|
263
|
+
Foreign-emitter entities have a different `uri` shape:
|
|
264
|
+
- CSV row entities: empty `uri` (synthetic — meta is the content)
|
|
265
|
+
- HTTP-fetched CSV parent: `uri = 'https://…'`
|
|
266
|
+
- gdrive-sourced docs: `uri = 'gdrive://…'`
|
|
267
|
+
|
|
268
|
+
…and the LIKE clause excludes them. Without the scope, every cycle's
|
|
269
|
+
file-source sweep silently wipes every co-collection emitter's
|
|
270
|
+
entities. Throwing instead of silently sweeping the whole collection
|
|
271
|
+
is intentional — this used to be a class-of-bugs landmine. If you're
|
|
272
|
+
writing a new source-shaped plugin, the scoping is mandatory.
|
|
273
|
+
|
|
274
|
+
Test coverage: `test/unit/source-sweep.test.js`.
|
|
275
|
+
|
|
143
276
|
## Naming conventions
|
|
144
277
|
|
|
145
278
|
- **Engine state** at `runtime.<name>` — `runtime.refs`, `runtime.catalog`.
|
|
146
279
|
- **Plugin surfaces** at `runtime.options.<plugin>` —
|
|
147
280
|
`runtime.options.preview`, `runtime.options.layouts.inspect`.
|
|
281
|
+
- **Reachability:** `runtime.options.url` is the engine's resolved
|
|
282
|
+
public URL (from `--url` or `config.url`, trailing slash stripped,
|
|
283
|
+
validated). Plugins that need external reachability — webhook
|
|
284
|
+
receivers, absolute links in emails, MCP preview URLs returned to
|
|
285
|
+
agents, forms share links — read this. Standard gating pattern:
|
|
286
|
+
```js
|
|
287
|
+
const canPush = runtime.options.url?.startsWith('https://')
|
|
288
|
+
if (canPush) registerWebhookAt(`${runtime.options.url}/api/X/webhook`)
|
|
289
|
+
else setupPollingFallback()
|
|
290
|
+
```
|
|
291
|
+
`runtime.options.port` is the internal listener port (kept for
|
|
292
|
+
loopback/dev URL building); `runtime.options.url` is the external
|
|
293
|
+
origin. When in doubt: external = `url`, internal = `port`.
|
|
148
294
|
- **Engine functions** as module-level exports from `mikser-io`:
|
|
149
295
|
`import { queryEntities, subscribe, useRenderer, useCollection,
|
|
150
296
|
readEntityContent, isTextEntity } from 'mikser-io'`.
|
|
151
297
|
- **Plugin packages**: `mikser-io-<name>` (mikser-io-mcp, mikser-io-vector,
|
|
152
|
-
mikser-io-schemas, etc.).
|
|
298
|
+
mikser-io-schemas, etc.). Each exports a v9 named factory in
|
|
299
|
+
camelCase: `import { vector } from 'mikser-io-vector'`,
|
|
300
|
+
`import { renderHbs } from 'mikser-io'`. Consumer uses
|
|
301
|
+
`plugins: [vector({...})]` — never the bare string.
|
|
153
302
|
- **MCP tools**: `mikser_<verb>` or `mikser_<subsystem>_<verb>`:
|
|
154
303
|
`mikser_query_entities`, `mikser_read_entity`, `mikser_update_entity`,
|
|
155
304
|
`mikser_delete_entity`, `mikser_render`, `mikser_refs_inbound`,
|
|
@@ -206,25 +355,39 @@ brevity.
|
|
|
206
355
|
persistence pattern. Journal-on-sqlite (Phase 7) enables `--resume`;
|
|
207
356
|
auto-persist (Phase 9) means plugins mutate the yielded entity and
|
|
208
357
|
the journal writes back without an explicit `updateEntry` call.
|
|
358
|
+
- **0010** — Plugin bundles + factory-call form + inline options.
|
|
359
|
+
Plugins are imported by name and called as factories;
|
|
360
|
+
`plugins: []` carries factory returns, never strings.
|
|
361
|
+
Lifecycle plugins are `(options) => (core) => void`; renderers
|
|
362
|
+
return `{name, options, load?, render?}`; postprocessors return
|
|
363
|
+
`{name, options, output?, setup?, postprocess, teardown?}`. Per-
|
|
364
|
+
plugin config moved entirely off `runtime.config.<plugin>`; it
|
|
365
|
+
arrives as factory args, gets stashed on the descriptor, and is
|
|
366
|
+
passed as `config` to `load`/`render`/`setup`/`postprocess`.
|
|
209
367
|
|
|
210
368
|
## MCP
|
|
211
369
|
|
|
212
|
-
Lives in `mikser-io-mcp` plugin (separate repo). Activate by
|
|
213
|
-
`
|
|
370
|
+
Lives in `mikser-io-mcp` plugin (separate repo). Activate by calling
|
|
371
|
+
`mcp()` **first** in your `mikser.config.js` plugins array:
|
|
214
372
|
|
|
215
373
|
```js
|
|
374
|
+
import { mcp } from 'mikser-io-mcp'
|
|
375
|
+
|
|
216
376
|
export default {
|
|
217
|
-
plugins: [
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
377
|
+
plugins: [
|
|
378
|
+
mcp({
|
|
379
|
+
// path: '/mcp', // default '/mcp'
|
|
380
|
+
// endpoints: { ... }, // per-endpoint token + scope
|
|
381
|
+
}),
|
|
382
|
+
/* ...other plugins */
|
|
383
|
+
],
|
|
222
384
|
}
|
|
223
385
|
```
|
|
224
386
|
|
|
225
387
|
Must be first because its factory creates `runtime.options.mcp`
|
|
226
|
-
synchronously, and other plugins gate their
|
|
227
|
-
`if (runtime.options.mcp)` in their own
|
|
388
|
+
synchronously when its closure runs, and other plugins gate their
|
|
389
|
+
MCP tool registration on `if (runtime.options.mcp)` in their own
|
|
390
|
+
`onLoaded`.
|
|
228
391
|
|
|
229
392
|
There is **no `--mcp` CLI flag**. Activation is plugin-presence only.
|
|
230
393
|
|
|
@@ -276,15 +439,35 @@ There is **no `--mcp` CLI flag**. Activation is plugin-presence only.
|
|
|
276
439
|
|
|
277
440
|
- **New engine capability?** Run through ADR-0006's five tests. Bar
|
|
278
441
|
is high. Express is the only earned addition.
|
|
279
|
-
- **New plugin?** Own repo, named `mikser-io-<name>`.
|
|
280
|
-
|
|
281
|
-
|
|
442
|
+
- **New plugin?** Own repo, named `mikser-io-<name>`. Exports a
|
|
443
|
+
named v9 factory (e.g. `export function vector(options = {}) {
|
|
444
|
+
return (core) => { ... } }`). Composes against
|
|
445
|
+
`runtime.options.app` / `runtime.options.mcp` / lifecycle hooks.
|
|
446
|
+
Never imports another plugin's source — and the engine never
|
|
447
|
+
reads `runtime.config.<plugin>` for plugin options; everything
|
|
448
|
+
flows through the factory arg (ADR-0010).
|
|
282
449
|
- **New MCP tool?** Add to `mikser-io-mcp/index.js` via
|
|
283
450
|
`mcp.simpleTool(name, description, zodSchema, handler)`. Tool name
|
|
284
451
|
follows `mikser_*` convention.
|
|
285
452
|
- **New lifecycle hook?** Almost certainly no. Existing hooks cover
|
|
286
453
|
all known patterns. If you think you need one, post the use case
|
|
287
454
|
to ADR-0006 review.
|
|
455
|
+
- **New content source (gdrive, notion, s3, github, …)?** Two
|
|
456
|
+
things ship together:
|
|
457
|
+
- A regular lifecycle plugin (the sync — emits entities into the
|
|
458
|
+
catalog, sets `entity.uri = '<scheme>://...'` so the dispatch
|
|
459
|
+
knows where to route reads).
|
|
460
|
+
- A named export `read(entity)` from the same package, which
|
|
461
|
+
must be named `mikser-io-provider-<scheme>` so the engine can
|
|
462
|
+
find it. `readEntityContent(entity)` parses the scheme out of
|
|
463
|
+
`entity.uri`, dynamic-imports the package, calls its `read`.
|
|
464
|
+
No registry, no descriptor — same package-name convention as
|
|
465
|
+
`mikser-io-render-*` and `mikser-io-post-*`.
|
|
466
|
+
Built-in: plain local paths (no scheme) and `file://` URIs are
|
|
467
|
+
read via `fs.readFile` directly — no provider package needed for
|
|
468
|
+
the canonical local-filesystem case. `entity.content` already
|
|
469
|
+
populated is a fast-path that skips the dispatch entirely (for
|
|
470
|
+
small remote docs eager-fetched at sync time).
|
|
288
471
|
|
|
289
472
|
## Test suites
|
|
290
473
|
|
|
@@ -301,6 +484,48 @@ There is **no `--mcp` CLI flag**. Activation is plugin-presence only.
|
|
|
301
484
|
+ clean-build timing). `SIZE=realistic` + entity count knobs
|
|
302
485
|
documented in **Perf**.
|
|
303
486
|
|
|
487
|
+
## Dev workspace
|
|
488
|
+
|
|
489
|
+
The siblings live side-by-side under `/Users/dick/Projects/mikser/`
|
|
490
|
+
and share an npm workspace declared at the parent's `package.json`:
|
|
491
|
+
|
|
492
|
+
```json
|
|
493
|
+
{
|
|
494
|
+
"private": true,
|
|
495
|
+
"workspaces": ["mikser-io", "mikser-io-*"]
|
|
496
|
+
}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
`npm install` at that root hoists everything: each sibling's
|
|
500
|
+
`node_modules/mikser-io` (and any other cross-workspace dep)
|
|
501
|
+
becomes a symlink to the working copy. The whole tree runs
|
|
502
|
+
against ONE module instance of mikser-io.
|
|
503
|
+
|
|
504
|
+
**This isn't optional ergonomics — it's correctness.** Plugins
|
|
505
|
+
use `AsyncLocalStorage` (currently `queryContext` for sidecar
|
|
506
|
+
findEntities tracking; the schema-version meta could grow more).
|
|
507
|
+
Without workspace deduplication, npm 7+ auto-installs the peer
|
|
508
|
+
dep into each sibling's own `node_modules`. Layouts'
|
|
509
|
+
`import { queryContext } from 'mikser-io'` then resolves to its
|
|
510
|
+
bundled copy — a different AsyncLocalStorage instance than the
|
|
511
|
+
engine uses. Sidecar queries don't get tracked → no `query`
|
|
512
|
+
edges in `mikser_snapshots.refClosure` →
|
|
513
|
+
`manifest.queryAffected` returns empty → aggregate layouts
|
|
514
|
+
(index pages, sitemaps, RSS feeds) never invalidate when new
|
|
515
|
+
matching entities land. Silent broken-incremental.
|
|
516
|
+
|
|
517
|
+
`test/scenarios/_harness.js` also auto-coalesces (replaces any
|
|
518
|
+
real `mikser-io-layouts/node_modules/mikser-io` directory with a
|
|
519
|
+
symlink to MIKSER_ROOT on every `setupFixture`) as a backstop —
|
|
520
|
+
the scenarios pass with or without the workspace, but the
|
|
521
|
+
workspace is the canonical setup. Same shape applies to any
|
|
522
|
+
future sibling that uses `queryContext` or other module-level
|
|
523
|
+
state from mikser-io.
|
|
524
|
+
|
|
525
|
+
For production consumers (someone `npm install mikser-io
|
|
526
|
+
mikser-io-layouts` in their own project), npm resolves both from
|
|
527
|
+
the consumer's own project tree — no duplication, no bug.
|
|
528
|
+
|
|
304
529
|
## Reference
|
|
305
530
|
|
|
306
531
|
- `documentation/architecture.md` — module map (audit before relying
|
|
@@ -309,5 +534,8 @@ There is **no `--mcp` CLI flag**. Activation is plugin-presence only.
|
|
|
309
534
|
- `documentation/configuration.md` — config reference
|
|
310
535
|
- `documentation/api-reference.md` — public API
|
|
311
536
|
- `test/perf/` — render-pipeline perf rig
|
|
312
|
-
- Sibling repos: `mikser-io-
|
|
537
|
+
- Sibling repos: `mikser-io-layouts` (canonical SSG-flavor render-task
|
|
538
|
+
production policy), `mikser-io-mcp`, `mikser-io-vector`,
|
|
539
|
+
`mikser-io-schemas`, `mikser-io-forms`, `mikser-io-post-{mjml,pdf,email}`,
|
|
540
|
+
`mikser-io-render-{eta,liquid,markdown,metatext}`,
|
|
313
541
|
`mikser-io-sdk-{api,react,svelte,vue,vector}`, `mikser-io-example-blog`
|
package/README.md
CHANGED
|
@@ -50,6 +50,28 @@ Build mikser into the parts of your application that are content-shaped. Keep th
|
|
|
50
50
|
|
|
51
51
|
**Open source.** MIT-licensed, on GitHub, no telemetry, no auth wall, no SaaS dependency. What you see is what runs.
|
|
52
52
|
|
|
53
|
+
## Mix content from anywhere, ship to anywhere
|
|
54
|
+
|
|
55
|
+
A real content stack pulls from more than one system. Marketing copy lives in a CMS or a spreadsheet. Prices and stock live in an ERP. Hero images live in a DAM. Editorial pages live in markdown files in the repo. Most teams either pick one tool and contort the rest to fit it, or build sync services that copy everything into one database — and then more sync services when things drift out of sync.
|
|
56
|
+
|
|
57
|
+
Mikser is built around a different bet: **one queryable substrate that any source can pour into, and any frontend can read from.**
|
|
58
|
+
|
|
59
|
+
**Any source.** Provider plugins let you treat external systems as content sources. A Google Sheet, an ERP feed, a Drive folder, a Notion database, an HTTP webhook, the local repo's `.md` files — each becomes a stream of entities flowing into mikser's catalog with the same shape. The authoring tool doesn't change. The editorial workflow doesn't change. The team writing product copy in Google Sheets keeps writing product copy in Google Sheets — mikser just notices when they save.
|
|
60
|
+
|
|
61
|
+
**Cross-source composition through references.** Mikser tracks references between entities the way a graph database does. A product page can declare `$marketing` pointing at a row pulled from a spreadsheet, `$pricing` at a record pulled from your ERP, `$hero` at an asset pulled from your DAM. A single API call from your frontend asks for the product page *with* its referenced data — and gets the page, the marketing copy, the price, and the hero image merged into one response. One round trip, not three or four, and no consumer-side join logic to maintain.
|
|
62
|
+
|
|
63
|
+
**Live updates are uniform across every source.** Edit a cell in the spreadsheet, change a price in the ERP, replace an asset in the DAM — the pages depending on those entities update within seconds, in every frontend connected to mikser's live channel. You don't write per-source invalidation; mikser already knows which pages reference what.
|
|
64
|
+
|
|
65
|
+
**Your frontend is whatever you want.** Mikser exposes the catalog over HTTP (and over MCP, for AI agents). You query it from React, Vue, Svelte, SvelteKit, Next.js, an iOS app, a kiosk — anything that speaks HTTP. The framework SDKs (`mikser-io-sdk-react`, `mikser-io-sdk-vue`, `mikser-io-sdk-svelte`) make the calls feel native, but they're optional. Headless CMSes free you from the database lock. Frameworks like Astro free you from one kind of authoring lock. Mikser frees you from both at once: **any source on the input side, any framework on the output side.**
|
|
66
|
+
|
|
67
|
+
What this looks like in practice:
|
|
68
|
+
|
|
69
|
+
- Marketing edits product copy in Google Sheets, prices come from your ERP, both flow into the same product page in your storefront — changes appear within seconds, without your e-commerce team writing webhook handlers or cache-invalidation logic.
|
|
70
|
+
- A SaaS pulls customer-facing release notes from `release-notes/*.md` in the repo and feature flags from LaunchDarkly; both render in a dashboard built in the team's own React stack, not a framework somebody else picked.
|
|
71
|
+
- A magazine pulls editorial articles from local markdown, contributor bios from Notion, sponsorship info from Airtable, and renders the same article surface across web, email (MJML), and PDF — one catalog, one set of references, three outputs.
|
|
72
|
+
|
|
73
|
+
Adding a source is mechanical. See [`mikser-io-csv`](https://github.com/almero-digital-marketing/mikser-io-csv) (any HTTP-served CSV, with live polling), [`mikser-io-provider-gdrive`](https://github.com/almero-digital-marketing/mikser-io-provider-gdrive) (Google Drive), and the built-in `http` provider for the shape. If a system has an API, it can be a mikser source, usually in well under 100 lines.
|
|
74
|
+
|
|
53
75
|
## Built for AI-assisted development
|
|
54
76
|
|
|
55
77
|
Files-as-source isn't just a portability story — it makes the project unusually friendly to AI coding agents. There's a static-time half (the agent reads your tree the way it reads any repo — no DB connection, no schema upload, no sandboxed query layer to learn) and a runtime half (when the agent needs to write or render, mikser ships its own MCP server in core, so it talks to the live engine instead of a parallel REST shim you have to maintain).
|
|
@@ -92,9 +114,11 @@ Plugins extend the tool surface the same way they mount HTTP routes; install the
|
|
|
92
114
|
|
|
93
115
|
```js
|
|
94
116
|
// mikser.config.js
|
|
117
|
+
import { mcp } from 'mikser-io-mcp'
|
|
118
|
+
|
|
95
119
|
export default {
|
|
96
|
-
plugins: [
|
|
97
|
-
//
|
|
120
|
+
plugins: [mcp(), /* … */],
|
|
121
|
+
// mcp({ path: '/mcp', endpoints: { … } }) when options are needed
|
|
98
122
|
}
|
|
99
123
|
```
|
|
100
124
|
|
|
@@ -148,6 +172,7 @@ The engine is what stays stable — the lifecycle, the catalog, the file-based c
|
|
|
148
172
|
|---|---|
|
|
149
173
|
| [`mikser-io-vector`](https://github.com/almero-digital-marketing/mikser-io-vector) | OpenAI embeddings + semantic search (sqlite-vec or pgvector) |
|
|
150
174
|
| [`mikser-io-schemas`](https://github.com/almero-digital-marketing/mikser-io-schemas) | Zod-backed entity validation + auto-generated TypeScript declarations for the SDK. Auto-detects `$`-keyed references and warns on broken ones — see [ADR-0007](./documentation/decisions/0007-references-declaration-and-expansion.md) |
|
|
175
|
+
| [`mikser-io-forms`](https://github.com/almero-digital-marketing/mikser-io-forms) | Public form-submission endpoints. POST → captcha + schema validation → write a document file plus uploaded files to disk; the `documents` / `files` plugins pick them up via their normal watch loop. Composes with `mikser-io-schemas` for schema-by-name; built-in captcha providers (Google v2/v3, hCaptcha, Turnstile) plus a custom-verify escape hatch |
|
|
151
176
|
| [`mikser-io-archive`](https://github.com/almero-digital-marketing/mikser-io-archive) | Persist matching entities to YAML — audit trail, versioned content history, downstream export |
|
|
152
177
|
| `mapper` | Run config-supplied transforms over matched entities each cycle (in-core, generic transformation layer) |
|
|
153
178
|
| [`mikser-io-live`](https://github.com/almero-digital-marketing/mikser-io-live) | Lightweight dev server with browser auto-refresh — pair with `--watch` for the classic save→reload loop |
|
package/index.js
CHANGED
|
@@ -15,4 +15,35 @@ export * from './src/manager.js'
|
|
|
15
15
|
export * from './src/logger.js'
|
|
16
16
|
export * from './src/engine.js'
|
|
17
17
|
export * from './src/render.js'
|
|
18
|
-
export * from './src/source.js'
|
|
18
|
+
export * from './src/source.js'
|
|
19
|
+
|
|
20
|
+
// Built-in plugin factories. Each takes options and returns the
|
|
21
|
+
// (core) => void closure the engine calls at onLoad time. See ADR-0010
|
|
22
|
+
// for the v9 plugin shape.
|
|
23
|
+
export { api } from './src/plugins/api.js'
|
|
24
|
+
export { assets } from './src/plugins/assets.js'
|
|
25
|
+
export { commands } from './src/plugins/commands.js'
|
|
26
|
+
export { data } from './src/plugins/data.js'
|
|
27
|
+
export { documents } from './src/plugins/documents.js'
|
|
28
|
+
export { files } from './src/plugins/files.js'
|
|
29
|
+
export { frontMatter } from './src/plugins/front-matter.js'
|
|
30
|
+
export { json } from './src/plugins/json.js'
|
|
31
|
+
// layouts moved to sibling: import from 'mikser-io-layouts'
|
|
32
|
+
export { mapper } from './src/plugins/mapper.js'
|
|
33
|
+
export { observer } from './src/plugins/observer.js'
|
|
34
|
+
export { preview } from './src/plugins/preview.js'
|
|
35
|
+
export { resources } from './src/plugins/resources.js'
|
|
36
|
+
export { shares } from './src/plugins/shares.js'
|
|
37
|
+
export { validator } from './src/plugins/validator.js'
|
|
38
|
+
export { yaml } from './src/plugins/yaml.js'
|
|
39
|
+
|
|
40
|
+
// Built-in renderers. v9 factory shape returns the descriptor that the
|
|
41
|
+
// loader stores in `runtime.renderers`; the same module also still
|
|
42
|
+
// exports `load`/`render` at the top level so Piscina worker dispatch
|
|
43
|
+
// can resolve via dynamic import. ADR-0010.
|
|
44
|
+
export { renderAsset } from './src/plugins/render/asset.js'
|
|
45
|
+
export { renderFile } from './src/plugins/render/file.js'
|
|
46
|
+
export { renderHbs } from './src/plugins/render/hbs.js'
|
|
47
|
+
export { renderHref } from './src/plugins/render/href.js'
|
|
48
|
+
export { renderPreset } from './src/plugins/render/preset.js'
|
|
49
|
+
export { renderResource } from './src/plugins/render/resource.js'
|
package/package.json
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mikser-io",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.0.1",
|
|
4
4
|
"description": "<p align=\"center\"> <img src=\"mikser-lockup-stacked.svg\" alt=\"mikser\" width=\"198\" /> </p>",
|
|
5
5
|
"main": "index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./index.js",
|
|
8
|
+
"./package.json": "./package.json",
|
|
9
|
+
"./*": "./*"
|
|
10
|
+
},
|
|
6
11
|
"scripts": {
|
|
7
12
|
"debug": "node --no-warnings app.js --server --watch --working-folder test/fixture",
|
|
8
13
|
"test:unit": "node --test --test-reporter=spec 'test/unit/**/*.test.js'",
|
|
@@ -56,6 +61,7 @@
|
|
|
56
61
|
"express": "^5.2.1"
|
|
57
62
|
},
|
|
58
63
|
"devDependencies": {
|
|
64
|
+
"@ai-sdk/openai": "^3.0.71",
|
|
59
65
|
"fluent-ffmpeg": "^2.1.3",
|
|
60
66
|
"mikser-io-decap": "file:../mikser-io-decap",
|
|
61
67
|
"mikser-io-post-mjml": "file:../mikser-io-post-mjml",
|
package/src/config.js
CHANGED
|
@@ -4,34 +4,25 @@ import { onLoad } from './lifecycle.js'
|
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
|
|
6
6
|
onLoad(async () => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
7
|
+
const logger = useLogger()
|
|
8
|
+
const configFile = path.resolve(runtime.options.config)
|
|
9
|
+
logger.info('Config: %s', configFile)
|
|
10
|
+
try {
|
|
11
|
+
const config = await import(configFile)
|
|
12
|
+
if (typeof config.default == 'function') {
|
|
13
|
+
runtime.config = await config.default(runtime)
|
|
14
|
+
} else if (typeof config.default == 'object') {
|
|
15
|
+
runtime.config = config.default
|
|
16
|
+
}
|
|
17
|
+
} catch (err) {
|
|
18
|
+
if (err.code != 'ERR_MODULE_NOT_FOUND') throw err
|
|
19
|
+
}
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
runtime.config[plugin] = await config.default(runtime)
|
|
29
|
-
} else if (typeof config.default == 'object') {
|
|
30
|
-
runtime.config[plugin] = config.default
|
|
31
|
-
}
|
|
32
|
-
} catch (err) {
|
|
33
|
-
if (err.code != 'ERR_MODULE_NOT_FOUND') throw err
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
21
|
+
// v8 used to walk `runtime.config.plugins` looking for matching
|
|
22
|
+
// `config/<plugin>.config.js` files to merge into `runtime.config`,
|
|
23
|
+
// because plugin entries were strings (names). v9 entries are factory
|
|
24
|
+
// call results (closures or descriptors), and plugin options arrive
|
|
25
|
+
// as factory args — see ADR-0010 — so per-plugin auxiliary config
|
|
26
|
+
// files have nothing to bind to. The loader was removed when the
|
|
27
|
+
// plugins list stopped carrying names.
|
|
37
28
|
})
|