mikser-io 10.11.0 → 10.13.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/docs/architecture.md +12 -12
- package/docs/configuration.md +21 -32
- package/docs/diagnostics.md +122 -3
- package/docs/lifecycle.md +2 -2
- package/docs/overview.md +1 -1
- package/docs/plugins.md +30 -206
- package/docs/rendering.md +1 -1
- package/index.js +14 -12
- package/package.json +1 -1
- package/src/auth.js +17 -2
- package/src/catalog.js +2 -2
- package/src/changeset.js +2 -2
- package/src/config.js +2 -2
- package/src/database/index.js +1 -1
- package/src/engine/boot.js +300 -0
- package/src/engine/checks.js +226 -0
- package/src/engine/dispatch.js +97 -0
- package/src/engine/finalize.js +95 -0
- package/src/engine/index.js +150 -0
- package/src/engine/postprocess-cycle.js +223 -0
- package/src/engine/render-cycle.js +432 -0
- package/src/engine/report-only.js +189 -0
- package/src/engine/workers.js +46 -0
- package/src/explain.js +1 -1
- package/src/instance.js +2 -2
- package/src/journal.js +3 -3
- package/src/lifecycle.js +1 -1
- package/src/logger/index.js +39 -0
- package/src/logger/levels.js +180 -0
- package/src/logger/progress.js +224 -0
- package/src/logger/streams.js +361 -0
- package/src/manager.js +2 -2
- package/src/manifest/cycle.js +307 -0
- package/src/{manifest.js → manifest/index.js} +42 -752
- package/src/manifest/schema.js +73 -0
- package/src/manifest/snapshot.js +182 -0
- package/src/manifest/sources.js +57 -0
- package/src/manifest/statements.js +190 -0
- package/src/plugins/api.js +1 -1
- package/src/plugins/providers/http.js +1 -1
- package/src/plugins/render/href.js +1 -1
- package/src/plugins/render/resource.js +1 -1
- package/src/plugins/resources.js +1 -1
- package/src/plugins.js +1 -1
- package/src/postprocess.js +1 -1
- package/src/provenance.js +1 -1
- package/src/references.js +1 -1
- package/src/refs.js +4 -4
- package/src/render.js +2 -2
- package/src/report.js +1 -1
- package/src/roles.js +21 -5
- package/src/routes.js +1 -1
- package/src/runtime.js +1 -1
- package/src/search.js +1 -1
- package/src/server.js +1 -1
- package/src/source.js +1 -1
- package/src/subscriptions.js +1 -1
- package/src/use-logger.js +18 -0
- package/src/utils/entity.js +423 -0
- package/src/utils/errors.js +45 -0
- package/src/utils/expand.js +237 -0
- package/src/utils/hash.js +197 -0
- package/src/utils/index.js +19 -0
- package/src/utils/junk.js +109 -0
- package/src/utils/library.js +29 -0
- package/src/utils/net.js +33 -0
- package/src/utils/output.js +195 -0
- package/src/utils/refs.js +127 -0
- package/src/write.js +1 -1
- package/testing/harness.js +1 -1
- package/src/engine.js +0 -1672
- package/src/logger.js +0 -723
- package/src/plugins/assets.js +0 -1023
- package/src/plugins/render/asset.js +0 -83
- package/src/plugins/render/preset.js +0 -67
- package/src/utils.js +0 -1369
package/docs/architecture.md
CHANGED
|
@@ -11,12 +11,12 @@ mikser-io/
|
|
|
11
11
|
│
|
|
12
12
|
└── src/
|
|
13
13
|
├── runtime.js Singleton object — global state and lifecycle coordination
|
|
14
|
-
├── engine
|
|
14
|
+
├── engine/ setup() and the lifecycle phases, one file each: boot, dispatch, render-cycle, postprocess-cycle, finalize; plus report-only, checks and workers. index.js wires them in order
|
|
15
15
|
├── lifecycle.js Hook registration functions + entity write helpers
|
|
16
16
|
├── journal.js Per-cycle queue persisted to mikser_journal (auto-persist on yielded entities; survives crashes so --resume can pick up)
|
|
17
17
|
├── catalog.js Persistent entity registry (mikser_entities sqlite table + 10k LRU on findById)
|
|
18
18
|
├── refs.js Inverse-reference graph (mikser_refs sqlite table)
|
|
19
|
-
├── manifest.js
|
|
19
|
+
├── manifest/ schema, snapshot build/parse, sources, prepared statements, the onLoaded/onFinalize cycle; index.js keeps createManifest
|
|
20
20
|
├── database/ createSqliteDatabase, registerSchema, useDatabase, sift→SQL translator, queryContext (AsyncLocalStorage)
|
|
21
21
|
├── subscriptions.js subscribe() primitive (journal-walk + graph-dispatch modes)
|
|
22
22
|
├── source.js useSource — folder-of-files import pattern shared by documents/files (and used by sibling mikser-io-layouts)
|
|
@@ -24,11 +24,11 @@ mikser-io/
|
|
|
24
24
|
├── config.js Config file loading
|
|
25
25
|
├── plugins.js Plugin resolution and loading
|
|
26
26
|
├── manager.js File watching and cron scheduling
|
|
27
|
-
├── logger.js
|
|
27
|
+
├── logger/ streams.js (pino + pretty + multistream), levels.js (which level is in force), progress.js (the gauge and the records that replace it); index.js is the barrel
|
|
28
28
|
├── render.js Render worker function (runs in main or Piscina threads); ensureWorkerDb gives each worker a read-only sqlite handle
|
|
29
29
|
├── postprocess.js Postprocess worker function (runs in main or Piscina threads)
|
|
30
30
|
├── track.js Render-time dependency tracking (catalog queries auto-report via queryContext)
|
|
31
|
-
├── utils
|
|
31
|
+
├── utils/ entity, refs, hash, expand, output, errors, junk, net, library; index.js re-exports all 37
|
|
32
32
|
├── constants.js OPERATION, ACTION, TASKS enums
|
|
33
33
|
│
|
|
34
34
|
├── plugins/ Built-in content source and transform plugins
|
|
@@ -72,13 +72,13 @@ runtime
|
|
|
72
72
|
├── state Arbitrary plugin state — catalog/refs/manifest are NOT here; they live as their own sqlite-backed facades below
|
|
73
73
|
├── catalog entity catalog facade (set by catalog.js) — findEntity / findEntities / iterateEntities / queryEntities / readEntity / subscribe / assertExpand. Backed by mikser_entities + 10k LRU on findById.
|
|
74
74
|
├── refs inverse-ref graph facade (set by refs.js) — inboundFor / outboundFor / allRefs / size / rename / subscribeGraph / inverseClosureOf. Backed by mikser_refs.
|
|
75
|
-
├── manifest render snapshot facade (set by manifest.js) — shouldSkip / record / recordedHashes. Backed by mikser_snapshots.
|
|
75
|
+
├── manifest render snapshot facade (set by manifest/cycle.js) — shouldSkip / record / recordedHashes. Backed by mikser_snapshots.
|
|
76
76
|
├── lookupHref Sync href→entity lookup; available inside Piscina workers (each opens its own read-only sqlite handle on first task)
|
|
77
77
|
├── validators[] Array of validation functions
|
|
78
78
|
├── mutex Semaphore for process() serialisation
|
|
79
79
|
├── abortController Current run's AbortController
|
|
80
80
|
│
|
|
81
|
-
├── engine Service objects (set by engine.js)
|
|
81
|
+
├── engine Service objects (set by engine/index.js)
|
|
82
82
|
│ ├── logger pino instance
|
|
83
83
|
│ ├── commander Commander instance
|
|
84
84
|
│ ├── renderWorkers Lazy Piscina pool (minThreads: 0, idleTimeout: 30_000)
|
|
@@ -155,7 +155,7 @@ mikser_journal (RENDER rows)
|
|
|
155
155
|
│
|
|
156
156
|
▼
|
|
157
157
|
[RENDER phase]
|
|
158
|
-
engine.js dispatches RENDER rows → render() per row, dispatch mode
|
|
158
|
+
engine/render-cycle.js dispatches RENDER rows → render() per row, dispatch mode
|
|
159
159
|
picked per row from { INLINE, SERIAL, WORKER }
|
|
160
160
|
│
|
|
161
161
|
▼
|
|
@@ -173,7 +173,7 @@ mikser_journal (POSTPROCESS rows)
|
|
|
173
173
|
│
|
|
174
174
|
▼
|
|
175
175
|
[POSTPROCESS phase]
|
|
176
|
-
engine.js dispatches POSTPROCESS rows → postprocess() per row
|
|
176
|
+
engine/render-cycle.js dispatches POSTPROCESS rows → postprocess() per row
|
|
177
177
|
(same INLINE/SERIAL/WORKER dispatch model as render)
|
|
178
178
|
│
|
|
179
179
|
▼
|
|
@@ -322,7 +322,7 @@ export { default as runtime } from './src/runtime.js'
|
|
|
322
322
|
export * as constants from './src/constants.js'
|
|
323
323
|
|
|
324
324
|
// Utilities
|
|
325
|
-
export * from './src/utils.js' // checksum(), normalize(), matchEntity(), AbortError, etc.
|
|
325
|
+
export * from './src/utils/index.js' // checksum(), normalize(), matchEntity(), AbortError, etc.
|
|
326
326
|
|
|
327
327
|
// Lifecycle hooks and entity operations
|
|
328
328
|
export * from './src/lifecycle.js' // onXxx(), createEntity(), updateEntity(), deleteEntity(), renderEntity(), etc.
|
|
@@ -332,7 +332,7 @@ export * from './src/database/index.js' // useDatabase, registerSchema
|
|
|
332
332
|
export * from './src/journal.js' // addEntry, addEntries, updateEntry, useJournal, clearJournal
|
|
333
333
|
export * from './src/catalog.js' // findEntity, findEntities, iterateEntities, queryEntities, readEntity, assertExpand
|
|
334
334
|
export * from './src/refs.js' // refExists (runtime.refs.* is the main surface)
|
|
335
|
-
export * from './src/manifest.js' // (runtime.manifest.* is the main surface)
|
|
335
|
+
export * from './src/manifest/index.js' // (runtime.manifest.* is the main surface)
|
|
336
336
|
|
|
337
337
|
// Dependency tracking + subscriptions
|
|
338
338
|
export * from './src/track.js'
|
|
@@ -346,10 +346,10 @@ export * from './src/plugins.js'
|
|
|
346
346
|
export * from './src/manager.js' // watch(), schedule(), createdHook/updatedHook/deletedHook/triggeredHook
|
|
347
347
|
|
|
348
348
|
// Logger / progress
|
|
349
|
-
export * from './src/logger.js'
|
|
349
|
+
export * from './src/logger/index.js' // trackProgress, stopProgress, updateProgress, setLogLevel, etc.
|
|
350
350
|
|
|
351
351
|
// setup() and render dispatcher entry
|
|
352
|
-
export * from './src/engine.js' // setup()
|
|
352
|
+
export * from './src/engine/index.js' // setup()
|
|
353
353
|
export * from './src/render.js' // useRenderer
|
|
354
354
|
|
|
355
355
|
// Folder-of-files import helper
|
package/docs/configuration.md
CHANGED
|
@@ -53,8 +53,8 @@ These options are part of `runtime.options` and apply to the engine itself.
|
|
|
53
53
|
| `watch` | `-w, --watch` | boolean | `false` | Watch source folders for changes and rebuild incrementally. |
|
|
54
54
|
| `force` | `-f, --force` | boolean | `false` | Rebuild everything; disable incremental dispatch. |
|
|
55
55
|
| `resume` | `-R, --resume` | boolean | `false` | Continue from journal entries left by a previous interrupted run; skip the initial filesystem scan. The journal table survives crashes, so an interrupted cycle can be picked up by re-running with `--resume`. |
|
|
56
|
-
|
|
|
57
|
-
| `log` | `-l, --log` | string | — | Log level for this run: `trace`, `debug`, `info`, `notice`, `warn`, `error`, `fatal`, `silent`.
|
|
56
|
+
| — | `--audit-output` | boolean | `false` | Check every tree mikser writes into against the manifest snapshots — report drift instead of building. Command line only; there is no config key. |
|
|
57
|
+
| `log` | `-l, --log` | string | — | Log level for this run: `trace`, `debug`, `info`, `notice`, `warn`, `error`, `fatal`, `silent`. A level above `info` suppresses the version banner too. |
|
|
58
58
|
| `logInstall` | `--log-install` | string | — | Set the level on a **running** instance, so its own rebuilds are verbose too — the case a per-request flag cannot serve. Expires after 30 minutes, dies with the process, and is disclosed in the build report under `logLevel`. |
|
|
59
59
|
| `logReset` | `--log-reset` | boolean | `false` | Return a running instance to its configured level. |
|
|
60
60
|
| `threads` | — | number | `4` | Worker thread count for the Piscina pools (`renderWorkers`, `postprocessWorkers`). Both pools are lazy (`minThreads: 0` + `idleTimeout: 30_000`) so INLINE-only workloads spin up zero workers. |
|
|
@@ -232,40 +232,29 @@ The renderers (`render-hbs`, `render-eta`, `render-liquid`) consume the stripped
|
|
|
232
232
|
|
|
233
233
|
ECT layouts (`mikser-io-render-ect`) still file-load via ECT's own resolver — YAML at the top of `.ect` files renders as literal text. Pick `hbs` / `eta` / `liquid` for layouts that need self-describing metadata.
|
|
234
234
|
|
|
235
|
-
### `assets`
|
|
235
|
+
### `assets` *(moved to `mikser-io-assets`)*
|
|
236
|
+
|
|
237
|
+
Preset derivatives are a sibling package as of 10.12.0. There is no `assets`
|
|
238
|
+
key in `mikser.config.js`: options are factory arguments like every other
|
|
239
|
+
plugin (ADR-0010).
|
|
236
240
|
|
|
237
241
|
```js
|
|
238
|
-
|
|
239
|
-
assets: {
|
|
240
|
-
assetsFolder: 'assets', // Source folder for assets. Default: 'assets'
|
|
241
|
-
outputFolder: '', // Output subfolder. Default: root
|
|
242
|
-
|
|
243
|
-
// Preset definitions: preset name → match patterns (and optional config).
|
|
244
|
-
// Two shapes accepted:
|
|
245
|
-
presets: {
|
|
246
|
-
// 1. Bare string or array — backwards-compatible match patterns.
|
|
247
|
-
'thumbnail': ['@/images/*'],
|
|
248
|
-
'hero': '@/images/hero*',
|
|
249
|
-
|
|
250
|
-
// 2. Object with `match` and per-preset `options` — options merge
|
|
251
|
-
// over the preset module's own defaults at render time
|
|
252
|
-
// (config-side overrides win on overlap).
|
|
253
|
-
'medium-image': {
|
|
254
|
-
match: ['@/images/*', '@/files/photos/*'],
|
|
255
|
-
options: {
|
|
256
|
-
width: 800,
|
|
257
|
-
height: 600,
|
|
258
|
-
quality: 80,
|
|
259
|
-
},
|
|
260
|
-
},
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
```
|
|
242
|
+
import { assets, renderPreset, assetUrlHelper } from 'mikser-io-assets'
|
|
265
243
|
|
|
266
|
-
|
|
244
|
+
plugins: [
|
|
245
|
+
assets({
|
|
246
|
+
presets: { web: { match: ['/files/media/**'] } },
|
|
247
|
+
// auditIgnore: ['**/*.poster.jpg'], // extras a preset writes
|
|
248
|
+
}),
|
|
249
|
+
renderPreset(),
|
|
250
|
+
assetUrlHelper(),
|
|
251
|
+
]
|
|
252
|
+
```
|
|
267
253
|
|
|
268
|
-
|
|
254
|
+
`--assets <folder>`, `--presets <folder>` and `--render-presets [name]` are
|
|
255
|
+
declared by that package, so a config without `assets()` does not have them —
|
|
256
|
+
the flag is refused by name rather than accepted and ignored. Full reference:
|
|
257
|
+
[mikser-io-assets](https://github.com/almero-digital-marketing/mikser-io-assets#readme).
|
|
269
258
|
|
|
270
259
|
### `resources`
|
|
271
260
|
|
package/docs/diagnostics.md
CHANGED
|
@@ -344,8 +344,18 @@ sentence someone may later reword.
|
|
|
344
344
|
|
|
345
345
|
### `--audit-output`
|
|
346
346
|
|
|
347
|
-
Walks
|
|
348
|
-
instead of building.
|
|
347
|
+
Walks every tree mikser writes into, against the recorded snapshots, and
|
|
348
|
+
reports drift instead of building.
|
|
349
|
+
|
|
350
|
+
Every tree, not just the output folder: preset derivatives live at the
|
|
351
|
+
working-folder root and reach the site through a symlink the walk does not
|
|
352
|
+
follow, so anything unclaimed there used to be invisible to the one check
|
|
353
|
+
whose job is "what is on disk that mikser did not record". Plugins declare
|
|
354
|
+
their trees on `runtime.options.auditRoots` as `{ path, ignore }`, which is
|
|
355
|
+
also how a plugin states what is bookkeeping rather than output — a `.md5`
|
|
356
|
+
beside every derivative would otherwise be one orphan per derivative.
|
|
357
|
+
|
|
358
|
+
Five categories, and the split matters:
|
|
349
359
|
|
|
350
360
|
| Category | Meaning | Severity |
|
|
351
361
|
| --- | --- | --- |
|
|
@@ -367,9 +377,17 @@ missing or mismatched, `1` if only warnings, `0` when clean, and `2` when
|
|
|
367
377
|
there is no manifest to check against.
|
|
368
378
|
|
|
369
379
|
```bash
|
|
370
|
-
npx mikser --audit-output || echo "output
|
|
380
|
+
npx mikser --audit-output || echo "the output has drifted"
|
|
371
381
|
```
|
|
372
382
|
|
|
383
|
+
A preset that writes **more than one file** — a poster frame beside a video —
|
|
384
|
+
leaves the extras genuinely unclaimed, because the engine records the one
|
|
385
|
+
destination it handed over. They are reported, correctly, and that would be a
|
|
386
|
+
permanent non-zero exit for a legitimate preset. `assets({ auditIgnore: [...] })`
|
|
387
|
+
is how a project says those are expected. Stated rather than inferred:
|
|
388
|
+
guessing which unclaimed files are intentional is how a check learns to cry
|
|
389
|
+
wolf.
|
|
390
|
+
|
|
373
391
|
A destination is resolved against the output folder first and, when that
|
|
374
392
|
finds nothing, treated as a filesystem path — assets carry an absolute
|
|
375
393
|
destination built from `assetsFolder`, which may sit outside the output
|
|
@@ -957,6 +975,107 @@ that follows is what clears it — an engine deciding on a subsystem's behalf
|
|
|
957
975
|
that it has recovered would be inventing the one fact this surface exists to
|
|
958
976
|
report honestly.
|
|
959
977
|
|
|
978
|
+
## Codes
|
|
979
|
+
|
|
980
|
+
Every warning and fault carries a `code`, and the code is the stable part —
|
|
981
|
+
`--json` keys `warnings` and `faults` by it, `mikser_ping` surfaces faults by
|
|
982
|
+
it, and a check that greps message text breaks the first time someone
|
|
983
|
+
improves the wording. Assert on codes.
|
|
984
|
+
|
|
985
|
+
```bash
|
|
986
|
+
npx mikser --json | jq '[.warnings[].code] | group_by(.) | map({(.[0]): length}) | add'
|
|
987
|
+
npx mikser --json | jq -e '[.warnings[] | select(.code == "reference-broken")] | length == 0'
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
Severity is the level the record was logged at: `warn` becomes a warning in
|
|
991
|
+
the report, `error` **with a code** becomes a fault. An error without a code
|
|
992
|
+
is one render that threw, not a subsystem declaring it cannot work — that
|
|
993
|
+
distinction is the whole of [Faults](#faults) above.
|
|
994
|
+
|
|
995
|
+
### What was written
|
|
996
|
+
|
|
997
|
+
| Code | Severity | Means |
|
|
998
|
+
| --- | --- | --- |
|
|
999
|
+
| `reference-broken` | warn | A `src` / `href` / `url()` in the emitted output resolves to no file. |
|
|
1000
|
+
| `reference-over-deep` | warn | It resolves only because a `..` run was floored at the site root. Loads today; breaks one nesting level deeper. |
|
|
1001
|
+
| `reference-wrong-base` | warn | Nothing at the path written, but the file exists elsewhere — resolved against the wrong site root. |
|
|
1002
|
+
| `reference-no-derivative` | warn | A reference to a preset derivative that nothing produced. |
|
|
1003
|
+
| `reference-broken-summary`, `reference-over-deep-summary` | warn | The counts, so a check can assert on one line rather than N. |
|
|
1004
|
+
| `asset-missing` | warn | A template asked for an asset URL that is not in the output. Complements the reference scan: this one sees URLs that never reach an HTML file — a feed, a sitemap — and knows the entity. |
|
|
1005
|
+
| `asset-missing-summary` | warn | The count. |
|
|
1006
|
+
| `destination-collision` | warn | Two or more entities wrote the same destination this cycle. Usually produces **no** mismatch, because each render hashes the file after writing and the loser reads the winner's bytes. |
|
|
1007
|
+
| `empty-output` | warn | A render succeeded and produced a zero-byte file. |
|
|
1008
|
+
| `output-drift` | warn | The same inputs produced different bytes than last time — a non-deterministic renderer, or an input nothing is tracking. |
|
|
1009
|
+
| `output-drift-summary` | warn | The count. |
|
|
1010
|
+
|
|
1011
|
+
### Derivatives (`mikser-io-assets`)
|
|
1012
|
+
|
|
1013
|
+
| Code | Severity | Means |
|
|
1014
|
+
| --- | --- | --- |
|
|
1015
|
+
| `preset-unfinished` | warn | A derivative the manifest claims is on disk with no completed-render marker beside it — a derive that was killed. It is being re-derived; the file that was there is not trustworthy. |
|
|
1016
|
+
| `preset-no-match` | warn | A configured preset matched none of the entities evaluated this run. Phrased as what was observed: an incremental cycle legitimately evaluates a handful. |
|
|
1017
|
+
| `preset-unknown` | warn | `--render-presets` named a preset that is not configured. |
|
|
1018
|
+
|
|
1019
|
+
### Configuration and stores
|
|
1020
|
+
|
|
1021
|
+
| Code | Severity | Means |
|
|
1022
|
+
| --- | --- | --- |
|
|
1023
|
+
| `config-coverage-partial` | warn | This Node build has no module loader hooks, so the config stamp covers the entry file alone. Editing an imported module will not invalidate the cache. |
|
|
1024
|
+
| `durable-open` | error | The durable store could not be opened. Auth grants and the change-set log are unavailable. |
|
|
1025
|
+
| `durable-migration` | error | A registered migration failed. |
|
|
1026
|
+
| `durable-gitignore` | error | The durable store could not be added to `.gitignore` — it holds credentials and the working folder is usually a repo. |
|
|
1027
|
+
| `change-set-log` | error | Writes still land on disk but cannot be listed or undone. |
|
|
1028
|
+
|
|
1029
|
+
### Process and control
|
|
1030
|
+
|
|
1031
|
+
| Code | Severity | Means |
|
|
1032
|
+
| --- | --- | --- |
|
|
1033
|
+
| `instance-listen-failed` | warn | The control socket could not be opened, so other mikser commands in this folder will start their own engine instead of forwarding. |
|
|
1034
|
+
| `command-from-cli` | warn | A hook command was supplied on the command line. Deliberately loud: this build is running code that is not in the config. |
|
|
1035
|
+
| `command-hook-not-reached` | warn | A command was attached to a hook the cycle never reached. |
|
|
1036
|
+
| `command-install-without-instance` | warn | `--command-install` with nothing listening to install it on. |
|
|
1037
|
+
| `log-level-installed` | warn | This instance is running at a level installed with `--log-install` — not the configured level, and not this build asking for it. Carries its expiry. |
|
|
1038
|
+
|
|
1039
|
+
### Sources and progress
|
|
1040
|
+
|
|
1041
|
+
| Code | Severity | Means |
|
|
1042
|
+
| --- | --- | --- |
|
|
1043
|
+
| `observer-bad-uri` | warn | An observer's `uri` is not an absolute URL, so no webhook can be routed to it. |
|
|
1044
|
+
| `untracked-file-read` | warn | A template read a file outside every folder mikser takes entities from, so it has no entity and changing it invalidates nothing. |
|
|
1045
|
+
| `progress` | info | A long phase reporting where it has got to. See [Progress](#progress). |
|
|
1046
|
+
| `progress-finished` | info | A phase completed: what it was, how much it carried, what it cost. |
|
|
1047
|
+
| `progress-unfinished` | warn | A phase stopped before finishing. |
|
|
1048
|
+
|
|
1049
|
+
## Progress
|
|
1050
|
+
|
|
1051
|
+
Progress is **tracked always, drawn only where a bar belongs, and narrated
|
|
1052
|
+
only for phases that run long**. Off a TTY — CI, a pipe, `--json` — there is
|
|
1053
|
+
no bar, and the same facts arrive as records:
|
|
1054
|
+
|
|
1055
|
+
```
|
|
1056
|
+
[progress-finished] Documents import finished: 5 in 1ms
|
|
1057
|
+
[progress] Rendering: 4500/6000 — /documents/p2356.md
|
|
1058
|
+
[progress-finished] Rendering finished: 6000 in 3.2s
|
|
1059
|
+
```
|
|
1060
|
+
|
|
1061
|
+
Three rules, and each exists because the obvious alternative was wrong:
|
|
1062
|
+
|
|
1063
|
+
- **`progress-finished` is never gated.** Every phase says it ran and what it
|
|
1064
|
+
cost, at any duration. With the commentary throttled it is the only record
|
|
1065
|
+
most phases produce, and off a TTY it is the only place phase timings come
|
|
1066
|
+
from at all.
|
|
1067
|
+
- **The running commentary is one line per 30 seconds**, not per item and not
|
|
1068
|
+
per quartile. A quartile is a fraction of the *work*, so it says nothing
|
|
1069
|
+
about how often a line appears — four records in three seconds on a fast
|
|
1070
|
+
phase, four across an hour on a slow one.
|
|
1071
|
+
- **A duration is a measurement, never a zero.** Phases are timed with
|
|
1072
|
+
`performance.now()`, so `0.12ms` rather than the `0s` that a whole class of
|
|
1073
|
+
phases used to report.
|
|
1074
|
+
|
|
1075
|
+
A bar never starts while stdout carries a document (`--json`, `--tool`,
|
|
1076
|
+
`--tools`): the gauge writes to stdout, and forwarded to an instance it landed
|
|
1077
|
+
inside the JSON.
|
|
1078
|
+
|
|
960
1079
|
## When mikser is silent
|
|
961
1080
|
|
|
962
1081
|
Silence is this engine's characteristic failure mode: a declaration that
|
package/docs/lifecycle.md
CHANGED
|
@@ -286,9 +286,9 @@ onFinalized(async () => {
|
|
|
286
286
|
```
|
|
287
287
|
|
|
288
288
|
**What Mikser does here:**
|
|
289
|
-
- `catalog.js` / `refs.js` / `manifest
|
|
289
|
+
- `catalog.js` / `refs.js` / `manifest/` commit their per-cycle
|
|
290
290
|
transaction against `mikser.sqlite` (skipped if no changes)
|
|
291
|
-
- `engine.js` cleans up broken symlinks in the output folder
|
|
291
|
+
- `engine/finalize.js` cleans up broken symlinks in the output folder
|
|
292
292
|
- `journal.js` clears the per-cycle queue: `DELETE FROM mikser_journal`.
|
|
293
293
|
Anything still in the table when the process exits is left for the
|
|
294
294
|
next `--resume` run.
|
package/docs/overview.md
CHANGED
|
@@ -98,7 +98,7 @@ The most common confusion when starting out is *which artifact lives where, at w
|
|
|
98
98
|
| Asset variants | `assets/<preset>/` (preset outputs) | `assets` plugin | `onRender` |
|
|
99
99
|
| Persistent entity registry | `mikser_entities` table in `runtime/mikser.sqlite` | `catalog.js` | `onPersist` |
|
|
100
100
|
| Inverse-reference graph | `mikser_refs` table in `runtime/mikser.sqlite` | `refs.js` | `onPersist` |
|
|
101
|
-
| Render snapshots (manifest) | `mikser_snapshots` table in `runtime/mikser.sqlite` | `manifest
|
|
101
|
+
| Render snapshots (manifest) | `mikser_snapshots` table in `runtime/mikser.sqlite` | `manifest/` | `onPersist` / `onFinalize` |
|
|
102
102
|
| Per-cycle operations (journal) | `mikser_journal` table in `runtime/mikser.sqlite` | every plugin | every phase |
|
|
103
103
|
| Front-matter schemas | `schemas/` | `mikser-io-schemas` | `onValidate` |
|
|
104
104
|
| Rendered HTML | `out/<route>.html` | post plugins + http server | `onRender` |
|
package/docs/plugins.md
CHANGED
|
@@ -250,221 +250,44 @@ See the [mikser-io-layouts README](https://github.com/almero-digital-marketing/m
|
|
|
250
250
|
|
|
251
251
|
---
|
|
252
252
|
|
|
253
|
-
### `assets`
|
|
253
|
+
### `assets` *(sibling: `mikser-io-assets`)*
|
|
254
254
|
|
|
255
|
-
|
|
255
|
+
Runs **user-authored preset modules** over binary inputs — images, video,
|
|
256
|
+
audio, anything — and writes the derived files. A preset is a plain Node
|
|
257
|
+
module: whatever you can call from Node you can run as a build step. No DSL,
|
|
258
|
+
no constrained options bag, no vendor pipeline to fight with. Image resizing
|
|
259
|
+
with sharp, video transcoding with ffmpeg, AI upscaling via Replicate,
|
|
260
|
+
watermarking with canvas — each is a preset. The plugin does not decide what
|
|
261
|
+
is possible; the preset does.
|
|
256
262
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
#### Config
|
|
260
|
-
|
|
261
|
-
```js
|
|
262
|
-
assets: {
|
|
263
|
-
assetsFolder: 'assets', // working folder for presets — default 'assets'
|
|
264
|
-
outputFolder: 'public', // where the processed outputs land — default root
|
|
265
|
-
|
|
266
|
-
// Each preset name maps to a list of source globs. A source matches
|
|
267
|
-
// multiple presets if needed (one image → thumbnail + hero + og-card).
|
|
268
|
-
presets: {
|
|
269
|
-
thumbnail: ['/files/images/*.{jpg,png}', '/resources/**/*.{jpg,png}'],
|
|
270
|
-
hero: ['/files/images/hero-*.jpg'],
|
|
271
|
-
'video-web': ['/files/videos/*.mp4', '/resources/**/*.mp4'],
|
|
272
|
-
'image-2x': ['/files/images/*.jpg'],
|
|
273
|
-
upscaled: ['/files/photos/raw-*.jpg'],
|
|
274
|
-
},
|
|
275
|
-
}
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
`presets/<name>.js` next to your `mikser.config.js` is the preset module for that name.
|
|
279
|
-
|
|
280
|
-
#### Where a preset comes from
|
|
281
|
-
|
|
282
|
-
A preset name resolves in two places, local first:
|
|
283
|
-
|
|
284
|
-
1. **`presets/<name>.js`** in your project — the common case.
|
|
285
|
-
2. **An npm package `mikser-io-preset-<name>`** — when no local file exists, the plugin resolves the name from your project's `node_modules`. Install a shared preset (`npm install mikser-io-preset-thumbnail`) and reference it by name in `assets.presets` with no local file. Same resolution convention as `post-*` plugins.
|
|
286
|
-
|
|
287
|
-
A local file always wins over an npm package of the same name — drop `presets/thumbnail.js` to override one preset from a package while leaving the rest. The two have different update lifetimes: local presets reload on file change in watch mode; npm presets are versioned by their package (bump the dependency to update). `node_modules` is never watched.
|
|
288
|
-
|
|
289
|
-
If a configured preset name resolves to neither a local file nor an npm package, the plugin logs `Preset not found: <name> ...` and skips it — the rest of the build proceeds.
|
|
290
|
-
|
|
291
|
-
#### Preset module shape
|
|
292
|
-
|
|
293
|
-
A preset is a default-exported async function. It receives the entity being processed (with `source`, `destination`, `preset`, `name`, etc.), runs whatever code it needs, and resolves (or rejects) when done.
|
|
294
|
-
|
|
295
|
-
```js
|
|
296
|
-
export const revision = 1 // bump to force re-render (cache-bust)
|
|
297
|
-
export const format = 'webp' // output format hint (used in the destination filename)
|
|
298
|
-
|
|
299
|
-
export default async ({ entity, runtime, logger }) => {
|
|
300
|
-
// entity.source — input file on disk
|
|
301
|
-
// entity.destination — where to write the result
|
|
302
|
-
// entity.preset — config of the matching preset (name, source, options)
|
|
303
|
-
// entity.name — original entity name (e.g. '/files/images/hero.jpg')
|
|
304
|
-
// runtime / logger — mikser context, including runtime.options for paths
|
|
305
|
-
}
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
That's the whole contract. Three real examples follow.
|
|
309
|
-
|
|
310
|
-
#### Example 1 — Video transcoding via ffmpeg
|
|
311
|
-
|
|
312
|
-
A 720×1080 portrait MP4 at 600kbps for the web. fluent-ffmpeg streams progress events back into mikser's logger so the build progress bar reflects encoder progress in real time.
|
|
313
|
-
|
|
314
|
-
```js
|
|
315
|
-
// presets/video-web.js
|
|
316
|
-
import ffmpeg from 'fluent-ffmpeg'
|
|
317
|
-
|
|
318
|
-
export const revision = 7
|
|
319
|
-
export const format = 'mp4'
|
|
320
|
-
|
|
321
|
-
export default ({ entity: { name, source, destination, preset }, logger }) => {
|
|
322
|
-
return new Promise((resolve, reject) => {
|
|
323
|
-
ffmpeg(source)
|
|
324
|
-
.videoCodec('libx264')
|
|
325
|
-
.size('810x1080')
|
|
326
|
-
.videoBitrate(600)
|
|
327
|
-
.outputOptions('-strict -2')
|
|
328
|
-
.on('progress', ({ percent }) =>
|
|
329
|
-
logger.trace(`Progress: [${preset.name}] ${name} ${Math.round(percent)}%`))
|
|
330
|
-
.on('error', reject)
|
|
331
|
-
.on('end', resolve)
|
|
332
|
-
.save(destination)
|
|
333
|
-
})
|
|
334
|
-
}
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
10 lines of glue around ffmpeg. Every published video in the catalog gets transcoded; rebuilds skip unchanged inputs because the journal tracks file mtimes; bumping `revision` re-encodes everything (useful when you change the bitrate).
|
|
338
|
-
|
|
339
|
-
#### Example 2 — Image variants via sharp
|
|
340
|
-
|
|
341
|
-
Resize + format negotiation. Most projects want srcset variants in WebP and AVIF; this preset emits both with a single sharp pipeline.
|
|
342
|
-
|
|
343
|
-
```js
|
|
344
|
-
// presets/image-2x.js
|
|
345
|
-
import sharp from 'sharp'
|
|
346
|
-
import { dirname, basename, extname, join } from 'node:path'
|
|
347
|
-
import { mkdir } from 'node:fs/promises'
|
|
348
|
-
|
|
349
|
-
export const revision = 3
|
|
350
|
-
|
|
351
|
-
export default async ({ entity: { source, destination }, logger }) => {
|
|
352
|
-
const dir = dirname(destination)
|
|
353
|
-
const stem = basename(destination, extname(destination))
|
|
354
|
-
await mkdir(dir, { recursive: true })
|
|
355
|
-
|
|
356
|
-
const pipeline = sharp(source).rotate() // honor EXIF orientation
|
|
357
|
-
// 2× variants for each format
|
|
358
|
-
await Promise.all([
|
|
359
|
-
pipeline.clone().resize({ width: 1600 }).webp({ quality: 85 }).toFile(join(dir, `${stem}@2x.webp`)),
|
|
360
|
-
pipeline.clone().resize({ width: 1600 }).avif({ quality: 60 }).toFile(join(dir, `${stem}@2x.avif`)),
|
|
361
|
-
pipeline.clone().resize({ width: 800 }).webp({ quality: 85 }).toFile(join(dir, `${stem}.webp`)),
|
|
362
|
-
pipeline.clone().resize({ width: 800 }).avif({ quality: 60 }).toFile(join(dir, `${stem}.avif`)),
|
|
363
|
-
])
|
|
364
|
-
logger.trace('image-2x emitted 4 variants for %s', source)
|
|
365
|
-
}
|
|
263
|
+
```bash
|
|
264
|
+
npm install mikser-io-assets
|
|
366
265
|
```
|
|
367
266
|
|
|
368
|
-
One preset, four output files per input image. The render-href plugin can then rewrite `<img src="hero.jpg">` to the `@2x.webp` URL with a fallback `<source>` chain — but that's a render-time concern, not the asset plugin's.
|
|
369
|
-
|
|
370
|
-
#### Example 3 — AI enhancement via Replicate
|
|
371
|
-
|
|
372
|
-
The interesting one. Hand a raw photo to a model on Replicate (here, an image upscaler), poll for completion, fetch the result, write it to disk. The preset is a normal Node module — `fetch` is just `fetch`, no special bridging.
|
|
373
|
-
|
|
374
267
|
```js
|
|
375
|
-
|
|
376
|
-
import { writeFile } from 'node:fs/promises'
|
|
377
|
-
|
|
378
|
-
export const revision = 2
|
|
379
|
-
export const format = 'jpg'
|
|
380
|
-
|
|
381
|
-
const REPLICATE_MODEL = 'nightmareai/real-esrgan'
|
|
382
|
-
const REPLICATE_VERSION = '...' // pin a model version
|
|
383
|
-
|
|
384
|
-
export default async ({ entity: { source, destination }, logger }) => {
|
|
385
|
-
// 1. Upload source — Replicate accepts a public URL or base64 data URI
|
|
386
|
-
const data = await readFile(source)
|
|
387
|
-
const dataUri = `data:image/jpeg;base64,${data.toString('base64')}`
|
|
388
|
-
|
|
389
|
-
// 2. Kick off the prediction
|
|
390
|
-
const start = await fetch('https://api.replicate.com/v1/predictions', {
|
|
391
|
-
method: 'POST',
|
|
392
|
-
headers: {
|
|
393
|
-
'authorization': `Token ${process.env.REPLICATE_TOKEN}`,
|
|
394
|
-
'content-type': 'application/json',
|
|
395
|
-
},
|
|
396
|
-
body: JSON.stringify({
|
|
397
|
-
version: REPLICATE_VERSION,
|
|
398
|
-
input: { image: dataUri, scale: 4 },
|
|
399
|
-
}),
|
|
400
|
-
}).then(r => r.json())
|
|
401
|
-
|
|
402
|
-
// 3. Poll until succeeded
|
|
403
|
-
let prediction = start
|
|
404
|
-
while (prediction.status === 'starting' || prediction.status === 'processing') {
|
|
405
|
-
await new Promise(r => setTimeout(r, 1500))
|
|
406
|
-
prediction = await fetch(start.urls.get, {
|
|
407
|
-
headers: { authorization: `Token ${process.env.REPLICATE_TOKEN}` },
|
|
408
|
-
}).then(r => r.json())
|
|
409
|
-
logger.trace('replicate %s: %s', start.id, prediction.status)
|
|
410
|
-
}
|
|
411
|
-
if (prediction.status !== 'succeeded') {
|
|
412
|
-
throw new Error(`Replicate failed: ${prediction.error}`)
|
|
413
|
-
}
|
|
268
|
+
import { assets, renderPreset, assetUrlHelper } from 'mikser-io-assets'
|
|
414
269
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
270
|
+
plugins: [
|
|
271
|
+
assets({ presets: { web: { match: ['/files/media/**'] } } }),
|
|
272
|
+
renderPreset(), // the renderer presets dispatch through
|
|
273
|
+
assetUrlHelper(), // runtime.asset() for templates
|
|
274
|
+
]
|
|
419
275
|
```
|
|
420
276
|
|
|
421
|
-
|
|
277
|
+
`renderPreset()` is not optional. It was resolved implicitly while this code
|
|
278
|
+
lived in the engine, whose renderer lookup falls back to a path inside its own
|
|
279
|
+
`src/plugins/render/` — that path no longer holds it.
|
|
422
280
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
export const revision = 1
|
|
430
|
-
export default async ({ entity: { source, destination } }) => {
|
|
431
|
-
const upscaled = await upscaleViaReplicate(source)
|
|
432
|
-
const watermark = await applyWatermark(upscaled, { text: '© Acme Co 2026' })
|
|
433
|
-
await sharp(watermark).resize({ width: 2400 }).avif({ quality: 70 }).toFile(destination)
|
|
434
|
-
}
|
|
435
|
-
```
|
|
436
|
-
|
|
437
|
-
This is the property no other SSG / CMS exposes at this level. Astro's `<Image>` resizes; that's it. Hugo's image processing has a fixed set of operations. Sanity's image CDN runs the transforms it decided to support. mikser's preset is whatever you write — including operations that don't exist anywhere else (upscaling specific to your domain, watermarks driven by per-image rules, multi-format outputs with vendor-specific encoders).
|
|
438
|
-
|
|
439
|
-
#### Composition with `resources`
|
|
440
|
-
|
|
441
|
-
The assets plugin processes whatever's on disk. Source files don't have to start in your repo — the **resources plugin** (next section) pulls them from external systems (company content servers, S3, vendor APIs) into the working folder so assets can then process them. That composition is where the "advanced pipeline" idea pays off — see the end-of-section example for the full chain.
|
|
442
|
-
|
|
443
|
-
#### Watch support
|
|
281
|
+
Split out of core in 10.12.0: it encodes one recipe for producing files from
|
|
282
|
+
files, the way `layouts` encodes one for producing render tasks. What stayed
|
|
283
|
+
in the engine is substrate — the render track that records which asset URLs a
|
|
284
|
+
template asked for, and the finalize check that reports the ones nothing
|
|
285
|
+
produced, both of which work whether or not this package is installed.
|
|
444
286
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
deleting a file removed its catalog row and its published copy and left the
|
|
450
|
-
derived file, because the delete handler dropped the in-memory mapping and
|
|
451
|
-
nothing on disk. Narrowing a preset's `match` left one the same way. A stale
|
|
452
|
-
derivative passes every check — the url resolves and the bytes are there —
|
|
453
|
-
so the only cleanup was `--clear`, or deleting it by hand.
|
|
454
|
-
|
|
455
|
-
The post-cycle pass that already walks the revision markers now also asks, per
|
|
456
|
-
derivative, whether it still has a source that this preset still covers. It is
|
|
457
|
-
answered from the **catalog**, not from the delete event: a delete entry is
|
|
458
|
-
sparse (`{ id, type, collection }`) in every source plugin, so it cannot say
|
|
459
|
-
where the derivative went, while the catalog answers whatever route the orphan
|
|
460
|
-
arrived by — including ones that predate the fix. Reported as
|
|
461
|
-
`Assets removed: N derivative(s) with no source`.
|
|
462
|
-
|
|
463
|
-
It does nothing when the catalog is empty. Every check concludes "no source,
|
|
464
|
-
therefore orphan", and an empty catalog answers that for every derivative on
|
|
465
|
-
the site, so a failed import would otherwise delete the whole assets folder.
|
|
466
|
-
|
|
467
|
-
---
|
|
287
|
+
See the [mikser-io-assets README](https://github.com/almero-digital-marketing/mikser-io-assets#readme)
|
|
288
|
+
for the full reference: preset module shape, `revision` and the
|
|
289
|
+
completed-render marker, recovery from an interrupted derive, `auditIgnore`,
|
|
290
|
+
`--render-presets`, and worked ffmpeg / sharp examples.
|
|
468
291
|
|
|
469
292
|
### `resources`
|
|
470
293
|
|
|
@@ -527,7 +350,8 @@ With this config:
|
|
|
527
350
|
|
|
528
351
|
```js
|
|
529
352
|
// mikser.config.js
|
|
530
|
-
import { files, resources,
|
|
353
|
+
import { files, resources, api } from 'mikser-io'
|
|
354
|
+
import { assets, renderPreset } from 'mikser-io-assets'
|
|
531
355
|
|
|
532
356
|
export default {
|
|
533
357
|
plugins: [
|
package/docs/rendering.md
CHANGED