@blamejs/core 0.4.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/CHANGELOG.md +230 -0
- package/LICENSE +201 -0
- package/LTS-CALENDAR.md +29 -0
- package/MIGRATING.md +7 -0
- package/NOTICE +59 -0
- package/README.md +100 -0
- package/bin/blamejs.js +13 -0
- package/index.js +253 -0
- package/lib/api-key.js +705 -0
- package/lib/api-snapshot.js +335 -0
- package/lib/app-shutdown.js +381 -0
- package/lib/app.js +364 -0
- package/lib/atomic-file.js +525 -0
- package/lib/audit-chain.js +168 -0
- package/lib/audit-sign.js +319 -0
- package/lib/audit-tools.js +682 -0
- package/lib/audit.js +753 -0
- package/lib/auth/jwt.js +280 -0
- package/lib/auth/oauth.js +691 -0
- package/lib/auth/passkey.js +185 -0
- package/lib/auth/password.js +139 -0
- package/lib/auth/totp.js +17 -0
- package/lib/auth-header.js +81 -0
- package/lib/backup/bundle.js +219 -0
- package/lib/backup/crypto.js +174 -0
- package/lib/backup/index.js +490 -0
- package/lib/backup/manifest.js +275 -0
- package/lib/bundler.js +295 -0
- package/lib/cache.js +819 -0
- package/lib/chain-writer.js +234 -0
- package/lib/cli-helpers.js +201 -0
- package/lib/cli.js +1377 -0
- package/lib/cluster-provider-db.js +245 -0
- package/lib/cluster-storage.js +166 -0
- package/lib/cluster.js +691 -0
- package/lib/consent.js +222 -0
- package/lib/constants.js +186 -0
- package/lib/cookies.js +293 -0
- package/lib/credential-hash.js +303 -0
- package/lib/crypto-field.js +159 -0
- package/lib/crypto.js +250 -0
- package/lib/db-query.js +297 -0
- package/lib/db-schema.js +250 -0
- package/lib/db.js +1054 -0
- package/lib/deprecate.js +226 -0
- package/lib/dev.js +324 -0
- package/lib/error-page.js +424 -0
- package/lib/events.js +135 -0
- package/lib/external-db.js +422 -0
- package/lib/forms.js +378 -0
- package/lib/framework-error.js +189 -0
- package/lib/framework-schema.js +604 -0
- package/lib/handlers.js +350 -0
- package/lib/html-balance.js +227 -0
- package/lib/http-client.js +615 -0
- package/lib/i18n.js +780 -0
- package/lib/jobs.js +181 -0
- package/lib/lazy-require.js +48 -0
- package/lib/log-stream-local.js +137 -0
- package/lib/log-stream-webhook.js +170 -0
- package/lib/log-stream.js +211 -0
- package/lib/log.js +355 -0
- package/lib/mail-bounce.js +507 -0
- package/lib/mail.js +701 -0
- package/lib/metrics.js +647 -0
- package/lib/middleware/api-encrypt.js +553 -0
- package/lib/middleware/attach-user.js +156 -0
- package/lib/middleware/body-parser.js +883 -0
- package/lib/middleware/bot-guard.js +148 -0
- package/lib/middleware/compression.js +436 -0
- package/lib/middleware/cors.js +236 -0
- package/lib/middleware/csp-nonce.js +332 -0
- package/lib/middleware/csrf-protect.js +275 -0
- package/lib/middleware/error-handler.js +46 -0
- package/lib/middleware/health.js +358 -0
- package/lib/middleware/index.js +52 -0
- package/lib/middleware/rate-limit.js +319 -0
- package/lib/middleware/request-id.js +53 -0
- package/lib/middleware/require-auth.js +95 -0
- package/lib/middleware/security-headers.js +91 -0
- package/lib/migrations.js +353 -0
- package/lib/mtls-ca.js +333 -0
- package/lib/mtls-engine-default.js +285 -0
- package/lib/nonce-store.js +177 -0
- package/lib/notify.js +643 -0
- package/lib/ntp-check.js +178 -0
- package/lib/object-store/azure-blob.js +467 -0
- package/lib/object-store/gcs.js +469 -0
- package/lib/object-store/http-put.js +153 -0
- package/lib/object-store/index.js +140 -0
- package/lib/object-store/local.js +163 -0
- package/lib/object-store/retry.js +15 -0
- package/lib/object-store/sigv4.js +535 -0
- package/lib/observability.js +114 -0
- package/lib/pagination.js +371 -0
- package/lib/parsers/index.js +64 -0
- package/lib/parsers/safe-csv.js +224 -0
- package/lib/parsers/safe-env.js +614 -0
- package/lib/parsers/safe-toml.js +745 -0
- package/lib/parsers/safe-xml.js +379 -0
- package/lib/parsers/safe-yaml.js +977 -0
- package/lib/permissions.js +430 -0
- package/lib/pqc-agent.js +85 -0
- package/lib/pqc-gate.js +266 -0
- package/lib/protocol-dispatcher.js +144 -0
- package/lib/queue-local.js +327 -0
- package/lib/queue.js +430 -0
- package/lib/redact.js +192 -0
- package/lib/render.js +193 -0
- package/lib/request-helpers.js +178 -0
- package/lib/restore-bundle.js +239 -0
- package/lib/restore-rollback.js +254 -0
- package/lib/restore.js +301 -0
- package/lib/retry.js +329 -0
- package/lib/router.js +437 -0
- package/lib/safe-async.js +520 -0
- package/lib/safe-buffer.js +162 -0
- package/lib/safe-json.js +532 -0
- package/lib/safe-schema.js +1176 -0
- package/lib/safe-sql.js +157 -0
- package/lib/safe-url.js +109 -0
- package/lib/scheduler.js +680 -0
- package/lib/seeders.js +622 -0
- package/lib/session.js +304 -0
- package/lib/slug.js +243 -0
- package/lib/static.js +268 -0
- package/lib/storage.js +470 -0
- package/lib/subject.js +281 -0
- package/lib/template.js +781 -0
- package/lib/testing.js +621 -0
- package/lib/totp.js +285 -0
- package/lib/tracing.js +484 -0
- package/lib/validate-opts.js +56 -0
- package/lib/vault/index.js +299 -0
- package/lib/vault/passphrase-ops.js +311 -0
- package/lib/vault/passphrase-source.js +198 -0
- package/lib/vault/rotate.js +761 -0
- package/lib/vault/wrap.js +289 -0
- package/lib/vendor/MANIFEST.json +84 -0
- package/lib/vendor/argon2/argon2.cjs +466 -0
- package/lib/vendor/argon2/argon2.d.cts +62 -0
- package/lib/vendor/argon2/package.json +1 -0
- package/lib/vendor/argon2/prebuilds/darwin-arm64/argon2.armv8.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/darwin-x64/argon2.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/freebsd-arm64/argon2.armv8.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/freebsd-x64/argon2.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.musl.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.musl.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-x64/argon2.glibc.node +0 -0
- package/lib/vendor/argon2/prebuilds/linux-x64/argon2.musl.node +0 -0
- package/lib/vendor/argon2/prebuilds/win32-x64/argon2.glibc.node +0 -0
- package/lib/vendor/noble-ciphers.cjs +9 -0
- package/lib/vendor/pki.cjs +181 -0
- package/lib/vendor/simplewebauthn-server.cjs +328 -0
- package/lib/webhook.js +632 -0
- package/lib/websocket-channels.js +413 -0
- package/lib/websocket.js +833 -0
- package/package.json +39 -0
package/lib/deprecate.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* deprecate — runtime deprecation API for the framework's LTS contract.
|
|
4
|
+
*
|
|
5
|
+
* Operators see a one-time stderr warning the first time deprecated
|
|
6
|
+
* surface is used, with the version it was deprecated in and the
|
|
7
|
+
* version it'll be removed in. Production runs suppress warnings by
|
|
8
|
+
* default (operators don't want stderr noise from deprecated paths in
|
|
9
|
+
* code they don't own), but BLAMEJS_DEPRECATIONS env var inverts that
|
|
10
|
+
* for visibility-on-demand.
|
|
11
|
+
*
|
|
12
|
+
* var dep = b.deprecate;
|
|
13
|
+
*
|
|
14
|
+
* // Direct warning at the call site
|
|
15
|
+
* dep.warn("auth.legacyVerify", {
|
|
16
|
+
* since: "0.2.0",
|
|
17
|
+
* removeIn: "0.4.0",
|
|
18
|
+
* message: "use auth.password.verify(stored, plain) instead",
|
|
19
|
+
* hint: "see MIGRATING.md#0-2-to-0-4",
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* // Wrap an old function so calls trigger the warning automatically
|
|
23
|
+
* var legacyVerify = dep.wrap(newVerify, "auth.legacyVerify", {
|
|
24
|
+
* since: "0.2.0", removeIn: "0.4.0",
|
|
25
|
+
* message: "renamed to auth.password.verify",
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* // Mark a property as deprecated; access triggers the warning
|
|
29
|
+
* dep.alias(targetObj, "oldKey", "newKey", {
|
|
30
|
+
* since: "0.2.0", removeIn: "0.4.0",
|
|
31
|
+
* });
|
|
32
|
+
*
|
|
33
|
+
* dep.list(); // → [{ name, since, removeIn, callCount, firstSeen }]
|
|
34
|
+
* dep.reset(); // clears the seen-set; tests
|
|
35
|
+
*
|
|
36
|
+
* BLAMEJS_DEPRECATIONS env var controls runtime behavior:
|
|
37
|
+
* "warn" — stderr warning on first use of each (name, since) pair
|
|
38
|
+
* (default outside production)
|
|
39
|
+
* "silent" — skip entirely (default in production)
|
|
40
|
+
* "error" — throw on first use; development tool to surface every
|
|
41
|
+
* deprecated call site as a hard failure during a sweep
|
|
42
|
+
*
|
|
43
|
+
* Mode resolution order:
|
|
44
|
+
* 1. process.env.BLAMEJS_DEPRECATIONS if set
|
|
45
|
+
* 2. "silent" when process.env.NODE_ENV === "production"
|
|
46
|
+
* 3. "warn" otherwise
|
|
47
|
+
*
|
|
48
|
+
* Warnings dedupe by (name, since) — calling deprecate.warn(...) ten
|
|
49
|
+
* thousand times with the same args produces one stderr line. The
|
|
50
|
+
* call counter is still incremented so dep.list() shows usage volume.
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
var { FrameworkError } = require("./framework-error");
|
|
54
|
+
|
|
55
|
+
class DeprecateError extends FrameworkError {
|
|
56
|
+
constructor(code, message) {
|
|
57
|
+
super(message, code);
|
|
58
|
+
this.name = "DeprecateError";
|
|
59
|
+
this.permanent = true;
|
|
60
|
+
this.isDeprecateError = true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Map of "<name>:<since>" → { name, since, removeIn, callCount, firstSeen }
|
|
65
|
+
var _seen = new Map();
|
|
66
|
+
|
|
67
|
+
function _modeFromEnv() {
|
|
68
|
+
var env = process.env.BLAMEJS_DEPRECATIONS;
|
|
69
|
+
if (typeof env === "string" && env.length > 0) {
|
|
70
|
+
var v = env.toLowerCase();
|
|
71
|
+
if (v === "warn" || v === "silent" || v === "error") return v;
|
|
72
|
+
}
|
|
73
|
+
if (process.env.NODE_ENV === "production") return "silent";
|
|
74
|
+
return "warn";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function _format(name, opts) {
|
|
78
|
+
opts = opts || {};
|
|
79
|
+
var line = "[blamejs:deprecated] " + name;
|
|
80
|
+
if (opts.since) line += " (since " + opts.since + ")";
|
|
81
|
+
if (opts.removeIn) line += "; removed in " + opts.removeIn;
|
|
82
|
+
if (opts.message) line += " — " + opts.message;
|
|
83
|
+
if (opts.hint) line += " · " + opts.hint;
|
|
84
|
+
return line;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function _validateOpts(opts, fnName) {
|
|
88
|
+
if (!opts || typeof opts !== "object") {
|
|
89
|
+
throw new DeprecateError("deprecate/bad-opts",
|
|
90
|
+
fnName + ": opts is required (with at least 'since' and 'removeIn')");
|
|
91
|
+
}
|
|
92
|
+
if (typeof opts.since !== "string" || opts.since.length === 0) {
|
|
93
|
+
throw new DeprecateError("deprecate/bad-opts",
|
|
94
|
+
fnName + ": opts.since (version string) is required");
|
|
95
|
+
}
|
|
96
|
+
if (typeof opts.removeIn !== "string" || opts.removeIn.length === 0) {
|
|
97
|
+
throw new DeprecateError("deprecate/bad-opts",
|
|
98
|
+
fnName + ": opts.removeIn (version string) is required");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function warn(name, opts) {
|
|
103
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
104
|
+
throw new DeprecateError("deprecate/bad-name",
|
|
105
|
+
"warn: name is required (the public identifier being deprecated)");
|
|
106
|
+
}
|
|
107
|
+
_validateOpts(opts, "warn");
|
|
108
|
+
|
|
109
|
+
var key = name + ":" + opts.since;
|
|
110
|
+
var entry = _seen.get(key);
|
|
111
|
+
if (!entry) {
|
|
112
|
+
entry = {
|
|
113
|
+
name: name,
|
|
114
|
+
since: opts.since,
|
|
115
|
+
removeIn: opts.removeIn,
|
|
116
|
+
message: opts.message || null,
|
|
117
|
+
hint: opts.hint || null,
|
|
118
|
+
callCount: 0,
|
|
119
|
+
firstSeen: new Date().toISOString(),
|
|
120
|
+
};
|
|
121
|
+
_seen.set(key, entry);
|
|
122
|
+
}
|
|
123
|
+
entry.callCount++;
|
|
124
|
+
|
|
125
|
+
var mode = _modeFromEnv();
|
|
126
|
+
if (mode === "silent") return;
|
|
127
|
+
|
|
128
|
+
// Emit on first occurrence only (dedupe)
|
|
129
|
+
if (entry.callCount > 1) return;
|
|
130
|
+
|
|
131
|
+
var line = _format(name, opts);
|
|
132
|
+
if (mode === "error") {
|
|
133
|
+
throw new DeprecateError("deprecate/used-in-error-mode",
|
|
134
|
+
line + " — BLAMEJS_DEPRECATIONS=error in effect");
|
|
135
|
+
}
|
|
136
|
+
// mode === "warn"
|
|
137
|
+
try { process.stderr.write(line + "\n"); }
|
|
138
|
+
catch (_e) { /* stderr write best-effort */ }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Wrap a function so calling it issues a deprecation warning + delegates.
|
|
142
|
+
// The wrapper preserves the original function's `.length` (arity) so
|
|
143
|
+
// callers introspecting it as a callable see the same shape.
|
|
144
|
+
function wrap(fn, name, opts) {
|
|
145
|
+
if (typeof fn !== "function") {
|
|
146
|
+
throw new DeprecateError("deprecate/bad-target",
|
|
147
|
+
"wrap: first arg must be the replacement function (the new API)");
|
|
148
|
+
}
|
|
149
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
150
|
+
throw new DeprecateError("deprecate/bad-name",
|
|
151
|
+
"wrap: name is required (the deprecated identifier)");
|
|
152
|
+
}
|
|
153
|
+
_validateOpts(opts, "wrap");
|
|
154
|
+
var wrapper = function () {
|
|
155
|
+
warn(name, opts);
|
|
156
|
+
return fn.apply(this, arguments);
|
|
157
|
+
};
|
|
158
|
+
// Preserve identity hints
|
|
159
|
+
Object.defineProperty(wrapper, "name", { value: name + ":deprecated", configurable: true });
|
|
160
|
+
return wrapper;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Define `oldKey` on `target` as a getter that warns then returns
|
|
164
|
+
// `target[newKey]`. The setter writes through so existing assignments
|
|
165
|
+
// still work, but the getter access trips the warning.
|
|
166
|
+
function alias(target, oldKey, newKey, opts) {
|
|
167
|
+
if (!target || typeof target !== "object") {
|
|
168
|
+
throw new DeprecateError("deprecate/bad-target",
|
|
169
|
+
"alias: target must be an object");
|
|
170
|
+
}
|
|
171
|
+
if (typeof oldKey !== "string" || oldKey.length === 0) {
|
|
172
|
+
throw new DeprecateError("deprecate/bad-name",
|
|
173
|
+
"alias: oldKey is required");
|
|
174
|
+
}
|
|
175
|
+
if (typeof newKey !== "string" || newKey.length === 0) {
|
|
176
|
+
throw new DeprecateError("deprecate/bad-name",
|
|
177
|
+
"alias: newKey is required");
|
|
178
|
+
}
|
|
179
|
+
_validateOpts(opts, "alias");
|
|
180
|
+
var aliasName = opts.aliasName ||
|
|
181
|
+
((target.constructor && target.constructor.name &&
|
|
182
|
+
target.constructor.name !== "Object" ? target.constructor.name + "." : "") + oldKey);
|
|
183
|
+
var fullOpts = Object.assign({
|
|
184
|
+
message: "use '" + newKey + "' instead",
|
|
185
|
+
}, opts);
|
|
186
|
+
Object.defineProperty(target, oldKey, {
|
|
187
|
+
configurable: true,
|
|
188
|
+
enumerable: false,
|
|
189
|
+
get: function () { warn(aliasName, fullOpts); return target[newKey]; },
|
|
190
|
+
set: function (v) { warn(aliasName, fullOpts); target[newKey] = v; },
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function list() {
|
|
195
|
+
var out = [];
|
|
196
|
+
_seen.forEach(function (v) {
|
|
197
|
+
out.push({
|
|
198
|
+
name: v.name,
|
|
199
|
+
since: v.since,
|
|
200
|
+
removeIn: v.removeIn,
|
|
201
|
+
callCount: v.callCount,
|
|
202
|
+
firstSeen: v.firstSeen,
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
// Stable order: most-frequent first, ties broken by first-seen
|
|
206
|
+
out.sort(function (a, b) {
|
|
207
|
+
if (a.callCount !== b.callCount) return b.callCount - a.callCount;
|
|
208
|
+
return a.firstSeen < b.firstSeen ? -1 : 1;
|
|
209
|
+
});
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function reset() { _seen.clear(); }
|
|
214
|
+
|
|
215
|
+
// Export the resolved mode so tests + ops dashboards can introspect
|
|
216
|
+
function getMode() { return _modeFromEnv(); }
|
|
217
|
+
|
|
218
|
+
module.exports = {
|
|
219
|
+
warn: warn,
|
|
220
|
+
wrap: wrap,
|
|
221
|
+
alias: alias,
|
|
222
|
+
list: list,
|
|
223
|
+
reset: reset,
|
|
224
|
+
getMode: getMode,
|
|
225
|
+
DeprecateError: DeprecateError,
|
|
226
|
+
};
|
package/lib/dev.js
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
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 { FrameworkError } = require("./framework-error");
|
|
52
|
+
|
|
53
|
+
class DevError extends FrameworkError {
|
|
54
|
+
constructor(code, message) {
|
|
55
|
+
super(message, code);
|
|
56
|
+
this.name = "DevError";
|
|
57
|
+
this.permanent = true;
|
|
58
|
+
this.isDevError = true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
var DEFAULT_GRACE_MS = 250;
|
|
63
|
+
var DEFAULT_KILL_TIMEOUT_MS = 4000;
|
|
64
|
+
var DEFAULT_KILL_SIGNAL = "SIGTERM";
|
|
65
|
+
var DEFAULT_IGNORE = [
|
|
66
|
+
/node_modules/,
|
|
67
|
+
/\.git\b/,
|
|
68
|
+
/^\./, // dotfiles + dotdirs
|
|
69
|
+
/\.db$/, /\.db-journal$/, /\.db-wal$/, /\.db-shm$/,
|
|
70
|
+
/\.log$/,
|
|
71
|
+
/\.swp$/, /~$/, // editor scratch files
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
function _matchesAny(patterns, value) {
|
|
75
|
+
for (var i = 0; i < patterns.length; i++) {
|
|
76
|
+
var p = patterns[i];
|
|
77
|
+
if (p instanceof RegExp) { if (p.test(value)) return true; }
|
|
78
|
+
else if (typeof p === "string") { if (value.indexOf(p) !== -1) return true; }
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function _logVia(log, level, message, fields) {
|
|
84
|
+
if (log && typeof log[level] === "function") {
|
|
85
|
+
try { log[level](message, fields); }
|
|
86
|
+
catch (_e) { /* logger best-effort */ }
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// Fallback: prefixed line on stdout/stderr
|
|
90
|
+
var line = "[blamejs:dev] " + message + (fields ? " " + JSON.stringify(fields) : "");
|
|
91
|
+
if (level === "error" || level === "warn" || level === "fatal") console.error(line);
|
|
92
|
+
else console.log(line);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function create(opts) {
|
|
96
|
+
opts = opts || {};
|
|
97
|
+
if (typeof opts.command !== "string" || opts.command.length === 0) {
|
|
98
|
+
throw new DevError("dev/no-command",
|
|
99
|
+
"dev.create requires opts.command (the program to spawn)");
|
|
100
|
+
}
|
|
101
|
+
var args = Array.isArray(opts.args) ? opts.args.slice() : [];
|
|
102
|
+
var watch = Array.isArray(opts.watch) && opts.watch.length > 0
|
|
103
|
+
? opts.watch.slice() : ["."];
|
|
104
|
+
var ignore = Array.isArray(opts.ignore)
|
|
105
|
+
? DEFAULT_IGNORE.concat(opts.ignore)
|
|
106
|
+
: DEFAULT_IGNORE.slice();
|
|
107
|
+
var graceMs = typeof opts.graceMs === "number" && opts.graceMs >= 0
|
|
108
|
+
? opts.graceMs : DEFAULT_GRACE_MS;
|
|
109
|
+
var killSignal = typeof opts.killSignal === "string" ? opts.killSignal : DEFAULT_KILL_SIGNAL;
|
|
110
|
+
var killTimeoutMs = typeof opts.killTimeoutMs === "number" && opts.killTimeoutMs >= 0
|
|
111
|
+
? opts.killTimeoutMs : DEFAULT_KILL_TIMEOUT_MS;
|
|
112
|
+
var log = opts.log || null;
|
|
113
|
+
var env = opts.env || process.env;
|
|
114
|
+
var cwd = opts.cwd || process.cwd();
|
|
115
|
+
|
|
116
|
+
// Test seams
|
|
117
|
+
var spawnFn = opts._spawn || function (cmd, sargs, sopts) {
|
|
118
|
+
return require("child_process").spawn(cmd, sargs, sopts);
|
|
119
|
+
};
|
|
120
|
+
var watchFn = opts._watch || function (dir, wopts, listener) {
|
|
121
|
+
return require("fs").watch(dir, wopts, listener);
|
|
122
|
+
};
|
|
123
|
+
var setTimeoutFn = opts._setTimeout || setTimeout;
|
|
124
|
+
var clearTimeoutFn = opts._clearTimeout || clearTimeout;
|
|
125
|
+
|
|
126
|
+
var child = null;
|
|
127
|
+
var watchers = [];
|
|
128
|
+
var debounceTimer = null;
|
|
129
|
+
var killTimer = null;
|
|
130
|
+
var started = false;
|
|
131
|
+
var stopping = false;
|
|
132
|
+
var restarting = false;
|
|
133
|
+
var queuedRestart = false;
|
|
134
|
+
var restartCount = 0;
|
|
135
|
+
var lastRestartAt = null;
|
|
136
|
+
|
|
137
|
+
function _spawnChild() {
|
|
138
|
+
var c = spawnFn(opts.command, args, {
|
|
139
|
+
stdio: opts.stdio || "inherit",
|
|
140
|
+
env: env,
|
|
141
|
+
cwd: cwd,
|
|
142
|
+
});
|
|
143
|
+
c.on("exit", function (code, signal) {
|
|
144
|
+
// If the child exits while we're not stopping/restarting, that's
|
|
145
|
+
// a crash — log it and wait for a file change to retry. This
|
|
146
|
+
// matches nodemon's behavior: a bad commit leaves the child
|
|
147
|
+
// corpse around without spawn-thrashing.
|
|
148
|
+
if (!stopping && !restarting) {
|
|
149
|
+
_logVia(log, "warn",
|
|
150
|
+
"child exited unexpectedly — waiting for file change to restart",
|
|
151
|
+
{ code: code, signal: signal });
|
|
152
|
+
child = null;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
c.on("error", function (err) {
|
|
156
|
+
_logVia(log, "error", "spawn error", { error: (err && err.message) || String(err) });
|
|
157
|
+
});
|
|
158
|
+
return c;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function _killChild() {
|
|
162
|
+
return new Promise(function (resolve) {
|
|
163
|
+
if (!child) return resolve();
|
|
164
|
+
var c = child;
|
|
165
|
+
var settled = false;
|
|
166
|
+
function done() {
|
|
167
|
+
if (settled) return;
|
|
168
|
+
settled = true;
|
|
169
|
+
if (killTimer) { try { clearTimeoutFn(killTimer); } catch (_e) {} killTimer = null; }
|
|
170
|
+
resolve();
|
|
171
|
+
}
|
|
172
|
+
c.once("exit", done);
|
|
173
|
+
try { c.kill(killSignal); }
|
|
174
|
+
catch (e) {
|
|
175
|
+
_logVia(log, "warn", "kill threw, child may already be gone",
|
|
176
|
+
{ error: (e && e.message) || String(e) });
|
|
177
|
+
return done();
|
|
178
|
+
}
|
|
179
|
+
// Hard-kill if the child ignores SIGTERM
|
|
180
|
+
killTimer = setTimeoutFn(function () {
|
|
181
|
+
if (settled) return;
|
|
182
|
+
_logVia(log, "warn",
|
|
183
|
+
"child did not exit after " + killTimeoutMs + "ms — sending SIGKILL");
|
|
184
|
+
try { c.kill("SIGKILL"); }
|
|
185
|
+
catch (_e) { done(); }
|
|
186
|
+
}, killTimeoutMs);
|
|
187
|
+
if (killTimer && typeof killTimer.unref === "function") killTimer.unref();
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function _restart() {
|
|
192
|
+
if (restarting) {
|
|
193
|
+
// Coalesce: a restart-in-flight gets one followup queued, never more.
|
|
194
|
+
queuedRestart = true;
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
restarting = true;
|
|
198
|
+
try {
|
|
199
|
+
await _killChild();
|
|
200
|
+
child = null;
|
|
201
|
+
if (stopping) return;
|
|
202
|
+
child = _spawnChild();
|
|
203
|
+
restartCount += 1;
|
|
204
|
+
lastRestartAt = new Date().toISOString();
|
|
205
|
+
_logVia(log, "info", "restarted",
|
|
206
|
+
{ pid: child && child.pid, restarts: restartCount });
|
|
207
|
+
} catch (e) {
|
|
208
|
+
_logVia(log, "error", "restart failed",
|
|
209
|
+
{ error: (e && e.message) || String(e) });
|
|
210
|
+
} finally {
|
|
211
|
+
restarting = false;
|
|
212
|
+
if (queuedRestart && !stopping) {
|
|
213
|
+
queuedRestart = false;
|
|
214
|
+
// Tail-call the queued restart on the next microtask so we
|
|
215
|
+
// unwind the current stack first.
|
|
216
|
+
Promise.resolve().then(_restart);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function _scheduleRestart(reason) {
|
|
222
|
+
if (stopping) return;
|
|
223
|
+
if (debounceTimer) clearTimeoutFn(debounceTimer);
|
|
224
|
+
debounceTimer = setTimeoutFn(function () {
|
|
225
|
+
debounceTimer = null;
|
|
226
|
+
_logVia(log, "info", "change detected, restarting", { reason: reason });
|
|
227
|
+
_restart();
|
|
228
|
+
}, graceMs);
|
|
229
|
+
if (debounceTimer && typeof debounceTimer.unref === "function") debounceTimer.unref();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function _onWatchEvent(dir, eventType, filename) {
|
|
233
|
+
if (!filename) return;
|
|
234
|
+
var rel = String(filename);
|
|
235
|
+
var full = path.join(dir, rel);
|
|
236
|
+
if (_matchesAny(ignore, rel) || _matchesAny(ignore, full)) return;
|
|
237
|
+
_scheduleRestart(eventType + ":" + rel);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function _armWatchers() {
|
|
241
|
+
for (var i = 0; i < watch.length; i++) {
|
|
242
|
+
(function (dir) {
|
|
243
|
+
var resolved = path.isAbsolute(dir) ? dir : path.resolve(cwd, dir);
|
|
244
|
+
var w;
|
|
245
|
+
try {
|
|
246
|
+
w = watchFn(resolved, { recursive: true, persistent: false }, function (eventType, filename) {
|
|
247
|
+
_onWatchEvent(resolved, eventType, filename);
|
|
248
|
+
});
|
|
249
|
+
} catch (e) {
|
|
250
|
+
// Missing dir, permission error, platform that doesn't
|
|
251
|
+
// support recursive — log and move on so a bad watch entry
|
|
252
|
+
// doesn't take the whole dev loop down.
|
|
253
|
+
_logVia(log, "warn", "could not watch " + resolved,
|
|
254
|
+
{ error: (e && e.message) || String(e) });
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (w && typeof w.on === "function") {
|
|
258
|
+
w.on("error", function (err) {
|
|
259
|
+
_logVia(log, "warn", "watcher error on " + resolved,
|
|
260
|
+
{ error: (err && err.message) || String(err) });
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
watchers.push(w);
|
|
264
|
+
})(watch[i]);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function _disarmWatchers() {
|
|
269
|
+
for (var i = 0; i < watchers.length; i++) {
|
|
270
|
+
try { if (watchers[i] && typeof watchers[i].close === "function") watchers[i].close(); }
|
|
271
|
+
catch (_e) { /* close best-effort */ }
|
|
272
|
+
}
|
|
273
|
+
watchers = [];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function start() {
|
|
277
|
+
if (started) return;
|
|
278
|
+
started = true;
|
|
279
|
+
stopping = false;
|
|
280
|
+
_armWatchers();
|
|
281
|
+
child = _spawnChild();
|
|
282
|
+
_logVia(log, "info", "started", { pid: child && child.pid, watch: watch });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function stop() {
|
|
286
|
+
if (!started) return;
|
|
287
|
+
stopping = true;
|
|
288
|
+
if (debounceTimer) { try { clearTimeoutFn(debounceTimer); } catch (_e) {} debounceTimer = null; }
|
|
289
|
+
_disarmWatchers();
|
|
290
|
+
await _killChild();
|
|
291
|
+
child = null;
|
|
292
|
+
started = false;
|
|
293
|
+
stopping = false;
|
|
294
|
+
queuedRestart = false;
|
|
295
|
+
_logVia(log, "info", "stopped", { restarts: restartCount });
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function restart() { await _restart(); }
|
|
299
|
+
|
|
300
|
+
function stats() {
|
|
301
|
+
return {
|
|
302
|
+
pid: child && child.pid !== undefined ? child.pid : null,
|
|
303
|
+
running: !!child && started && !stopping,
|
|
304
|
+
restarts: restartCount,
|
|
305
|
+
lastRestartAt: lastRestartAt,
|
|
306
|
+
watchers: watchers.length,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
start: start,
|
|
312
|
+
stop: stop,
|
|
313
|
+
restart: restart,
|
|
314
|
+
stats: stats,
|
|
315
|
+
// Test hook: simulate a watcher event for engine tests
|
|
316
|
+
_emit: function (dir, eventType, filename) { _onWatchEvent(dir, eventType, filename); },
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
module.exports = {
|
|
321
|
+
create: create,
|
|
322
|
+
DevError: DevError,
|
|
323
|
+
DEFAULT_IGNORE: DEFAULT_IGNORE,
|
|
324
|
+
};
|