mikser-io 9.14.0 → 9.15.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/index.js CHANGED
@@ -45,9 +45,18 @@ export { yaml } from './src/plugins/yaml.js'
45
45
  // loader stores in `runtime.renderers`; the same module also still
46
46
  // exports `load`/`render` at the top level so Piscina worker dispatch
47
47
  // can resolve via dynamic import. ADR-0010.
48
- export { renderAsset } from './src/plugins/render/asset.js'
49
- export { renderFile } from './src/plugins/render/file.js'
50
- export { renderHbs } from './src/plugins/render/hbs.js'
51
- export { renderHref } from './src/plugins/render/href.js'
52
- export { renderPreset } from './src/plugins/render/preset.js'
53
- export { renderResource } from './src/plugins/render/resource.js'
48
+ // Renderers — these have a render() and turn an entity into a file.
49
+ export { renderHbs } from './src/plugins/render/hbs.js'
50
+ export { renderPreset } from './src/plugins/render/preset.js'
51
+
52
+ // Template helpers — these only install functions on `runtime` for templates
53
+ // to call. They render nothing, which is what the old render* names hid: two
54
+ // of the six factories in this folder are renderers and four are not, and
55
+ // naming all six after the object they concern rather than the job they do
56
+ // led someone to add renderPreset() expecting a helper and watch every page
57
+ // render throw. Renamed in 10.0.0; there are no aliases.
58
+ export { assetUrlHelper } from './src/plugins/render/asset.js'
59
+ export { hrefUrlHelpers } from './src/plugins/render/href.js'
60
+ export { resourceUrlHelper } from './src/plugins/render/resource.js'
61
+ export { fileHelpers } from './src/plugins/render/file.js'
62
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mikser-io",
3
- "version": "9.14.0",
3
+ "version": "9.15.1",
4
4
  "description": "<p align=\"center\"> <img src=\"mikser-lockup-stacked.svg\" alt=\"mikser\" width=\"198\" /> </p>",
5
5
  "main": "index.js",
6
6
  "exports": {
package/src/manifest.js CHANGED
@@ -440,6 +440,22 @@ export function createManifest(db) {
440
440
  })
441
441
  }
442
442
  }
443
+ if (track?.lookups) {
444
+ for (const target of track.lookups) {
445
+ // findById is extension-tolerant and also resolves a
446
+ // meta.href, so the hash is the resolved entity's when
447
+ // there is one. No hash means "nothing resolved" —
448
+ // shouldSkip treats a hashless edge whose target mutated
449
+ // as a re-render, which is what should happen when a page
450
+ // that was linked-to-but-missing finally appears.
451
+ const resolved = findById(target)
452
+ edges.push({
453
+ kind: 'lookup',
454
+ target,
455
+ hash: resolved ? inputHashOf(resolved) : undefined,
456
+ })
457
+ }
458
+ }
443
459
  if (track?.queries) {
444
460
  for (const filter of track.queries) {
445
461
  edges.push({ kind: 'query', filter })
@@ -10,6 +10,6 @@ export function load({ runtime, entity, state, options }) {
10
10
  }
11
11
  }
12
12
 
13
- export function renderAsset(options = {}) {
13
+ export function assetUrlHelper(options = {}) {
14
14
  return { name: options.name ?? 'asset', options, load }
15
- }
15
+ }
@@ -44,6 +44,6 @@ export function load({ runtime }) {
44
44
  }
45
45
  }
46
46
 
