mikser-io 9.61.0 → 9.62.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/CLAUDE.md CHANGED
@@ -195,6 +195,11 @@ brevity.
195
195
  over ids, not the matched paths, so a file appearing later still
196
196
  invalidates. Paths resolve against `options.workingFolder` — the
197
197
  render-time `runtime` is a per-render projection with no options on it.
198
+ The "this file has no entity" warning reads `options.sourceFolders` —
199
+ the set useSource records as it registers each collection — NOT a list
200
+ of folder names written in the plugin. A hardcoded list misses every
201
+ collection a project registers through `sources()`, which produced 63
202
+ false warnings per build on a real site.
198
203
  - `render.js` / `postprocess.js` — Piscina worker entry points AND the
199
204
  default-export functions the INLINE/SERIAL dispatcher calls directly.
200
205
  Each receives entity + options + config + state; the WORKER path also
@@ -228,7 +233,14 @@ brevity.
228
233
  - `{ name, options, postprocess, output?, setup?, teardown? }` →
229
234
  postprocessor descriptor; stored in `runtime.postprocessors`.
230
235
  Strings produce a v9 migration error pointing at the new shape.
231
- - `manager.js` — file watching (chokidar) and cron scheduling.
236
+ - `manager.js` — file watching (chokidar) and cron scheduling. `watch()`
237
+ turns file events into SYNC events — it is how a source folder becomes
238
+ entities, so pointing it at the output folder feeds output back in as
239
+ input. `watchFolder(folder, handler, options)` is the primitive without
240
+ that meaning, for a plugin that only needs to know bytes changed; it
241
+ carries the shared junk filter and `followSymlinks: true`, which is
242
+ load-bearing because the files plugin serves by symlinking into the
243
+ output folder.
232
244
  - `source.js` — `useSource` codifies the folder-of-files pattern.
233
245
  - `tools.js` — tool registry. `registerTool(name, {description,
234
246
  inputSchema}, handler)` / `toolNames()` / `toolSchema()` / `invokeTool()`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mikser-io",
3
- "version": "9.61.0",
3
+ "version": "9.62.0",
4
4
  "description": "A mixer for content: entities in, configurable render pipelines, outputs of any kind. Static sites are the canonical recipe, not the definition — the same engine renders PDFs, emails and whatever a renderer plugin produces. Files are the source of truth, every lifecycle phase is observable, and the build graph is queryable by an agent.",
5
5
  "main": "index.js",
