@blamejs/core 0.7.1 → 0.7.18
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/CHANGELOG.md +423 -389
- package/README.md +150 -149
- package/bin/blamejs.js +0 -0
- package/index.js +308 -282
- package/lib/api-key.js +660 -672
- package/lib/api-snapshot.js +338 -338
- package/lib/app-shutdown.js +385 -385
- package/lib/app.js +365 -365
- package/lib/archive.js +250 -250
- package/lib/atomic-file.js +544 -544
- package/lib/audit-chain.js +177 -177
- package/lib/audit-sign.js +344 -344
- package/lib/audit-tools.js +677 -677
- package/lib/audit.js +766 -766
- package/lib/auth/jwt.js +311 -311
- package/lib/auth/lockout.js +436 -436
- package/lib/auth/oauth.js +721 -721
- package/lib/auth/passkey.js +181 -181
- package/lib/auth/password.js +594 -594
- package/lib/backup/bundle.js +217 -217
- package/lib/backup/crypto.js +176 -176
- package/lib/backup/index.js +515 -515
- package/lib/backup/manifest.js +282 -282
- package/lib/break-glass.js +1338 -1338
- package/lib/bundler.js +441 -441
- package/lib/cache-redis.js +256 -266
- package/lib/cache.js +1206 -1211
- package/lib/canonical-json.js +115 -115
- package/lib/chain-writer.js +234 -234
- package/lib/cli-helpers.js +206 -206
- package/lib/cli.js +2334 -2334
- package/lib/cluster-provider-db.js +317 -317
- package/lib/cluster-storage.js +226 -226
- package/lib/cluster.js +703 -703
- package/lib/codepoint-class.js +196 -0
- package/lib/config-drift.js +301 -301
- package/lib/consent.js +222 -222
- package/lib/constants.js +191 -191
- package/lib/cookies.js +315 -315
- package/lib/credential-hash.js +322 -322
- package/lib/crypto.js +266 -266
- package/lib/csv.js +275 -286
- package/lib/db-declare-row-policy.js +267 -267
- package/lib/db-declare-view.js +420 -421
- package/lib/db-query.js +406 -406
- package/lib/db-schema.js +319 -319
- package/lib/db.js +1288 -1288
- package/lib/deprecate.js +222 -222
- package/lib/dev.js +335 -335
- package/lib/dual-control.js +473 -473
- package/lib/error-page.js +420 -420
- package/lib/external-db-migrate.js +441 -441
- package/lib/external-db.js +1061 -1061
- package/lib/file-type.js +273 -273
- package/lib/file-upload.js +1136 -0
- package/lib/forms.js +422 -422
- package/lib/framework-error.js +293 -202
- package/lib/framework-schema.js +717 -717
- package/lib/gate-contract.js +971 -0
- package/lib/guard-all.js +405 -0
- package/lib/guard-archive.js +739 -0
- package/lib/guard-csv.js +816 -0
- package/lib/guard-email.js +744 -0
- package/lib/guard-filename.js +724 -0
- package/lib/guard-html.js +976 -0
- package/lib/guard-json.js +729 -0
- package/lib/guard-markdown.js +586 -0
- package/lib/guard-svg.js +976 -0
- package/lib/guard-xml.js +405 -0
- package/lib/guard-yaml.js +529 -0
- package/lib/handlers.js +350 -350
- package/lib/http-client-cookie-jar.js +508 -508
- package/lib/http-client.js +1195 -1195
- package/lib/i18n.js +878 -878
- package/lib/jobs.js +185 -185
- package/lib/log-stream-cloudwatch.js +369 -369
- package/lib/log-stream-local.js +146 -146
- package/lib/log-stream-otlp-grpc.js +410 -410
- package/lib/log-stream-otlp.js +286 -286
- package/lib/log-stream-syslog.js +302 -302
- package/lib/log-stream-webhook.js +199 -199
- package/lib/log-stream.js +330 -330
- package/lib/log.js +500 -500
- package/lib/mail-bounce.js +528 -528
- package/lib/mail-dkim.js +369 -362
- package/lib/mail.js +981 -962
- package/lib/metrics.js +683 -683
- package/lib/middleware/api-encrypt.js +936 -573
- package/lib/middleware/attach-user.js +157 -157
- package/lib/middleware/body-parser.js +1170 -1091
- package/lib/middleware/bot-guard.js +178 -178
- package/lib/middleware/compression.js +452 -452
- package/lib/middleware/cors.js +314 -314
- package/lib/middleware/csp-nonce.js +348 -348
- package/lib/middleware/csrf-protect.js +316 -316
- package/lib/middleware/db-role-for.js +264 -269
- package/lib/middleware/health.js +392 -392
- package/lib/middleware/index.js +79 -79
- package/lib/middleware/rate-limit.js +358 -358
- package/lib/middleware/request-id.js +61 -61
- package/lib/middleware/request-log.js +168 -168
- package/lib/middleware/require-auth.js +104 -104
- package/lib/middleware/security-headers.js +116 -116
- package/lib/middleware/sse.js +166 -166
- package/lib/migrations.js +383 -383
- package/lib/mtls-ca.js +518 -518
- package/lib/mtls-engine-default.js +481 -481
- package/lib/network-dns.js +632 -632
- package/lib/network-heartbeat.js +290 -290
- package/lib/network-nts.js +574 -574
- package/lib/network-proxy.js +265 -265
- package/lib/network-tls.js +328 -328
- package/lib/network.js +233 -233
- package/lib/notify.js +612 -614
- package/lib/ntp-check.js +229 -229
- package/lib/numeric-bounds.js +111 -91
- package/lib/object-store/azure-blob-bucket-ops.js +349 -349
- package/lib/object-store/azure-blob.js +488 -451
- package/lib/object-store/gcs-bucket-ops.js +351 -351
- package/lib/object-store/gcs.js +519 -479
- package/lib/object-store/http-put.js +153 -153
- package/lib/object-store/index.js +197 -197
- package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
- package/lib/object-store/sigv4.js +903 -855
- package/lib/observability.js +151 -151
- package/lib/otel-export.js +269 -269
- package/lib/pagination.js +464 -464
- package/lib/parsers/index.js +80 -80
- package/lib/parsers/safe-env.js +642 -642
- package/lib/parsers/safe-ini.js +292 -292
- package/lib/parsers/safe-toml.js +784 -784
- package/lib/parsers/safe-xml.js +390 -390
- package/lib/parsers/safe-yaml.js +1015 -1015
- package/lib/permissions.js +708 -708
- package/lib/pqc-agent.js +87 -87
- package/lib/pqc-gate.js +279 -279
- package/lib/protobuf-encoder.js +190 -190
- package/lib/protocol-dispatcher.js +161 -161
- package/lib/pubsub-redis.js +167 -177
- package/lib/pubsub.js +429 -429
- package/lib/queue-local.js +476 -476
- package/lib/queue-redis.js +745 -752
- package/lib/queue-sqs.js +319 -319
- package/lib/queue.js +695 -695
- package/lib/redis-client.js +519 -489
- package/lib/request-helpers.js +340 -336
- package/lib/restore-bundle.js +237 -237
- package/lib/restore-rollback.js +259 -259
- package/lib/restore.js +409 -409
- package/lib/retry.js +376 -376
- package/lib/router.js +748 -748
- package/lib/safe-async.js +735 -735
- package/lib/safe-buffer.js +237 -237
- package/lib/safe-json.js +541 -541
- package/lib/safe-schema.js +1266 -1266
- package/lib/safe-url.js +159 -159
- package/lib/scheduler.js +706 -706
- package/lib/security-assert.js +373 -373
- package/lib/seeders.js +618 -630
- package/lib/session.js +478 -478
- package/lib/slug.js +269 -269
- package/lib/ssrf-guard.js +401 -401
- package/lib/static.js +879 -114
- package/lib/storage.js +471 -471
- package/lib/subject.js +281 -281
- package/lib/template.js +791 -791
- package/lib/testing.js +798 -798
- package/lib/time.js +310 -310
- package/lib/totp.js +302 -302
- package/lib/tracing.js +494 -494
- package/lib/uuid.js +132 -132
- package/lib/validate-opts.js +340 -270
- package/lib/vault/index.js +308 -308
- package/lib/vault/rotate.js +784 -784
- package/lib/vault/wrap.js +296 -296
- package/lib/vendor/noble-ciphers.cjs +9 -9
- package/lib/webhook.js +595 -598
- package/lib/websocket.js +1048 -1048
- package/package.json +77 -77
- package/sbom.cyclonedx.json +7 -7
package/lib/dev.js
CHANGED
|
@@ -1,335 +1,335 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* dev — file-watch + child-process restart for iteration loops.
|
|
4
|
-
*
|
|
5
|
-
* The framework's hot-reload primitive. Spawn the app as a child
|
|
6
|
-
* process, watch the source directories, and restart the child when a
|
|
7
|
-
* file changes. On-disk state (vault keys, encrypted DB, sealed
|
|
8
|
-
* cookies) survives the restart because the child re-opens the files
|
|
9
|
-
* — only in-process state is lost, which is the correct semantic for
|
|
10
|
-
* "I just edited a route handler and want to see it."
|
|
11
|
-
*
|
|
12
|
-
* var dev = b.dev.create({
|
|
13
|
-
* command: "node",
|
|
14
|
-
* args: ["./server.js"],
|
|
15
|
-
* watch: ["./routes", "./views", "./public", "./lib"],
|
|
16
|
-
* ignore: [/node_modules/, /\.db$/, /\.tmp$/, /^\./],
|
|
17
|
-
* graceMs: 250, // debounce: collapse bursts into one restart
|
|
18
|
-
* killSignal: "SIGTERM",
|
|
19
|
-
* killTimeoutMs: 4000, // SIGKILL after this if SIGTERM is ignored
|
|
20
|
-
* log: logInstance, // optional structured logger
|
|
21
|
-
* env: { ...process.env, BLAMEJS_DEV: "1" },
|
|
22
|
-
* cwd: process.cwd(),
|
|
23
|
-
* });
|
|
24
|
-
*
|
|
25
|
-
* await dev.start(); // launches child + arms watchers
|
|
26
|
-
* await dev.stop(); // signals child + closes watchers
|
|
27
|
-
*
|
|
28
|
-
* dev.stats(); // → { pid, running, restarts, lastRestartAt }
|
|
29
|
-
*
|
|
30
|
-
* Operator-side, this is what `blamejs dev` will call (next CLI slice).
|
|
31
|
-
*
|
|
32
|
-
* Engine hygiene:
|
|
33
|
-
* - Bursts of file events (a saved-everything keystroke, a multi-
|
|
34
|
-
* file format-on-save) collapse into one restart via debounce.
|
|
35
|
-
* - The child is spawned with stdio: 'inherit' so the operator sees
|
|
36
|
-
* their app's output unchanged.
|
|
37
|
-
* - Parent SIGINT/SIGTERM are forwarded: stop() before exit so an
|
|
38
|
-
* orphan child can't outlive the dev session.
|
|
39
|
-
* - Restart in-flight when a new event arrives: queue one followup,
|
|
40
|
-
* no more — many edits during a slow restart still result in only
|
|
41
|
-
* two restarts, not N.
|
|
42
|
-
*
|
|
43
|
-
* Test seams:
|
|
44
|
-
* opts._spawn(cmd, args, sopts) → child-process-shaped object
|
|
45
|
-
* opts._watch(dir, wopts, listener) → fs.watcher-shaped object
|
|
46
|
-
* These default to child_process.spawn and fs.watch; tests pass
|
|
47
|
-
* fakes to drive the engine without real subprocesses.
|
|
48
|
-
*/
|
|
49
|
-
|
|
50
|
-
var path = require("path");
|
|
51
|
-
var childProcess = require("child_process");
|
|
52
|
-
var fs = require("fs");
|
|
53
|
-
var logModule = require("./log");
|
|
54
|
-
var nb = require("./numeric-bounds");
|
|
55
|
-
var validateOpts = require("./validate-opts");
|
|
56
|
-
var { FrameworkError } = require("./framework-error");
|
|
57
|
-
|
|
58
|
-
class DevError extends FrameworkError {
|
|
59
|
-
constructor(code, message) {
|
|
60
|
-
super(message, code);
|
|
61
|
-
this.name = "DevError";
|
|
62
|
-
this.permanent = true;
|
|
63
|
-
this.isDevError = true;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
var bootLog = logModule.boot("dev");
|
|
68
|
-
|
|
69
|
-
var DEFAULT_GRACE_MS = 250;
|
|
70
|
-
// Default SIGKILL escalation budget: 4s — long enough for graceful
|
|
71
|
-
// shutdown handlers to flush, short enough that an ignored SIGTERM
|
|
72
|
-
// doesn't keep the dev loop hanging.
|
|
73
|
-
var DEFAULT_KILL_TIMEOUT_MS = 0xFA0;
|
|
74
|
-
var DEFAULT_KILL_SIGNAL = "SIGTERM";
|
|
75
|
-
var DEFAULT_IGNORE = [
|
|
76
|
-
/node_modules/,
|
|
77
|
-
/\.git\b/,
|
|
78
|
-
/^\./, // dotfiles + dotdirs
|
|
79
|
-
/\.db$/, /\.db-journal$/, /\.db-wal$/, /\.db-shm$/,
|
|
80
|
-
/\.log$/,
|
|
81
|
-
/\.swp$/, /~$/, // editor scratch files
|
|
82
|
-
];
|
|
83
|
-
|
|
84
|
-
function _matchesAny(patterns, value) {
|
|
85
|
-
for (var i = 0; i < patterns.length; i++) {
|
|
86
|
-
var p = patterns[i];
|
|
87
|
-
if (p instanceof RegExp) { if (p.test(value)) return true; }
|
|
88
|
-
else if (typeof p === "string") { if (value.indexOf(p) !== -1) return true; }
|
|
89
|
-
}
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function _logVia(log, level, message, fields) {
|
|
94
|
-
if (log && typeof log[level] === "function") {
|
|
95
|
-
try { log[level](message, fields); }
|
|
96
|
-
catch (_e) { /* logger best-effort */ }
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
// Fallback: framework boot channel (TTY-aware, structured-JSON when piped).
|
|
100
|
-
var line = message + (fields ? " " + JSON.stringify(fields) : "");
|
|
101
|
-
var emit = (level === "error" || level === "fatal") ? bootLog.error
|
|
102
|
-
: (level === "warn" ? bootLog.warn : bootLog.info);
|
|
103
|
-
emit(line);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function create(opts) {
|
|
107
|
-
opts = opts || {};
|
|
108
|
-
validateOpts.requireNonEmptyString(opts.command, "dev.create: opts.command (the program to spawn)", DevError, "dev/no-command");
|
|
109
|
-
var args = Array.isArray(opts.args) ? opts.args.slice() : [];
|
|
110
|
-
var watch = Array.isArray(opts.watch) && opts.watch.length > 0
|
|
111
|
-
? opts.watch.slice() : ["."];
|
|
112
|
-
var ignore = Array.isArray(opts.ignore)
|
|
113
|
-
? DEFAULT_IGNORE.concat(opts.ignore)
|
|
114
|
-
: DEFAULT_IGNORE.slice();
|
|
115
|
-
nb.requireNonNegativeFiniteIntIfPresent(opts.graceMs,
|
|
116
|
-
"dev.create: opts.graceMs", DevError, "dev/bad-grace-ms");
|
|
117
|
-
var graceMs = opts.graceMs !== undefined ? opts.graceMs : DEFAULT_GRACE_MS;
|
|
118
|
-
var killSignal = typeof opts.killSignal === "string" ? opts.killSignal : DEFAULT_KILL_SIGNAL;
|
|
119
|
-
nb.requireNonNegativeFiniteIntIfPresent(opts.killTimeoutMs,
|
|
120
|
-
"dev.create: opts.killTimeoutMs", DevError, "dev/bad-kill-timeout-ms");
|
|
121
|
-
var killTimeoutMs = opts.killTimeoutMs !== undefined ? opts.killTimeoutMs : DEFAULT_KILL_TIMEOUT_MS;
|
|
122
|
-
var log = opts.log || null;
|
|
123
|
-
var env = opts.env || process.env;
|
|
124
|
-
var cwd = opts.cwd || process.cwd();
|
|
125
|
-
|
|
126
|
-
// Test seams
|
|
127
|
-
var spawnFn = opts._spawn || function (cmd, sargs, sopts) {
|
|
128
|
-
return childProcess.spawn(cmd, sargs, sopts);
|
|
129
|
-
};
|
|
130
|
-
var watchFn = opts._watch || function (dir, wopts, listener) {
|
|
131
|
-
return fs.watch(dir, wopts, listener);
|
|
132
|
-
};
|
|
133
|
-
var setTimeoutFn = opts._setTimeout || setTimeout;
|
|
134
|
-
var clearTimeoutFn = opts._clearTimeout || clearTimeout;
|
|
135
|
-
|
|
136
|
-
var child = null;
|
|
137
|
-
var watchers = [];
|
|
138
|
-
var debounceTimer = null;
|
|
139
|
-
var killTimer = null;
|
|
140
|
-
var started = false;
|
|
141
|
-
var stopping = false;
|
|
142
|
-
var restarting = false;
|
|
143
|
-
var queuedRestart = false;
|
|
144
|
-
var restartCount = 0;
|
|
145
|
-
var lastRestartAt = null;
|
|
146
|
-
|
|
147
|
-
function _spawnChild() {
|
|
148
|
-
var c = spawnFn(opts.command, args, {
|
|
149
|
-
stdio: opts.stdio || "inherit",
|
|
150
|
-
env: env,
|
|
151
|
-
cwd: cwd,
|
|
152
|
-
});
|
|
153
|
-
c.on("exit", function (code, signal) {
|
|
154
|
-
// If the child exits while we're not stopping/restarting, that's
|
|
155
|
-
// a crash — log it and wait for a file change to retry. This
|
|
156
|
-
// matches nodemon's behavior: a bad commit leaves the child
|
|
157
|
-
// corpse around without spawn-thrashing.
|
|
158
|
-
if (!stopping && !restarting) {
|
|
159
|
-
_logVia(log, "warn",
|
|
160
|
-
"child exited unexpectedly — waiting for file change to restart",
|
|
161
|
-
{ code: code, signal: signal });
|
|
162
|
-
child = null;
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
c.on("error", function (err) {
|
|
166
|
-
_logVia(log, "error", "spawn error", { error: (err && err.message) || String(err) });
|
|
167
|
-
});
|
|
168
|
-
return c;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function _killChild() {
|
|
172
|
-
return new Promise(function (resolve) {
|
|
173
|
-
if (!child) { resolve(); return; }
|
|
174
|
-
var c = child;
|
|
175
|
-
var settled = false;
|
|
176
|
-
function done() {
|
|
177
|
-
if (settled) return;
|
|
178
|
-
settled = true;
|
|
179
|
-
if (killTimer) { try { clearTimeoutFn(killTimer); } catch (_e) { /* timer already cleared */ } killTimer = null; }
|
|
180
|
-
resolve();
|
|
181
|
-
}
|
|
182
|
-
c.once("exit", done);
|
|
183
|
-
try { c.kill(killSignal); }
|
|
184
|
-
catch (e) {
|
|
185
|
-
_logVia(log, "warn", "kill threw, child may already be gone",
|
|
186
|
-
{ error: (e && e.message) || String(e) });
|
|
187
|
-
done();
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
// Hard-kill if the child ignores SIGTERM
|
|
191
|
-
killTimer = setTimeoutFn(function () {
|
|
192
|
-
if (settled) return;
|
|
193
|
-
_logVia(log, "warn",
|
|
194
|
-
"child did not exit after " + killTimeoutMs + "ms — sending SIGKILL");
|
|
195
|
-
try { c.kill("SIGKILL"); }
|
|
196
|
-
catch (_e) { done(); }
|
|
197
|
-
}, killTimeoutMs);
|
|
198
|
-
if (killTimer && typeof killTimer.unref === "function") killTimer.unref();
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
async function _restart() {
|
|
203
|
-
if (restarting) {
|
|
204
|
-
// Coalesce: a restart-in-flight gets one followup queued, never more.
|
|
205
|
-
queuedRestart = true;
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
restarting = true;
|
|
209
|
-
try {
|
|
210
|
-
await _killChild();
|
|
211
|
-
child = null;
|
|
212
|
-
if (stopping) return;
|
|
213
|
-
child = _spawnChild();
|
|
214
|
-
restartCount += 1;
|
|
215
|
-
lastRestartAt = new Date().toISOString();
|
|
216
|
-
_logVia(log, "info", "restarted",
|
|
217
|
-
{ pid: child && child.pid, restarts: restartCount });
|
|
218
|
-
} catch (e) {
|
|
219
|
-
_logVia(log, "error", "restart failed",
|
|
220
|
-
{ error: (e && e.message) || String(e) });
|
|
221
|
-
} finally {
|
|
222
|
-
restarting = false;
|
|
223
|
-
if (queuedRestart && !stopping) {
|
|
224
|
-
queuedRestart = false;
|
|
225
|
-
// Tail-call the queued restart on the next microtask so we
|
|
226
|
-
// unwind the current stack first.
|
|
227
|
-
Promise.resolve().then(_restart);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function _scheduleRestart(reason) {
|
|
233
|
-
if (stopping) return;
|
|
234
|
-
if (debounceTimer) clearTimeoutFn(debounceTimer);
|
|
235
|
-
debounceTimer = setTimeoutFn(function () {
|
|
236
|
-
debounceTimer = null;
|
|
237
|
-
_logVia(log, "info", "change detected, restarting", { reason: reason });
|
|
238
|
-
_restart();
|
|
239
|
-
}, graceMs);
|
|
240
|
-
if (debounceTimer && typeof debounceTimer.unref === "function") debounceTimer.unref();
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function _onWatchEvent(dir, eventType, filename) {
|
|
244
|
-
if (!filename) return;
|
|
245
|
-
var rel = String(filename);
|
|
246
|
-
var full = path.join(dir, rel);
|
|
247
|
-
if (_matchesAny(ignore, rel) || _matchesAny(ignore, full)) return;
|
|
248
|
-
_scheduleRestart(eventType + ":" + rel);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function _armWatchers() {
|
|
252
|
-
for (var i = 0; i < watch.length; i++) {
|
|
253
|
-
(function (dir) {
|
|
254
|
-
var resolved = path.isAbsolute(dir) ? dir : path.resolve(cwd, dir);
|
|
255
|
-
var w;
|
|
256
|
-
try {
|
|
257
|
-
w = watchFn(resolved, { recursive: true, persistent: false }, function (eventType, filename) {
|
|
258
|
-
_onWatchEvent(resolved, eventType, filename);
|
|
259
|
-
});
|
|
260
|
-
} catch (e) {
|
|
261
|
-
// Missing dir, permission error, platform that doesn't
|
|
262
|
-
// support recursive — log and move on so a bad watch entry
|
|
263
|
-
// doesn't take the whole dev loop down.
|
|
264
|
-
_logVia(log, "warn", "could not watch " + resolved,
|
|
265
|
-
{ error: (e && e.message) || String(e) });
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
if (w && typeof w.on === "function") {
|
|
269
|
-
w.on("error", function (err) {
|
|
270
|
-
_logVia(log, "warn", "watcher error on " + resolved,
|
|
271
|
-
{ error: (err && err.message) || String(err) });
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
watchers.push(w);
|
|
275
|
-
})(watch[i]);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function _disarmWatchers() {
|
|
280
|
-
for (var i = 0; i < watchers.length; i++) {
|
|
281
|
-
try { if (watchers[i] && typeof watchers[i].close === "function") watchers[i].close(); }
|
|
282
|
-
catch (_e) { /* close best-effort */ }
|
|
283
|
-
}
|
|
284
|
-
watchers = [];
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
async function start() {
|
|
288
|
-
if (started) return;
|
|
289
|
-
started = true;
|
|
290
|
-
stopping = false;
|
|
291
|
-
_armWatchers();
|
|
292
|
-
child = _spawnChild();
|
|
293
|
-
_logVia(log, "info", "started", { pid: child && child.pid, watch: watch });
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
async function stop() {
|
|
297
|
-
if (!started) return;
|
|
298
|
-
stopping = true;
|
|
299
|
-
if (debounceTimer) { try { clearTimeoutFn(debounceTimer); } catch (_e) { /* timer already cleared */ } debounceTimer = null; }
|
|
300
|
-
_disarmWatchers();
|
|
301
|
-
await _killChild();
|
|
302
|
-
child = null;
|
|
303
|
-
started = false;
|
|
304
|
-
stopping = false;
|
|
305
|
-
queuedRestart = false;
|
|
306
|
-
_logVia(log, "info", "stopped", { restarts: restartCount });
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
async function restart() { await _restart(); }
|
|
310
|
-
|
|
311
|
-
function stats() {
|
|
312
|
-
return {
|
|
313
|
-
pid: child && child.pid !== undefined ? child.pid : null,
|
|
314
|
-
running: !!child && started && !stopping,
|
|
315
|
-
restarts: restartCount,
|
|
316
|
-
lastRestartAt: lastRestartAt,
|
|
317
|
-
watchers: watchers.length,
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
return {
|
|
322
|
-
start: start,
|
|
323
|
-
stop: stop,
|
|
324
|
-
restart: restart,
|
|
325
|
-
stats: stats,
|
|
326
|
-
// Test hook: simulate a watcher event for engine tests
|
|
327
|
-
_emit: function (dir, eventType, filename) { _onWatchEvent(dir, eventType, filename); },
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
module.exports = {
|
|
332
|
-
create: create,
|
|
333
|
-
DevError: DevError,
|
|
334
|
-
DEFAULT_IGNORE: DEFAULT_IGNORE,
|
|
335
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* dev — file-watch + child-process restart for iteration loops.
|
|
4
|
+
*
|
|
5
|
+
* The framework's hot-reload primitive. Spawn the app as a child
|
|
6
|
+
* process, watch the source directories, and restart the child when a
|
|
7
|
+
* file changes. On-disk state (vault keys, encrypted DB, sealed
|
|
8
|
+
* cookies) survives the restart because the child re-opens the files
|
|
9
|
+
* — only in-process state is lost, which is the correct semantic for
|
|
10
|
+
* "I just edited a route handler and want to see it."
|
|
11
|
+
*
|
|
12
|
+
* var dev = b.dev.create({
|
|
13
|
+
* command: "node",
|
|
14
|
+
* args: ["./server.js"],
|
|
15
|
+
* watch: ["./routes", "./views", "./public", "./lib"],
|
|
16
|
+
* ignore: [/node_modules/, /\.db$/, /\.tmp$/, /^\./],
|
|
17
|
+
* graceMs: 250, // debounce: collapse bursts into one restart
|
|
18
|
+
* killSignal: "SIGTERM",
|
|
19
|
+
* killTimeoutMs: 4000, // SIGKILL after this if SIGTERM is ignored
|
|
20
|
+
* log: logInstance, // optional structured logger
|
|
21
|
+
* env: { ...process.env, BLAMEJS_DEV: "1" },
|
|
22
|
+
* cwd: process.cwd(),
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* await dev.start(); // launches child + arms watchers
|
|
26
|
+
* await dev.stop(); // signals child + closes watchers
|
|
27
|
+
*
|
|
28
|
+
* dev.stats(); // → { pid, running, restarts, lastRestartAt }
|
|
29
|
+
*
|
|
30
|
+
* Operator-side, this is what `blamejs dev` will call (next CLI slice).
|
|
31
|
+
*
|
|
32
|
+
* Engine hygiene:
|
|
33
|
+
* - Bursts of file events (a saved-everything keystroke, a multi-
|
|
34
|
+
* file format-on-save) collapse into one restart via debounce.
|
|
35
|
+
* - The child is spawned with stdio: 'inherit' so the operator sees
|
|
36
|
+
* their app's output unchanged.
|
|
37
|
+
* - Parent SIGINT/SIGTERM are forwarded: stop() before exit so an
|
|
38
|
+
* orphan child can't outlive the dev session.
|
|
39
|
+
* - Restart in-flight when a new event arrives: queue one followup,
|
|
40
|
+
* no more — many edits during a slow restart still result in only
|
|
41
|
+
* two restarts, not N.
|
|
42
|
+
*
|
|
43
|
+
* Test seams:
|
|
44
|
+
* opts._spawn(cmd, args, sopts) → child-process-shaped object
|
|
45
|
+
* opts._watch(dir, wopts, listener) → fs.watcher-shaped object
|
|
46
|
+
* These default to child_process.spawn and fs.watch; tests pass
|
|
47
|
+
* fakes to drive the engine without real subprocesses.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
var path = require("path");
|
|
51
|
+
var childProcess = require("child_process");
|
|
52
|
+
var fs = require("fs");
|
|
53
|
+
var logModule = require("./log");
|
|
54
|
+
var nb = require("./numeric-bounds");
|
|
55
|
+
var validateOpts = require("./validate-opts");
|
|
56
|
+
var { FrameworkError } = require("./framework-error");
|
|
57
|
+
|
|
58
|
+
class DevError extends FrameworkError {
|
|
59
|
+
constructor(code, message) {
|
|
60
|
+
super(message, code);
|
|
61
|
+
this.name = "DevError";
|
|
62
|
+
this.permanent = true;
|
|
63
|
+
this.isDevError = true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
var bootLog = logModule.boot("dev");
|
|
68
|
+
|
|
69
|
+
var DEFAULT_GRACE_MS = 250;
|
|
70
|
+
// Default SIGKILL escalation budget: 4s — long enough for graceful
|
|
71
|
+
// shutdown handlers to flush, short enough that an ignored SIGTERM
|
|
72
|
+
// doesn't keep the dev loop hanging.
|
|
73
|
+
var DEFAULT_KILL_TIMEOUT_MS = 0xFA0;
|
|
74
|
+
var DEFAULT_KILL_SIGNAL = "SIGTERM";
|
|
75
|
+
var DEFAULT_IGNORE = [
|
|
76
|
+
/node_modules/,
|
|
77
|
+
/\.git\b/,
|
|
78
|
+
/^\./, // dotfiles + dotdirs
|
|
79
|
+
/\.db$/, /\.db-journal$/, /\.db-wal$/, /\.db-shm$/,
|
|
80
|
+
/\.log$/,
|
|
81
|
+
/\.swp$/, /~$/, // editor scratch files
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
function _matchesAny(patterns, value) {
|
|
85
|
+
for (var i = 0; i < patterns.length; i++) {
|
|
86
|
+
var p = patterns[i];
|
|
87
|
+
if (p instanceof RegExp) { if (p.test(value)) return true; }
|
|
88
|
+
else if (typeof p === "string") { if (value.indexOf(p) !== -1) return true; }
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function _logVia(log, level, message, fields) {
|
|
94
|
+
if (log && typeof log[level] === "function") {
|
|
95
|
+
try { log[level](message, fields); }
|
|
96
|
+
catch (_e) { /* logger best-effort */ }
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// Fallback: framework boot channel (TTY-aware, structured-JSON when piped).
|
|
100
|
+
var line = message + (fields ? " " + JSON.stringify(fields) : "");
|
|
101
|
+
var emit = (level === "error" || level === "fatal") ? bootLog.error
|
|
102
|
+
: (level === "warn" ? bootLog.warn : bootLog.info);
|
|
103
|
+
emit(line);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function create(opts) {
|
|
107
|
+
opts = opts || {};
|
|
108
|
+
validateOpts.requireNonEmptyString(opts.command, "dev.create: opts.command (the program to spawn)", DevError, "dev/no-command");
|
|
109
|
+
var args = Array.isArray(opts.args) ? opts.args.slice() : [];
|
|
110
|
+
var watch = Array.isArray(opts.watch) && opts.watch.length > 0
|
|
111
|
+
? opts.watch.slice() : ["."];
|
|
112
|
+
var ignore = Array.isArray(opts.ignore)
|
|
113
|
+
? DEFAULT_IGNORE.concat(opts.ignore)
|
|
114
|
+
: DEFAULT_IGNORE.slice();
|
|
115
|
+
nb.requireNonNegativeFiniteIntIfPresent(opts.graceMs,
|
|
116
|
+
"dev.create: opts.graceMs", DevError, "dev/bad-grace-ms");
|
|
117
|
+
var graceMs = opts.graceMs !== undefined ? opts.graceMs : DEFAULT_GRACE_MS;
|
|
118
|
+
var killSignal = typeof opts.killSignal === "string" ? opts.killSignal : DEFAULT_KILL_SIGNAL;
|
|
119
|
+
nb.requireNonNegativeFiniteIntIfPresent(opts.killTimeoutMs,
|
|
120
|
+
"dev.create: opts.killTimeoutMs", DevError, "dev/bad-kill-timeout-ms");
|
|
121
|
+
var killTimeoutMs = opts.killTimeoutMs !== undefined ? opts.killTimeoutMs : DEFAULT_KILL_TIMEOUT_MS;
|
|
122
|
+
var log = opts.log || null;
|
|
123
|
+
var env = opts.env || process.env;
|
|
124
|
+
var cwd = opts.cwd || process.cwd();
|
|
125
|
+
|
|
126
|
+
// Test seams
|
|
127
|
+
var spawnFn = opts._spawn || function (cmd, sargs, sopts) {
|
|
128
|
+
return childProcess.spawn(cmd, sargs, sopts);
|
|
129
|
+
};
|
|
130
|
+
var watchFn = opts._watch || function (dir, wopts, listener) {
|
|
131
|
+
return fs.watch(dir, wopts, listener);
|
|
132
|
+
};
|
|
133
|
+
var setTimeoutFn = opts._setTimeout || setTimeout;
|
|
134
|
+
var clearTimeoutFn = opts._clearTimeout || clearTimeout;
|
|
135
|
+
|
|
136
|
+
var child = null;
|
|
137
|
+
var watchers = [];
|
|
138
|
+
var debounceTimer = null;
|
|
139
|
+
var killTimer = null;
|
|
140
|
+
var started = false;
|
|
141
|
+
var stopping = false;
|
|
142
|
+
var restarting = false;
|
|
143
|
+
var queuedRestart = false;
|
|
144
|
+
var restartCount = 0;
|
|
145
|
+
var lastRestartAt = null;
|
|
146
|
+
|
|
147
|
+
function _spawnChild() {
|
|
148
|
+
var c = spawnFn(opts.command, args, {
|
|
149
|
+
stdio: opts.stdio || "inherit",
|
|
150
|
+
env: env,
|
|
151
|
+
cwd: cwd,
|
|
152
|
+
});
|
|
153
|
+
c.on("exit", function (code, signal) {
|
|
154
|
+
// If the child exits while we're not stopping/restarting, that's
|
|
155
|
+
// a crash — log it and wait for a file change to retry. This
|
|
156
|
+
// matches nodemon's behavior: a bad commit leaves the child
|
|
157
|
+
// corpse around without spawn-thrashing.
|
|
158
|
+
if (!stopping && !restarting) {
|
|
159
|
+
_logVia(log, "warn",
|
|
160
|
+
"child exited unexpectedly — waiting for file change to restart",
|
|
161
|
+
{ code: code, signal: signal });
|
|
162
|
+
child = null;
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
c.on("error", function (err) {
|
|
166
|
+
_logVia(log, "error", "spawn error", { error: (err && err.message) || String(err) });
|
|
167
|
+
});
|
|
168
|
+
return c;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function _killChild() {
|
|
172
|
+
return new Promise(function (resolve) {
|
|
173
|
+
if (!child) { resolve(); return; }
|
|
174
|
+
var c = child;
|
|
175
|
+
var settled = false;
|
|
176
|
+
function done() {
|
|
177
|
+
if (settled) return;
|
|
178
|
+
settled = true;
|
|
179
|
+
if (killTimer) { try { clearTimeoutFn(killTimer); } catch (_e) { /* timer already cleared */ } killTimer = null; }
|
|
180
|
+
resolve();
|
|
181
|
+
}
|
|
182
|
+
c.once("exit", done);
|
|
183
|
+
try { c.kill(killSignal); }
|
|
184
|
+
catch (e) {
|
|
185
|
+
_logVia(log, "warn", "kill threw, child may already be gone",
|
|
186
|
+
{ error: (e && e.message) || String(e) });
|
|
187
|
+
done();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
// Hard-kill if the child ignores SIGTERM
|
|
191
|
+
killTimer = setTimeoutFn(function () {
|
|
192
|
+
if (settled) return;
|
|
193
|
+
_logVia(log, "warn",
|
|
194
|
+
"child did not exit after " + killTimeoutMs + "ms — sending SIGKILL");
|
|
195
|
+
try { c.kill("SIGKILL"); }
|
|
196
|
+
catch (_e) { done(); }
|
|
197
|
+
}, killTimeoutMs);
|
|
198
|
+
if (killTimer && typeof killTimer.unref === "function") killTimer.unref();
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function _restart() {
|
|
203
|
+
if (restarting) {
|
|
204
|
+
// Coalesce: a restart-in-flight gets one followup queued, never more.
|
|
205
|
+
queuedRestart = true;
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
restarting = true;
|
|
209
|
+
try {
|
|
210
|
+
await _killChild();
|
|
211
|
+
child = null;
|
|
212
|
+
if (stopping) return;
|
|
213
|
+
child = _spawnChild();
|
|
214
|
+
restartCount += 1;
|
|
215
|
+
lastRestartAt = new Date().toISOString();
|
|
216
|
+
_logVia(log, "info", "restarted",
|
|
217
|
+
{ pid: child && child.pid, restarts: restartCount });
|
|
218
|
+
} catch (e) {
|
|
219
|
+
_logVia(log, "error", "restart failed",
|
|
220
|
+
{ error: (e && e.message) || String(e) });
|
|
221
|
+
} finally {
|
|
222
|
+
restarting = false;
|
|
223
|
+
if (queuedRestart && !stopping) {
|
|
224
|
+
queuedRestart = false;
|
|
225
|
+
// Tail-call the queued restart on the next microtask so we
|
|
226
|
+
// unwind the current stack first.
|
|
227
|
+
Promise.resolve().then(_restart);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function _scheduleRestart(reason) {
|
|
233
|
+
if (stopping) return;
|
|
234
|
+
if (debounceTimer) clearTimeoutFn(debounceTimer);
|
|
235
|
+
debounceTimer = setTimeoutFn(function () {
|
|
236
|
+
debounceTimer = null;
|
|
237
|
+
_logVia(log, "info", "change detected, restarting", { reason: reason });
|
|
238
|
+
_restart();
|
|
239
|
+
}, graceMs);
|
|
240
|
+
if (debounceTimer && typeof debounceTimer.unref === "function") debounceTimer.unref();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function _onWatchEvent(dir, eventType, filename) {
|
|
244
|
+
if (!filename) return;
|
|
245
|
+
var rel = String(filename);
|
|
246
|
+
var full = path.join(dir, rel);
|
|
247
|
+
if (_matchesAny(ignore, rel) || _matchesAny(ignore, full)) return;
|
|
248
|
+
_scheduleRestart(eventType + ":" + rel);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function _armWatchers() {
|
|
252
|
+
for (var i = 0; i < watch.length; i++) {
|
|
253
|
+
(function (dir) {
|
|
254
|
+
var resolved = path.isAbsolute(dir) ? dir : path.resolve(cwd, dir);
|
|
255
|
+
var w;
|
|
256
|
+
try {
|
|
257
|
+
w = watchFn(resolved, { recursive: true, persistent: false }, function (eventType, filename) {
|
|
258
|
+
_onWatchEvent(resolved, eventType, filename);
|
|
259
|
+
});
|
|
260
|
+
} catch (e) {
|
|
261
|
+
// Missing dir, permission error, platform that doesn't
|
|
262
|
+
// support recursive — log and move on so a bad watch entry
|
|
263
|
+
// doesn't take the whole dev loop down.
|
|
264
|
+
_logVia(log, "warn", "could not watch " + resolved,
|
|
265
|
+
{ error: (e && e.message) || String(e) });
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (w && typeof w.on === "function") {
|
|
269
|
+
w.on("error", function (err) {
|
|
270
|
+
_logVia(log, "warn", "watcher error on " + resolved,
|
|
271
|
+
{ error: (err && err.message) || String(err) });
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
watchers.push(w);
|
|
275
|
+
})(watch[i]);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function _disarmWatchers() {
|
|
280
|
+
for (var i = 0; i < watchers.length; i++) {
|
|
281
|
+
try { if (watchers[i] && typeof watchers[i].close === "function") watchers[i].close(); }
|
|
282
|
+
catch (_e) { /* close best-effort */ }
|
|
283
|
+
}
|
|
284
|
+
watchers = [];
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function start() {
|
|
288
|
+
if (started) return;
|
|
289
|
+
started = true;
|
|
290
|
+
stopping = false;
|
|
291
|
+
_armWatchers();
|
|
292
|
+
child = _spawnChild();
|
|
293
|
+
_logVia(log, "info", "started", { pid: child && child.pid, watch: watch });
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function stop() {
|
|
297
|
+
if (!started) return;
|
|
298
|
+
stopping = true;
|
|
299
|
+
if (debounceTimer) { try { clearTimeoutFn(debounceTimer); } catch (_e) { /* timer already cleared */ } debounceTimer = null; }
|
|
300
|
+
_disarmWatchers();
|
|
301
|
+
await _killChild();
|
|
302
|
+
child = null;
|
|
303
|
+
started = false;
|
|
304
|
+
stopping = false;
|
|
305
|
+
queuedRestart = false;
|
|
306
|
+
_logVia(log, "info", "stopped", { restarts: restartCount });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function restart() { await _restart(); }
|
|
310
|
+
|
|
311
|
+
function stats() {
|
|
312
|
+
return {
|
|
313
|
+
pid: child && child.pid !== undefined ? child.pid : null,
|
|
314
|
+
running: !!child && started && !stopping,
|
|
315
|
+
restarts: restartCount,
|
|
316
|
+
lastRestartAt: lastRestartAt,
|
|
317
|
+
watchers: watchers.length,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
start: start,
|
|
323
|
+
stop: stop,
|
|
324
|
+
restart: restart,
|
|
325
|
+
stats: stats,
|
|
326
|
+
// Test hook: simulate a watcher event for engine tests
|
|
327
|
+
_emit: function (dir, eventType, filename) { _onWatchEvent(dir, eventType, filename); },
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
module.exports = {
|
|
332
|
+
create: create,
|
|
333
|
+
DevError: DevError,
|
|
334
|
+
DEFAULT_IGNORE: DEFAULT_IGNORE,
|
|
335
|
+
};
|