47
- export function renderFile(options = {}) {
47
+ export function fileHelpers(options = {}) {
48
48
  return { name: options.name ?? 'file', options, load }
49
49
  }
@@ -41,6 +41,6 @@ export function load({ entity, runtime, options }) {
41
41
  runtime.next = entity.page + 1 < entity.pages ? entity.page + 1 : false
42
42
  }
43
43
 
44
- export function renderHref(options = {}) {
44
+ export function hrefUrlHelpers(options = {}) {
45
45
  return { name: options.name ?? 'href', options, load }
46
- }
46
+ }
@@ -2,7 +2,7 @@ import { mkdir } from 'node:fs/promises'
2
2
  import path from 'node:path'
3
3
 
4
4
  // A renderer's `load` runs for EVERY entity in the cycle, not only the ones
5
- // this renderer will render — that is deliberate and is how renderAsset
5
+ // this renderer will render — that is deliberate and is how assetUrlHelper
6
6
  // installs runtime.asset() for all templates. So this has to tolerate an
7
7
  // entity that has no preset, rather than assume it is looking at one.
8
8
  //
@@ -11,13 +11,12 @@ import path from 'node:path'
11
11
  // "you have found something real", which is a much more expensive wrong
12
12
  // signal than a no-op. The names invite exactly that mistake:
13
13
  //
14
- // renderAsset() provides runtime.asset() to templates (a URL helper)
14
+ // assetUrlHelper() provides runtime.asset() to templates (a URL helper)
15
15
  // assets() runs presets and produces derivatives (the work)
16
16
  // renderPreset() renders a preset-authored layout (this file)
17
17
  //
18
- // All three are named after the object they concern rather than the job they
19
- // do, so reasoning "the one that RUNS presets must be renderPreset" is wrong
20
- // but not unreasonable.
18
+ // The helpers were renamed for their role in 10.0.0 for exactly this reason;
19
+ // renderPreset keeps its name because it really is a renderer.
21
20
  export async function load({ entity, runtime }) {
22
21
  if (!entity?.preset?.uri) return
23
22
  const preset = await import(`${entity.preset.uri}?stamp=${Date.now()}`)
@@ -16,6 +16,6 @@ export function load({ runtime, entity, state, options }) {
16
16
  }
17
17
  }
18
18
 
19
- export function renderResource(options = {}) {
19
+ export function resourceUrlHelper(options = {}) {
20
20
  return { name: options.name ?? 'resource', options, load }
21
- }
21
+ }
package/src/render.js CHANGED
@@ -187,10 +187,28 @@ export default async ({ entity, options, config, context, state, logger, port, t
187
187
  // state.layouts.sitemap map that was serialized per render
188
188
  // task. Goes through the same WAL-backed read-only handle
189
189
  // every worker shares with the engine writer.
190
- lookupHref: lookupHrefViaDb,
190
+ // Both lookups RECORD what they were asked for, via track.lookup.
191
+ //
192
+ // They read the catalog directly, and until this they told nobody —
193
+ // so nothing knew a page depended on the page it links to. Renaming
194
+ // a target left every page linking to it pointing at a file that no
195
+ // longer existed, on a green build, because manifest.shouldSkip had
196
+ // no edge to check. A sidecar's findEntities() was tracked all along;
197
+ // these two were the asymmetry.
198
+ //
199
+ // The recorded target is the string the template asked for, not the
200
+ // resolved id — see track.lookup for why that also covers a target that
201
+ // does not exist yet.
202
+ lookupHref: (href) => {
203
+ track?.lookup?.(href)
204
+ return lookupHrefViaDb(href)
205
+ },
191
206
  // Resolve a served-entity reference to its deployed URL, absolute
192
207
  // when runtime.options.url is set (ADR-0011).
193
- lookupUrl: (ref, preset) => lookupUrlViaDb(ref, preset, options.url),
208
+ lookupUrl: (ref, preset) => {
209
+ if (typeof ref === 'string') track?.lookup?.(ref)
210
+ return lookupUrlViaDb(ref, preset, options.url)
211
+ },
194
212
  content() {
195
213
  return readFileSync(entity.source, { encoding: 'utf8' })
196
214
  },
package/src/track.js CHANGED
@@ -40,8 +40,36 @@ export function filterKey(filter) {
40
40
  // The returned object exposes `partials: Set<string>` and
41
41
  // `queries: Array<filter | null>` directly. Consumers iterate either
42
42
  // shape; both are owned by the track for the lifetime of the run.
43
- export function createTrack({ partial = true, query = true } = {}) {
43
+ export function createTrack({ partial = true, query = true, lookup = true } = {}) {
44
44
  const track = {}
45
+ if (lookup) {
46
+ // Lookups a TEMPLATE made by name: runtime.href('/contacts'),
47
+ // runtime.lookupUrl('/media/clip.mp4'). Both read the catalog
48
+ // directly, and until this existed neither told anyone — so nothing
49
+ // recorded that a page depends on the page it links to.
50
+ //
51
+ // Measured consequence: rename contacts.md to contact-us.md and only
52
+ // the renamed page re-renders. Every page linking to it keeps a href
53
+ // pointing at a file that no longer exists, on a green build.
54
+ //
55
+ // The edge kind is 'lookup', NOT 'ref': mikser_refs divides ownership
56
+ // by kind — indexEntity owns kind='ref' (static frontmatter $-refs)
57
+ // and clears it per source, while replaceDynamic owns everything
58
+ // else. Writing these as 'ref' got them inserted by replaceDynamic
59
+ // and then wiped by the next indexEntity, so the edge existed in
60
+ // the manifest and never in the refs index — recorded, and still
61
+ // never scheduling a re-render.
62
+ //
63
+ // The target is the STRING the template asked for, not the resolved
64
+ // entity's id, and that is deliberate: lookupKeys() expands a mutated
65
+ // entity into its id, its meta.href AND its id-minus-extension, so an
66
+ // edge on '/contacts' fires whether the target was edited, renamed,
67
+ // deleted, or created for the first time. Recording the resolved id
68
+ // instead would miss the case where nothing resolved yet.
69
+ const lookups = new Set()
70
+ track.lookups = lookups
71
+ track.lookup = (target) => { if (target && typeof target === 'string') lookups.add(target) }
72
+ }
45
73
  if (partial) {
46
74
  const partials = new Set()
47
75
  track.partials = partials