mikser-io 9.73.0 → 9.75.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/app.js +3 -6
- package/docs/diagnostics.md +3 -2
- package/package.json +1 -1
- package/src/engine.js +0 -1
- package/src/instance.js +7 -52
- package/src/manifest.js +29 -0
- package/src/server.js +45 -4
package/app.js
CHANGED
|
@@ -49,8 +49,6 @@ function locate(argv) {
|
|
|
49
49
|
longRunning,
|
|
50
50
|
workingFolder: value('--working-folder', '-i') ?? '.',
|
|
51
51
|
config: value('--config', '-c') ?? 'mikser.config.js',
|
|
52
|
-
// Commander's negated form: `attach` is true unless --no-attach said so.
|
|
53
|
-
attach: has('--no-attach') ? false : true,
|
|
54
52
|
request,
|
|
55
53
|
}
|
|
56
54
|
}
|
|
@@ -62,18 +60,17 @@ async function main() {
|
|
|
62
60
|
// A second server or watcher is the hazard this whole surface exists to
|
|
63
61
|
// remove, and it is the one shape that cannot be answered by forwarding.
|
|
64
62
|
// So it stops, rather than silently doing something else.
|
|
65
|
-
if (where.
|
|
63
|
+
if (where.longRunning) {
|
|
66
64
|
if (await isInstanceLive(workingFolder)) {
|
|
67
65
|
process.stderr.write(
|
|
68
66
|
'mikser: another mikser is already running in this folder, and a server or watcher cannot be '
|
|
69
67
|
+ 'forwarded to it — it would have to open a port on your behalf.\n'
|
|
70
|
-
+ 'Stop that one
|
|
71
|
-
+ 'catalogue and the output tree, with no lock between them).\n')
|
|
68
|
+
+ 'Stop that one first.\n')
|
|
72
69
|
process.exit(1)
|
|
73
70
|
}
|
|
74
71
|
}
|
|
75
72
|
|
|
76
|
-
if (
|
|
73
|
+
if (!where.longRunning) {
|
|
77
74
|
const code = await forward({
|
|
78
75
|
workingFolder,
|
|
79
76
|
config: path.resolve(where.workingFolder, where.config),
|
package/docs/diagnostics.md
CHANGED
|
@@ -760,8 +760,9 @@ Three things it refuses or reports rather than guessing:
|
|
|
760
760
|
to *become* the instance, which is not something a running one can do for
|
|
761
761
|
you — it would have to open a port in your process. They exit 1 and say so,
|
|
762
762
|
rather than building and leaving nothing on the port.
|
|
763
|
-
-
|
|
764
|
-
|
|
763
|
+
There is no opt-out. A flag for running a second engine on a held folder
|
|
764
|
+
only ever enabled the accident this surface prevents, and stopping the
|
|
765
|
+
instance serves every case it was reached for.
|
|
765
766
|
|
|
766
767
|
`--tool`, `--tools`, `--verify` and `--explain` forward as well, and for a
|
|
767
768
|
different reason than builds do. They only read, so running one locally never
|
package/package.json
CHANGED
package/src/engine.js
CHANGED
|
@@ -386,7 +386,6 @@ export async function setup(options) {
|
|
|
386
386
|
runtime.engine.commander?.version(packageInfo.version)
|
|
387
387
|
.option('-i --working-folder <folder>', 'set mikser working folder', './')
|
|
388
388
|
.option('-c --config <file>', 'set mikser mikser.config.js location', './mikser.config.js')
|
|
389
|
-
.option('--no-attach', 'start a private engine instead of forwarding to the one already running here')
|
|
390
389
|
.option('-m --mode <mode>', 'set mikser runtime mode', 'development')
|
|
391
390
|
.option('-r --clear', 'clear current state before execution', false)
|
|
392
391
|
.option('-o --output-folder <folder>', 'set mikser output folder relative to working folder', 'out')
|
package/src/instance.js
CHANGED
|
@@ -19,10 +19,12 @@
|
|
|
19
19
|
// new to learn and no watermark to reason about — which matters, because a
|
|
20
20
|
// design that needs discipline from the caller is the one that gets violated.
|
|
21
21
|
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
22
|
+
// There is no opt-out. A flag for "run a second engine here anyway" only ever
|
|
23
|
+
// enabled the accident this file exists to prevent — two engines sharing one
|
|
24
|
+
// catalogue and one output tree with no lock between them — and no caller had
|
|
25
|
+
// a reason to want it that a stopped instance would not serve better. An
|
|
26
|
+
// option whose only use is the wrong one is not an escape hatch, it is a trap
|
|
27
|
+
// with a name.
|
|
26
28
|
|
|
27
29
|
import net from 'node:net'
|
|
28
30
|
import { createHash } from 'node:crypto'
|
|
@@ -280,7 +282,7 @@ function refuseConfig(socket, request, wrongConfig) {
|
|
|
280
282
|
type: 'refused',
|
|
281
283
|
reason: `this instance is running ${wrongConfig}, and you asked for ${path.resolve(request.config)}.`,
|
|
282
284
|
detail: 'Answering would use the wrong config — the accident this refusal exists to prevent. '
|
|
283
|
-
+ 'Stop that instance
|
|
285
|
+
+ 'Stop that instance and run this again.',
|
|
284
286
|
})
|
|
285
287
|
}
|
|
286
288
|
|
|
@@ -341,7 +343,6 @@ async function serveBuild(socket, request, logger) {
|
|
|
341
343
|
export function serveInstance() {
|
|
342
344
|
onLoaded(async () => {
|
|
343
345
|
if (!runtime.options.watch && !runtime.options.server) return
|
|
344
|
-
if (runtime.options.attach === false) return
|
|
345
346
|
const logger = runtime.engine?.logger
|
|
346
347
|
const endpoint = socketPath(runtime.options.workingFolder)
|
|
347
348
|
|
|
@@ -402,53 +403,7 @@ export function serveInstance() {
|
|
|
402
403
|
|
|
403
404
|
// Say so when a private engine is starting in a folder someone else holds.
|
|
404
405
|
//
|
|
405
|
-
// The local counterpart of the rule already written down for deployments —
|
|
406
|
-
// "never run a one-shot mikser command against the deployment" — which existed
|
|
407
|
-
// because there was no alternative. There is one now, so this covers what is
|
|
408
|
-
// left: --no-attach, and the report-only runs that stay local by design.
|
|
409
|
-
//
|
|
410
|
-
// A warning rather than a refusal. --no-attach is how you deliberately check
|
|
411
|
-
// that a cold start works, and refusing it would take away the escape hatch
|
|
412
|
-
// this design depends on having.
|
|
413
|
-
export async function warnIfHeld({ workingFolder, attached }) {
|
|
414
|
-
const endpoint = socketPath(workingFolder)
|
|
415
|
-
if (process.platform !== 'win32' && !existsSync(endpoint)) return false
|
|
416
|
-
|
|
417
|
-
const live = await new Promise((resolve) => {
|
|
418
|
-
const probe = net.connect(endpoint)
|
|
419
|
-
const done = (answer) => { try { probe.destroy() } catch { /* already gone */ } resolve(answer) }
|
|
420
|
-
probe.on('connect', () => done(true))
|
|
421
|
-
probe.on('error', () => done(false))
|
|
422
|
-
setTimeout(() => done(false), 250).unref?.()
|
|
423
|
-
})
|
|
424
|
-
if (!live) return false
|
|
425
|
-
|
|
426
|
-
const logger = runtime.engine?.logger
|
|
427
|
-
const message = attached === false
|
|
428
|
-
? 'Another mikser is already running in this folder, and --no-attach means this one will not talk to '
|
|
429
|
-
+ 'it. Two engines share the catalogue and the output tree with no lock between them; a --clear from '
|
|
430
|
-
+ 'either is what produces a cold rebuild that renders nothing.'
|
|
431
|
-
: 'Another mikser is already running in this folder. This command reads and does not write, so it is '
|
|
432
|
-
+ 'safe — but it may see a catalogue mid-cycle.'
|
|
433
|
-
logger?.warn?.({ code: 'instance-already-running' }, message)
|
|
434
|
-
return true
|
|
435
|
-
}
|
|
436
|
-
|
|
437
406
|
// Registered at setup so both halves are wired from one call.
|
|
438
407
|
export function instanceControl() {
|
|
439
408
|
serveInstance()
|
|
440
|
-
onLoaded(async () => {
|
|
441
|
-
// Only --no-attach reaches this now. Everything else either forwarded
|
|
442
|
-
// (a build, a report) or refused before setup ran (a second server or
|
|
443
|
-
// watcher), so a process that is still here and not attaching is one
|
|
444
|
-
// that deliberately opted out — and that is exactly the case worth
|
|
445
|
-
// saying something about, long-running or not. The earlier gate
|
|
446
|
-
// skipped watch and server, which excluded `--no-attach --server`:
|
|
447
|
-
// the very command that puts two engines on one folder.
|
|
448
|
-
if (runtime.options.attach !== false) return
|
|
449
|
-
await warnIfHeld({
|
|
450
|
-
workingFolder: runtime.options.workingFolder,
|
|
451
|
-
attached: false,
|
|
452
|
-
})
|
|
453
|
-
})
|
|
454
409
|
}
|
package/src/manifest.js
CHANGED
|
@@ -1289,7 +1289,36 @@ onFinalize(async () => {
|
|
|
1289
1289
|
// That made "resolve the collision by deleting the stub" delete the
|
|
1290
1290
|
// homepage, which is the opposite of what the operator asked for and the
|
|
1291
1291
|
// exact operation the new collision reporting invites.
|
|
1292
|
+
// Destinations THIS cycle's surviving renders just wrote.
|
|
1293
|
+
//
|
|
1294
|
+
// Their snapshot rows are not inserted until 3c below, so the
|
|
1295
|
+
// by-destination query cannot see them — the same blind spot 2a already
|
|
1296
|
+
// compensates for in the other direction ("recorded snapshots only
|
|
1297
|
+
// describe PRIOR cycles").
|
|
1298
|
+
//
|
|
1299
|
+
// Without this, renaming a source's extension unlinks the output the new
|
|
1300
|
+
// entity just wrote. `index.md` → `index.yml` keeps the entity NAME and
|
|
1301
|
+
// the destination but changes the id, so one cycle carries a DELETE for
|
|
1302
|
+
// the old id and a RENDER for the new one. The render writes the page,
|
|
1303
|
+
// the delete stages the same destination, nothing surviving claims it
|
|
1304
|
+
// yet, and the file goes — with `Rendered: 1`, a green build and no
|
|
1305
|
+
// warning. A second build does not fix it (nothing changed), nor does
|
|
1306
|
+
// touch (the input hash is the same); only a real content edit re-renders
|
|
1307
|
+
// it. Only --verify ever said so, and renaming an extension is the most
|
|
1308
|
+
// common action there is during a migration.
|
|
1309
|
+
const claimedByThisCycle = new Set()
|
|
1310
|
+
for (const { entity } of renderedEntries) {
|
|
1311
|
+
if (deleted.has(entity.id) || (entity.parent && deleted.has(entity.parent))) continue
|
|
1312
|
+
if (entity.destination) claimedByThisCycle.add(entity.destination)
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1292
1315
|
for (const { destination, reason } of filesToUnlink) {
|
|
1316
|
+
// Written this cycle by an entity that is staying. Keep it, and say
|
|
1317
|
+
// nothing: the snapshot recorded at 3c is this render's own, so there
|
|
1318
|
+
// is no staleness to report — unlike the surviving-snapshot case
|
|
1319
|
+
// below, where the bytes belong to the entity that went away.
|
|
1320
|
+
if (claimedByThisCycle.has(destination)) continue
|
|
1321
|
+
|
|
1293
1322
|
// "Still claimed" means by something that SURVIVES this pass. The
|
|
1294
1323
|
// ids going away here are not just the deleted entities: pagination
|
|
1295
1324
|
// children staged above are removed too, and counting a child's own
|
package/src/server.js
CHANGED
|
@@ -220,13 +220,42 @@ export function setupServer() {
|
|
|
220
220
|
.sendFile(faviconFile)
|
|
221
221
|
})
|
|
222
222
|
|
|
223
|
-
|
|
224
|
-
|
|
223
|
+
// Why this needs an error handler AND an address() check, rather
|
|
224
|
+
// than trusting the callback:
|
|
225
|
+
//
|
|
226
|
+
// `app.listen(port, cb)` invokes cb even when the bind FAILED —
|
|
227
|
+
// observed with a squatter on the port, cb running with
|
|
228
|
+
// `address() === null` and EADDRINUSE arriving on the error event
|
|
229
|
+
// afterwards. With no error handler, the process logged
|
|
230
|
+
// "Server listening: http://localhost:3779", printed a LAN
|
|
231
|
+
// address, completed the build green, and answered nothing: every
|
|
232
|
+
// request went to whoever actually held the port, which on a dev
|
|
233
|
+
// machine is another project's site. The operator opens the
|
|
234
|
+
// address, sees somebody else's pages, and the log insists they
|
|
235
|
+
// are their own.
|
|
236
|
+
//
|
|
237
|
+
// A server that cannot bind is not a server. --server asks to
|
|
238
|
+
// BECOME the instance, which is exactly the request that cannot be
|
|
239
|
+
// satisfied by carrying on — the same reasoning that makes a second
|
|
240
|
+
// server exit 1 rather than forward.
|
|
241
|
+
await new Promise((resolve, reject) => {
|
|
242
|
+
const httpServer = runtime.options.app.listen(runtime.options.port, function onListening() {
|
|
225
243
|
// Public URL wins for operator-clickable log lines —
|
|
226
244
|
// a reverse-proxy/tunnel/ngrok setup binds locally but
|
|
227
245
|
// is reached externally at runtime.options.url. Fall
|
|
228
246
|
// back to the bind URL when no public origin is set.
|
|
229
|
-
|
|
247
|
+
// The bind is the authority on which port answers, not
|
|
248
|
+
// the port that was asked for. A null address means the
|
|
249
|
+
// listen failed and the error handler below owns it — say
|
|
250
|
+
// nothing here, or the log claims a port it does not have.
|
|
251
|
+
// `this`, not the const above: EventEmitter calls a
|
|
252
|
+
// listener with the emitter, and a synchronous listen
|
|
253
|
+
// reaches here while `httpServer` is still in its
|
|
254
|
+
// temporal dead zone.
|
|
255
|
+
const bound = this?.address?.()
|
|
256
|
+
if (!bound) return
|
|
257
|
+
|
|
258
|
+
const externalUrl = runtime.options.url ?? `http://localhost:${bound.port}`
|
|
230
259
|
logger.info('Server listening: %s', externalUrl)
|
|
231
260
|
|
|
232
261
|
// The addresses that are NOT localhost.
|
|
@@ -246,13 +275,25 @@ export function setupServer() {
|
|
|
246
275
|
const lan = localAddresses()
|
|
247
276
|
if (lan.length) {
|
|
248
277
|
logger.info('Also on: %s', lan
|
|
249
|
-
.map(address => `http://${address}:${
|
|
278
|
+
.map(address => `http://${address}:${bound.port}`)
|
|
250
279
|
.join(' '))
|
|
251
280
|
}
|
|
252
281
|
}
|
|
253
282
|
resolve()
|
|
254
283
|
})
|
|
255
284
|
|
|
285
|
+
httpServer.on('error', (err) => {
|
|
286
|
+
if (err.code === 'EADDRINUSE') {
|
|
287
|
+
reject(new Error(
|
|
288
|
+
`port ${runtime.options.port} is already in use — something else is listening there, `
|
|
289
|
+
+ `and requests to it will reach that instead of this build. `
|
|
290
|
+
+ `Stop it, or pass --server <other port>.`))
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
reject(new Error(
|
|
294
|
+
`could not listen on port ${runtime.options.port} — ${err.code ?? err.message}`))
|
|
295
|
+
})
|
|
296
|
+
|
|
256
297
|
// Node caps a single request at 5 minutes (requestTimeout,
|
|
257
298
|
// default 300_000ms), which is not a timeout in the usual
|
|
258
299
|
// sense here — it is an upload size limit expressed in
|