mikser-io 8.3.0 → 8.3.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/README.md +2 -2
- package/package.json +1 -1
- package/src/engine.js +35 -13
- package/src/manifest.js +15 -0
package/README.md
CHANGED
|
@@ -208,8 +208,8 @@ For a working starter — config with a real plugin set, sample `documents/`, ex
|
|
|
208
208
|
The shape mikser fits cleanly:
|
|
209
209
|
|
|
210
210
|
- **Marketing sites with editorial teams** — content authors work in files (via their editor, a Git client, or `mikser-io-decap`), engineers ship features without negotiating with a CMS schema, the site stays portable.
|
|
211
|
-
- **Multilingual publishing platforms** — the `useHref()` / `useAlternates()` pattern
|
|
212
|
-
- **Content-heavy product catalogues** — `documents` + `mikser-io-schemas` + `data` plugin + a
|
|
211
|
+
- **Multilingual publishing platforms** — the `useHref()` / `useAlternates()` pattern decouples logical references from per-locale URLs. One source tree, many language deployments.
|
|
212
|
+
- **Content-heavy product catalogues** — `documents` + `mikser-io-schemas` + `data` plugin + a Frontend Framework = typed product listings with live updates, semantic search via `vector`, and static-CDN-friendly JSON snapshots all at once.
|
|
213
213
|
- **AI-augmented media pipelines** — `assets` plugin presets call out to Replicate / OpenAI / local models to upscale images, transcribe audio, transcode video. The pipeline is JS code, so anything Node can do is in scope.
|
|
214
214
|
- **Mixed-output publishing** — the same source document renders to HTML, PDF (via `post-pdf`), MJML email (via `post-mjml`), and JSON snapshots. One catalog, many output formats, all concurrent.
|
|
215
215
|
- **Headless backends for static frontends** — pair the `api` plugin with `sdk-api` for SSE-driven live frontends; pair the `data` plugin output with any static host for pre-rendered consumption.
|
package/package.json
CHANGED
package/src/engine.js
CHANGED
|
@@ -9,7 +9,7 @@ import { onInitialize, onInitialized, onRender, onCancel, onCancelled, onFinaliz
|
|
|
9
9
|
import { useJournal, updateEntry } from './journal.js'
|
|
10
10
|
import { globby } from 'globby'
|
|
11
11
|
import { OPERATION, TASKS } from './constants.js'
|
|
12
|
-
import { changeExtension, formatErrorContext, projectMeta } from './utils.js'
|
|
12
|
+
import { changeExtension, formatErrorContext, projectMeta, lookupKeys } from './utils.js'
|
|
13
13
|
import render from './render.js'
|
|
14
14
|
import postprocess, { loadPlugin as loadPostPlugin } from './postprocess.js'
|
|
15
15
|
import map from 'p-map'
|
|
@@ -221,8 +221,16 @@ export async function setup(options) {
|
|
|
221
221
|
// cycle's CREATE/UPDATE/DELETE journal entries — RENDER entries
|
|
222
222
|
// are this cycle's work, not the trigger.
|
|
223
223
|
//
|
|
224
|
-
// - `mutatedRefs` is a Set
|
|
225
|
-
//
|
|
224
|
+
// - `mutatedRefs` is a Map<key, Set<lang|null>>: the keys are
|
|
225
|
+
// the ids / hrefs / id-minus-extension forms a refClosure
|
|
226
|
+
// entry might target; the values are the set of languages
|
|
227
|
+
// that touched that key this cycle (null for entities with
|
|
228
|
+
// no meta.lang). The Map shape preserves the fast `.has(key)`
|
|
229
|
+
// membership check the existing skip logic relied on AND
|
|
230
|
+
// adds language information so multilingual sites don't
|
|
231
|
+
// over-invalidate. When a French author entity changes,
|
|
232
|
+
// English posts that reference the same /authors/<name>
|
|
233
|
+
// href don't re-render — the lang sets disagree.
|
|
226
234
|
// - `currentHashes` carries the current input hash for each
|
|
227
235
|
// mutated entity. Cold-start file discovery emits CREATE for
|
|
228
236
|
// every file even when content didn't change; without the
|
|
@@ -231,26 +239,40 @@ export async function setup(options) {
|
|
|
231
239
|
// - `mutatedEntities` carries the entity payloads themselves so
|
|
232
240
|
// the query-match check can call `sift(filter)` against each
|
|
233
241
|
// mutation to decide whether a stored query dep is hit.
|
|
234
|
-
const mutatedRefs = new
|
|
242
|
+
const mutatedRefs = new Map()
|
|
235
243
|
const currentHashes = new Map()
|
|
236
244
|
const mutatedEntities = new Map()
|
|
237
245
|
for await (let { entity, operation } of useJournal('Manifest mutations', [OPERATION.CREATE, OPERATION.UPDATE, OPERATION.DELETE])) {
|
|
238
246
|
if (!entity?.id) continue
|
|
239
|
-
mutatedRefs.add(entity.id)
|
|
240
|
-
if (entity.meta?.href) mutatedRefs.add(entity.meta.href)
|
|
241
247
|
mutatedEntities.set(entity.id, entity)
|
|
248
|
+
const hash = operation === OPERATION.DELETE ? null : inputHashOf(entity)
|
|
249
|
+
const lang = entity.meta?.lang ?? null
|
|
250
|
+
// Expand the mutated entity into every form a refClosure
|
|
251
|
+
// entry might target — id, meta.href, AND id-minus-extension
|
|
252
|
+
// — via lookupKeys. Without the stripped form, a refClosure
|
|
253
|
+
// recorded against the natural author/blog-post pattern
|
|
254
|
+
// (`$author: /documents/authors/dick`) would never match
|
|
255
|
+
// the mutated `/documents/authors/dick.yml` here and
|
|
256
|
+
// manifest.shouldSkip would silently return true, pinning
|
|
257
|
+
// the post's output to bytes that reference stale author
|
|
258
|
+
// data. refs.inverseClosureOf and catalog.findEntity both
|
|
259
|
+
// use the same extension-tolerant resolution; the manifest
|
|
260
|
+
// layer has to match.
|
|
261
|
+
//
|
|
262
|
+
// Each key carries the language tag of the mutation so
|
|
263
|
+
// shouldSkip can constrain by language compatibility.
|
|
264
|
+
//
|
|
242
265
|
// For DELETE we set `null` as the current hash so manifest.
|
|
243
266
|
// shouldSkip can distinguish "target was deleted from the
|
|
244
267
|
// catalog" from "target wasn't in this cycle's mutations
|
|
245
268
|
// at all." Without this distinction, a consumer whose
|
|
246
269
|
// refClosure points at a deleted partial/layout would
|
|
247
|
-
// silently skip re-rendering
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
)
|
|
270
|
+
// silently skip re-rendering.
|
|
271
|
+
for (const key of lookupKeys(entity)) {
|
|
272
|
+
if (!mutatedRefs.has(key)) mutatedRefs.set(key, new Set())
|
|
273
|
+
mutatedRefs.get(key).add(lang)
|
|
274
|
+
currentHashes.set(key, hash)
|
|
275
|
+
}
|
|
254
276
|
}
|
|
255
277
|
let skipped = 0
|
|
256
278
|
|
package/src/manifest.js
CHANGED
|
@@ -253,6 +253,7 @@ export function createManifest(db) {
|
|
|
253
253
|
if (!snapshot?.inputHash) return false
|
|
254
254
|
if (inputHashOf(entity) !== snapshot.inputHash) return false
|
|
255
255
|
if (!snapshot.refClosure?.length) return true
|
|
256
|
+
const sourceLang = entity?.meta?.lang ?? null
|
|
256
257
|
for (const entry of snapshot.refClosure) {
|
|
257
258
|
if (entry.kind === 'query') {
|
|
258
259
|
if (!entry.filter) return false
|
|
@@ -264,6 +265,20 @@ export function createManifest(db) {
|
|
|
264
265
|
continue
|
|
265
266
|
}
|
|
266
267
|
if (!mutatedRefs?.has(entry.target)) continue
|
|
268
|
+
// Language scope: if the source has a meta.lang AND the
|
|
269
|
+
// mutation came from a different meta.lang, the ref
|
|
270
|
+
// doesn't actually depend on what changed. A `null` lang
|
|
271
|
+
// in the mutation set means a shared (un-localized)
|
|
272
|
+
// entity touched the same key — that does invalidate
|
|
273
|
+
// language-specific sources because the shared entity
|
|
274
|
+
// is the only variant. Sources without a meta.lang are
|
|
275
|
+
// language-agnostic and accept any mutation lang.
|
|
276
|
+
if (sourceLang) {
|
|
277
|
+
const mutatedLangs = mutatedRefs.get(entry.target)
|
|
278
|
+
if (mutatedLangs && !mutatedLangs.has(sourceLang) && !mutatedLangs.has(null)) {
|
|
279
|
+
continue
|
|
280
|
+
}
|
|
281
|
+
}
|
|
267
282
|
if (!entry.hash) return false
|
|
268
283
|
const currentHash = currentHashes?.get(entry.target)
|
|
269
284
|
if (currentHash === undefined) continue
|