mikser-io 9.61.1 → 9.63.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
@@ -233,7 +233,14 @@ brevity.
233
233
  - `{ name, options, postprocess, output?, setup?, teardown? }` →
234
234
  postprocessor descriptor; stored in `runtime.postprocessors`.
235
235
  Strings produce a v9 migration error pointing at the new shape.
236
- - `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.
237
244
  - `source.js` — `useSource` codifies the folder-of-files pattern.
238
245
  - `tools.js` — tool registry. `registerTool(name, {description,
239
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.1",
3
+ "version": "9.63.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": {
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
 
package/src/server.js CHANGED
@@ -22,11 +22,25 @@
22
22
 
23
23
  import path from 'node:path'
24
24
  import { fileURLToPath } from 'node:url'
25
+ import { networkInterfaces } from 'node:os'
25
26
 
26
27
  import runtime from './runtime.js'
27
28
  import { useLogger } from './engine.js'
28
29
  import { onInitialized, onLoad, onLoaded } from './lifecycle.js'
29
30
 
31
+ // Every non-loopback IPv4 address this machine answers on.
32
+ //
33
+ // IPv4 only, deliberately. The non-internal IPv6 addresses on a typical
34
+ // machine are link-local (fe80::), which need a zone index to be usable and
35
+ // are not what anyone is going to type into a phone — listing them would make
36
+ // the useful lines harder to find, which is the opposite of the point.
37
+ export function localAddresses() {
38
+ return Object.values(networkInterfaces())
39
+ .flat()
40
+ .filter(iface => iface && iface.family === 'IPv4' && !iface.internal)
41
+ .map(iface => iface.address)
42
+ }
43
+
30
44
  // Extend the engine's commander with server-related flags. Called by
31
45
  // engine.js's onInitialize callback AFTER engine's own options chain
32
46
  // and BEFORE the .parse() call — preserves the order of options in
@@ -214,6 +228,28 @@ export function setupServer() {
214
228
  // back to the bind URL when no public origin is set.
215
229
  const externalUrl = runtime.options.url ?? `http://localhost:${runtime.options.port}`
216
230
  logger.info('Server listening: %s', externalUrl)
231
+
232
+ // The addresses that are NOT localhost.
233
+ //
234
+ // listen(port) binds every interface, so the server is
235
+ // already reachable from a phone or a second machine —
236
+ // and the one address printed above is the single one
237
+ // that will not work from either. Anyone testing a
238
+ // layout on a real device has to go and find their own
239
+ // IP, every time, on a machine that already knows it.
240
+ //
241
+ // Only when no public URL is configured. Behind a proxy
242
+ // or a tunnel the LAN address is not how the site is
243
+ // reached, and offering it would be an invitation to
244
+ // debug the wrong origin.
245
+ if (!runtime.options.url) {
246
+ const lan = localAddresses()
247
+ if (lan.length) {
248
+ logger.info('Also on: %s', lan
249
+ .map(address => `http://${address}:${runtime.options.port}`)
250
+ .join(' '))
251
+ }
252
+ }
217
253
  resolve()
218
254
  })
219
255