6
6
  "exports": {
@@ -331,9 +331,9 @@ export function createSqliteDatabase({
331
331
  configChanged ? 'config' : 'schema version')
332
332
  } else if (configChanged && !(recorded && recorded !== version)) {
333
333
  logger?.warn(
334
- 'Config changed since the last run. Wiping the cache and rebuilding from sources ' +
335
- '(files are the source of truth — no source data is affected). Note this tracks the ' +
336
- 'bytes of %s only: a change in a module it imports is not seen.',
334
+ 'Config changed since the last run. Wiping the cache and rebuilding from sources '
335
+ + '(files are the source of truth — no source data is affected). The stamp covers %s and '
336
+ + 'every local module it imports; `--json` reports exactly which files under `config.files`.',
337
337
  runtime.options.config,
338
338
  )
339
339
  }
package/src/manager.js CHANGED
@@ -90,6 +90,33 @@ export async function deletedHook(name, context) {
90
90
  // `ignored` and a function is the one form that has stayed stable.
91
91
  const ignoreJunk = (filePath) => /[/\\]\./.test(filePath) || junkFilter()(filePath)
92
92
 
93
+ // Watch a folder, with mikser's own settings and none of its lifecycle.
94
+ //
95
+ // `watch()` below turns file events into SYNC events — it is how a source
96
+ // folder becomes entities, and pointing it at anything else feeds output back
97
+ // in as input. A plugin that only wants to know when bytes changed needs the
98
+ // watching without the meaning, and was otherwise reaching for chokidar
99
+ // directly: a second copy of a dependency the engine already has, and a second
100
+ // junk filter that would drift from this one.
101
+ //
102
+ // followSymlinks matters more than it looks. The files plugin serves a file by
103
+ // symlinking it from the source folder into the output folder, so a watcher on
104
+ // the output folder that did not follow links would see the link created once
105
+ // and never hear about the file again — every stylesheet edit silently
106
+ // invisible to anything watching what is served.
107
+ export function watchFolder(folder, handler, options = {}) {
108
+ return chokidar
109
+ .watch(folder, {
110
+ interval: 1000,
111
+ binaryInterval: 3000,
112
+ ignored: ignoreJunk,
113
+ ignoreInitial: true,
114
+ followSymlinks: true,
115
+ ...options,
116
+ })
117
+ .on('all', (event, fullPath) => handler(event, fullPath))
118
+ }
119
+
93
120
  export function watch(name, folder, options = { interval: 1000, binaryInterval: 3000, ignored: ignoreJunk, ignoreInitial: true }) {
94
121
  if (runtime.options.watch !== true) return
95
122
 
@@ -67,16 +67,27 @@ function entityIdFor(workingFolder, resolved) {
67
67
  // nothing to track" read identically from a template. Said once per path.
68
68
  const warnedOutside = new Set()
69
69
  function warnIfUntrackable(options, resolved, logger) {
70
- const folders = ['documentsFolder', 'filesFolder', 'assetsFolder', 'resourcesFolder', 'dataFolder']
71
- .map(key => options?.[key]).filter(Boolean)
72
- if (!folders.length) return // nothing configured to compare against
70
+ // Every folder whose files become entities, as recorded by useSource when
71
+ // it registered them — NOT a list written here.
72
+ //
73
+ // The first version of this hardcoded five content folders, and a project
74
+ // registering its own collections through sources() has more than five. On
75
+ // lmed that meant 63 warnings per build, one for every stylesheet and
76
+ // script, all of them tracked correctly and every one of them saying the
77
+ // opposite. Which is worse than not warning: 63 spurious lines a build
78
+ // teaches you to filter the channel, and the filtered-out line is the real
79
+ // one.
80
+ const folders = Object.values(options?.sourceFolders ?? {})
81
+ // Nothing registered yet means nothing can be concluded. Silence is the
82
+ // only honest answer — the previous shape guessed instead.
83
+ if (!folders.length) return
73
84
  if (folders.some(folder => !path.relative(folder, resolved).startsWith('..'))) return
74
85
  if (warnedOutside.has(resolved)) return
75
86
  warnedOutside.add(resolved)
76
87
  logger?.warn?.({ code: 'untracked-file-read' },
77
- 'A template read %s, which is outside every content folder — so it has no entity, nothing watches it, '
78
- + 'and changing it will NOT rebuild the pages that read it. Move it under a content folder if that '
79
- + 'matters, or pass { track: false } to say the staleness is intended.', resolved)
88
+ 'A template read %s, which is outside every folder mikser takes entities from — so it has no entity, '
89
+ + 'nothing watches it, and changing it will NOT rebuild the pages that read it. Register the folder '
90
+ + 'with sources() if that matters, or pass { track: false } to say the staleness is intended.', resolved)
80
91
  }
81
92
 
82
93
  export function load({ runtime, options, track, logger }) {
package/src/source.js CHANGED
@@ -328,6 +328,18 @@ export function useSource(core, options) {
328
328
  ? folder
329
329
  : path.join(runtime.options.workingFolder, folder)
330
330
  runtime.options[`${collection}Folder`] = absFolder
331
+ // The authoritative set of folders whose files become entities.
332
+ //
333
+ // `<collection>Folder` above is the per-collection accessor and reads
334
+ // like one; this is the LIST, which is a different question and the
335
+ // one anything asking "could a file here be tracked?" needs. Deriving
336
+ // it by scanning options for a `*Folder` suffix would sweep up
337
+ // workingFolder, runtimeFolder and outputFolder, and hand-listing the
338
+ // content ones misses every collection a project registers itself —
339
+ // which is exactly how the file helpers came to warn 63 times a build
340
+ // about files they were tracking correctly.
341
+ runtime.options.sourceFolders ??= {}
342
+ runtime.options.sourceFolders[collection] = absFolder
331
343
  logger.debug('%s folder: %s', cap, absFolder)
332
344
 
333
345
  await mkdir(absFolder, { recursive: true })