@camstack/server 1.2.277 → 1.2.278
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/dist/agent/agent-update-service.js +6 -0
- package/dist/api/core/logging-settings.js +24 -1
- package/dist/closure-patches/patch-reach.json +7 -4
- package/dist/core/server-update/server-update.service.js +6 -0
- package/dist/core/server-update/system-apply-closure-patches.js +105 -0
- package/dist/server-root/index.js +23 -1
- package/package.json +13 -13
- package/scripts/closure-postinstall.mjs +11 -2
- package/dist/core/addon/require-cache.js +0 -106
|
@@ -66,6 +66,7 @@ const path = __importStar(require("node:path"));
|
|
|
66
66
|
const system_1 = require("@camstack/system");
|
|
67
67
|
const types_1 = require("@camstack/types");
|
|
68
68
|
const system_ensure_prebuilds_js_1 = require("../core/server-update/system-ensure-prebuilds.js");
|
|
69
|
+
const system_apply_closure_patches_js_1 = require("../core/server-update/system-apply-closure-patches.js");
|
|
69
70
|
const system_exec_npm_js_1 = require("../core/server-update/system-exec-npm.js");
|
|
70
71
|
const update_availability_emitter_js_1 = require("../core/update-availability-emitter.js");
|
|
71
72
|
const update_availability_store_js_1 = require("../core/update-availability-store.js");
|
|
@@ -194,6 +195,11 @@ class AgentUpdateService extends index_js_1.RootUpdateService {
|
|
|
194
195
|
// ships the identical bare Node, so its `applyServerUpdate` staging would
|
|
195
196
|
// hit the same source-only `better-sqlite3` miss without this.
|
|
196
197
|
ensureNativePrebuilds: options.ensureNativePrebuilds ?? (0, system_ensure_prebuilds_js_1.buildSystemEnsureNativePrebuilds)(options.logger),
|
|
198
|
+
// Apply the closure patches ourselves. npm 12 blocks the root package's
|
|
199
|
+
// postinstall by default, and a silently-unpatched closure is refused by
|
|
200
|
+
// the validator — an update that can never apply. Idempotent, so this
|
|
201
|
+
// costs nothing where npm did run the lifecycle.
|
|
202
|
+
applyClosurePatches: options.applyClosurePatches ?? (0, system_apply_closure_patches_js_1.buildSystemApplyClosurePatches)(options.logger),
|
|
197
203
|
env: options.env,
|
|
198
204
|
now: options.now,
|
|
199
205
|
});
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
* over the window (D245).
|
|
62
62
|
*/
|
|
63
63
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
64
|
-
exports.LoggingSettingsService = exports.MAX_CHANNEL_WINDOW_MS = exports.EMPTY_LOG_CHANNEL_PLANE = exports.HUB_NODE_ID = exports.LOGGING_SETTINGS_KEY = void 0;
|
|
64
|
+
exports.LoggingSettingsService = exports.MAX_CHANNEL_WINDOW_MS = exports.EMPTY_LOG_CHANNEL_PLANE = exports.OPS_LOG_RETENTION_DEFAULT_DAYS = exports.HUB_NODE_ID = exports.LOGGING_SETTINGS_KEY = void 0;
|
|
65
65
|
exports.buildLogChannelPlane = buildLogChannelPlane;
|
|
66
66
|
exports.toLoggingSettingsRecord = toLoggingSettingsRecord;
|
|
67
67
|
exports.resolveLevel = resolveLevel;
|
|
@@ -79,6 +79,22 @@ const SYSTEM_SETTINGS_COLLECTION = 'system-settings';
|
|
|
79
79
|
/** The node a hub speaks for when a caller does not name one. */
|
|
80
80
|
exports.HUB_NODE_ID = 'hub';
|
|
81
81
|
const LOG_LEVELS = ['debug', 'info', 'warn', 'error'];
|
|
82
|
+
/**
|
|
83
|
+
* The persisted row. Every field optional — an older row must still read, and
|
|
84
|
+
* a field this version does not know about must survive a write by a version
|
|
85
|
+
* that does not know it either.
|
|
86
|
+
*/
|
|
87
|
+
/**
|
|
88
|
+
* How long the analytics ops log is kept when nothing is configured.
|
|
89
|
+
*
|
|
90
|
+
* Thirty days, the number that was hardcoded in `pipeline-analytics/index.ts`
|
|
91
|
+
* until D485. It is longer than any device retention window on purpose:
|
|
92
|
+
* `retention-model.ts` calls it "the one root that must NOT follow a device
|
|
93
|
+
* window … the audit trail is not history of the camera, it is history of the
|
|
94
|
+
* sweep". That reasoning is why the value stays 30 by default; it was never a
|
|
95
|
+
* reason for the value to be unreachable.
|
|
96
|
+
*/
|
|
97
|
+
exports.OPS_LOG_RETENTION_DEFAULT_DAYS = 30;
|
|
82
98
|
/** A plane that reaches nothing. The shape of a hub with no addon booted yet. */
|
|
83
99
|
exports.EMPTY_LOG_CHANNEL_PLANE = {
|
|
84
100
|
declarations: async () => [],
|
|
@@ -184,6 +200,12 @@ class LoggingSettingsService {
|
|
|
184
200
|
if (levelChanged) {
|
|
185
201
|
next = mergeLevel(next, scope, component, patch.level ?? null, this.now());
|
|
186
202
|
}
|
|
203
|
+
if (patch.opsLogRetentionDays !== undefined) {
|
|
204
|
+
// A scalar, merged like any other field. Absent leaves it alone — the
|
|
205
|
+
// patch contract: arming a channel must not silently rewrite how long the
|
|
206
|
+
// audit trail lives.
|
|
207
|
+
next = { ...next, opsLogRetentionDays: patch.opsLogRetentionDays, updatedAt: this.now() };
|
|
208
|
+
}
|
|
187
209
|
if (channelsChanged) {
|
|
188
210
|
// Validated BEFORE anything is written: a patch that names an
|
|
189
211
|
// undeclared channel, or narrows a channel whose lines carry no
|
|
@@ -285,6 +307,7 @@ class LoggingSettingsService {
|
|
|
285
307
|
activeWindows: await this.describeWindows(),
|
|
286
308
|
channels: await this.declaredChannels(),
|
|
287
309
|
activeChannels: describeChannelWindows(record ?? {}, this.now()),
|
|
310
|
+
opsLogRetentionDays: record?.opsLogRetentionDays ?? exports.OPS_LOG_RETENTION_DEFAULT_DAYS,
|
|
288
311
|
persisted: record !== null,
|
|
289
312
|
};
|
|
290
313
|
}
|
|
@@ -74,10 +74,13 @@
|
|
|
74
74
|
"and no forceTurn guard.",
|
|
75
75
|
"",
|
|
76
76
|
"It now reaches production because `@camstack/server` ships these patches",
|
|
77
|
-
"in `dist/closure-patches/` and applies them from its own `postinstall
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
77
|
+
"in `dist/closure-patches/` and applies them from its own `postinstall`.",
|
|
78
|
+
"That alone was NOT enough (D484): npm 12 blocks a dependency's install",
|
|
79
|
+
"scripts by default, so on the Mac-mini agent the postinstall never ran and",
|
|
80
|
+
"the closure came out unpatched with exit 0. `stageClosure` now also",
|
|
81
|
+
"invokes the applier directly, and the staging manifest allow-lists the",
|
|
82
|
+
"packages whose scripts must run. A closure where a marker did not land is",
|
|
83
|
+
"still refused by `findUnappliedClosurePatches` before it can be swapped in.",
|
|
81
84
|
"",
|
|
82
85
|
"The version is EXACT, in this file and in all three declaring packages.",
|
|
83
86
|
"A caret would let npm resolve 0.22.10 into the closure, the patch would",
|
|
@@ -55,6 +55,7 @@ const path = __importStar(require("node:path"));
|
|
|
55
55
|
const index_js_1 = require("../../server-root/index.js");
|
|
56
56
|
const update_availability_emitter_js_1 = require("../update-availability-emitter.js");
|
|
57
57
|
const system_ensure_prebuilds_js_1 = require("./system-ensure-prebuilds.js");
|
|
58
|
+
const system_apply_closure_patches_js_1 = require("./system-apply-closure-patches.js");
|
|
58
59
|
const system_exec_npm_js_1 = require("./system-exec-npm.js");
|
|
59
60
|
class ServerUpdateService extends index_js_1.RootUpdateService {
|
|
60
61
|
updateAvailability;
|
|
@@ -95,6 +96,11 @@ class ServerUpdateService extends index_js_1.RootUpdateService {
|
|
|
95
96
|
// native-prebuild validator refuses the swap (toolchain-less packaged
|
|
96
97
|
// desktop). No-op on docker where the install already carried it.
|
|
97
98
|
ensureNativePrebuilds: options.ensureNativePrebuilds ?? (0, system_ensure_prebuilds_js_1.buildSystemEnsureNativePrebuilds)(options.logger),
|
|
99
|
+
// Apply the closure patches ourselves. npm 12 blocks the root package's
|
|
100
|
+
// postinstall by default, and a silently-unpatched closure is refused by
|
|
101
|
+
// the validator — an update that can never apply. Idempotent, so this
|
|
102
|
+
// costs nothing where npm did run the lifecycle.
|
|
103
|
+
applyClosurePatches: options.applyClosurePatches ?? (0, system_apply_closure_patches_js_1.buildSystemApplyClosurePatches)(options.logger),
|
|
98
104
|
env: options.env,
|
|
99
105
|
now: options.now,
|
|
100
106
|
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.buildSystemApplyClosurePatches = buildSystemApplyClosurePatches;
|
|
37
|
+
/**
|
|
38
|
+
* `buildSystemApplyClosurePatches` — run the root package's OWN shipped patch
|
|
39
|
+
* applier inside a staged closure, without going through npm.
|
|
40
|
+
*
|
|
41
|
+
* ## Why this exists
|
|
42
|
+
*
|
|
43
|
+
* `@camstack/server` applies its `closure-postinstall` patches (D478 — werift)
|
|
44
|
+
* from its `postinstall`. npm 12 blocks a dependency's install scripts unless
|
|
45
|
+
* the installing manifest allow-lists it, and the block is a WARNING, not an
|
|
46
|
+
* error: the install succeeds, the patch silently does not land, and the only
|
|
47
|
+
* thing that notices is `findUnappliedClosurePatches` refusing the swap.
|
|
48
|
+
*
|
|
49
|
+
* Measured 2026-09-13 on the Mac-mini agent, which had bootstrapped npm 12.0.1
|
|
50
|
+
* while hub and little-unraid still ran npm 11. That node sat two versions
|
|
51
|
+
* behind with no other symptom, and the applier — run by hand in the very same
|
|
52
|
+
* staging closure — reported `APPLIED werift@0.22.9` on three files. The code
|
|
53
|
+
* was never wrong; it was never called.
|
|
54
|
+
*
|
|
55
|
+
* `CLOSURE_ALLOW_SCRIPTS` in `root-update-service.ts` un-blocks the lifecycle
|
|
56
|
+
* again, and that is the right fix for `better-sqlite3` and `node-av`, which
|
|
57
|
+
* genuinely need npm to fetch or build something. It is NOT the right fix for a
|
|
58
|
+
* patch of ours: making our own patch depend on an npm policy is the dependency
|
|
59
|
+
* D478 was written to remove. So this runs unconditionally and in addition.
|
|
60
|
+
*
|
|
61
|
+
* Safe to run twice: the applier is idempotent by construction — every hunk
|
|
62
|
+
* recognises its own output, so an already-patched file is skipped rather than
|
|
63
|
+
* patched again.
|
|
64
|
+
*
|
|
65
|
+
* ## Which Node
|
|
66
|
+
*
|
|
67
|
+
* `process.execPath` — the one already running. A packaged desktop ships a bare
|
|
68
|
+
* bundled Node and has no `node` on PATH at all (the Mac agent's process runs
|
|
69
|
+
* with `PATH=/usr/bin:/bin:/usr/sbin:/sbin`), so resolving `node` by name would
|
|
70
|
+
* ENOENT on exactly the host this was written for.
|
|
71
|
+
*/
|
|
72
|
+
const node_child_process_1 = require("node:child_process");
|
|
73
|
+
const fs = __importStar(require("node:fs"));
|
|
74
|
+
const path = __importStar(require("node:path"));
|
|
75
|
+
const node_util_1 = require("node:util");
|
|
76
|
+
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
77
|
+
/** Where the published root package keeps its applier, inside a closure. */
|
|
78
|
+
const APPLIER_REL = ['node_modules', '@camstack', 'server', 'scripts', 'closure-postinstall.mjs'];
|
|
79
|
+
/** A patch pass must never be the slow part of an update. */
|
|
80
|
+
const APPLY_TIMEOUT_MS = 60_000;
|
|
81
|
+
function buildSystemApplyClosurePatches(logger) {
|
|
82
|
+
return async (closureDir) => {
|
|
83
|
+
const applier = path.join(closureDir, ...APPLIER_REL);
|
|
84
|
+
if (!fs.existsSync(applier)) {
|
|
85
|
+
// A closure from before D478, or a root package that ships no patches.
|
|
86
|
+
// Nothing to apply is not a failure — and the patch VALIDATOR, not this,
|
|
87
|
+
// is what decides whether a missing patch matters.
|
|
88
|
+
logger.debug('closure ships no patch applier — nothing to apply', {
|
|
89
|
+
meta: { applier },
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const { stdout, stderr } = await execFileAsync(process.execPath, [applier], {
|
|
94
|
+
cwd: path.dirname(path.dirname(applier)),
|
|
95
|
+
timeout: APPLY_TIMEOUT_MS,
|
|
96
|
+
});
|
|
97
|
+
// The applier names every file it touched and every one it refused. Those
|
|
98
|
+
// lines are the only record that the patch pass happened at all, so they
|
|
99
|
+
// are forwarded rather than swallowed.
|
|
100
|
+
const output = `${stdout}${stderr}`.trim();
|
|
101
|
+
if (output.length > 0) {
|
|
102
|
+
logger.info('closure patches applied', { meta: { output } });
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -1071,6 +1071,15 @@ var require_dist = __commonJS({
|
|
|
1071
1071
|
__name(buildNpmRegistryArgs, "buildNpmRegistryArgs");
|
|
1072
1072
|
var NPM_VIEW_TIMEOUT_MS = 2e4;
|
|
1073
1073
|
var NPM_INSTALL_TIMEOUT_MS = 15 * 6e4;
|
|
1074
|
+
var CLOSURE_ALLOW_SCRIPTS = {
|
|
1075
|
+
"@camstack/server": true,
|
|
1076
|
+
"better-sqlite3": true,
|
|
1077
|
+
"node-av": true,
|
|
1078
|
+
"node-pty": true,
|
|
1079
|
+
ssh2: true,
|
|
1080
|
+
"cpu-features": true,
|
|
1081
|
+
esbuild: true
|
|
1082
|
+
};
|
|
1074
1083
|
var RESTART_REASON_PREFIX = "server-update";
|
|
1075
1084
|
var STALE_TRANSIENT_MS = 24 * 60 * 60 * 1e3;
|
|
1076
1085
|
function readWrapperProbes() {
|
|
@@ -1118,6 +1127,7 @@ var require_dist = __commonJS({
|
|
|
1118
1127
|
dataDir;
|
|
1119
1128
|
execNpm;
|
|
1120
1129
|
ensureNativePrebuildsFn;
|
|
1130
|
+
applyClosurePatchesFn;
|
|
1121
1131
|
env;
|
|
1122
1132
|
readWrapperProbesFn;
|
|
1123
1133
|
now;
|
|
@@ -1146,6 +1156,7 @@ var require_dist = __commonJS({
|
|
|
1146
1156
|
};
|
|
1147
1157
|
});
|
|
1148
1158
|
this.ensureNativePrebuildsFn = options.ensureNativePrebuilds ?? (async () => void 0);
|
|
1159
|
+
this.applyClosurePatchesFn = options.applyClosurePatches ?? (async () => void 0);
|
|
1149
1160
|
this.env = options.env ?? process.env;
|
|
1150
1161
|
this.readWrapperProbesFn = options.readWrapperProbes ?? readWrapperProbes;
|
|
1151
1162
|
this.now = options.now ?? Date.now;
|
|
@@ -1461,7 +1472,8 @@ var require_dist = __commonJS({
|
|
|
1461
1472
|
} else {
|
|
1462
1473
|
fs6.writeFileSync(path6.join(stagingDir, "package.json"), JSON.stringify({
|
|
1463
1474
|
name: "camstack-node-root",
|
|
1464
|
-
private: true
|
|
1475
|
+
private: true,
|
|
1476
|
+
allowScripts: CLOSURE_ALLOW_SCRIPTS
|
|
1465
1477
|
}, null, 2), "utf-8");
|
|
1466
1478
|
await this.execNpm([
|
|
1467
1479
|
...installArgs,
|
|
@@ -1494,6 +1506,15 @@ var require_dist = __commonJS({
|
|
|
1494
1506
|
if (missingNatives.length > 0) {
|
|
1495
1507
|
throw new Error(`staged closure missing native prebuilds: ${missingNatives.join(", ")} \u2014 refusing to arm a swap that would break the forked runners`);
|
|
1496
1508
|
}
|
|
1509
|
+
try {
|
|
1510
|
+
await this.applyClosurePatchesFn(stagingDir);
|
|
1511
|
+
} catch (err) {
|
|
1512
|
+
this.logger.warn("closure-patch apply step failed \u2014 validator will decide", {
|
|
1513
|
+
meta: {
|
|
1514
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1515
|
+
}
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1497
1518
|
const unpatched = findUnappliedClosurePatches(stagingDir);
|
|
1498
1519
|
if (unpatched.length > 0) {
|
|
1499
1520
|
throw new Error(`staged closure did not apply its shipped patches: ${unpatched.join(", ")} \u2014 refusing to arm a swap into an unpatched closure`);
|
|
@@ -1541,6 +1562,7 @@ var require_dist = __commonJS({
|
|
|
1541
1562
|
return {
|
|
1542
1563
|
name: "camstack-node-root",
|
|
1543
1564
|
private: true,
|
|
1565
|
+
allowScripts: CLOSURE_ALLOW_SCRIPTS,
|
|
1544
1566
|
dependencies: {
|
|
1545
1567
|
[this.spec.packageName]: fileRef(rootTgz)
|
|
1546
1568
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.278",
|
|
4
4
|
"private": false,
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -35,18 +35,18 @@
|
|
|
35
35
|
]
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@camstack/addon-admin-ui": "
|
|
39
|
-
"@camstack/addon-agent-ui": "
|
|
40
|
-
"@camstack/addon-auth": "
|
|
41
|
-
"@camstack/addon-decoder-nodeav": "
|
|
42
|
-
"@camstack/addon-notifiers": "
|
|
43
|
-
"@camstack/addon-pipeline": "
|
|
44
|
-
"@camstack/addon-pipeline-orchestrator": "
|
|
45
|
-
"@camstack/addon-post-analysis": "
|
|
46
|
-
"@camstack/sdk": "
|
|
47
|
-
"@camstack/system": "
|
|
48
|
-
"@camstack/types": "
|
|
49
|
-
"@camstack/ui-library": "
|
|
38
|
+
"@camstack/addon-admin-ui": "1.2.187",
|
|
39
|
+
"@camstack/addon-agent-ui": "1.2.105",
|
|
40
|
+
"@camstack/addon-auth": "1.2.107",
|
|
41
|
+
"@camstack/addon-decoder-nodeav": "1.2.100",
|
|
42
|
+
"@camstack/addon-notifiers": "1.2.104",
|
|
43
|
+
"@camstack/addon-pipeline": "1.2.227",
|
|
44
|
+
"@camstack/addon-pipeline-orchestrator": "1.2.190",
|
|
45
|
+
"@camstack/addon-post-analysis": "1.2.228",
|
|
46
|
+
"@camstack/sdk": "1.2.103",
|
|
47
|
+
"@camstack/system": "1.2.227",
|
|
48
|
+
"@camstack/types": "1.2.184",
|
|
49
|
+
"@camstack/ui-library": "1.2.151",
|
|
50
50
|
"@fastify/compress": "^9.0.0",
|
|
51
51
|
"@fastify/cookie": "^11.0.2",
|
|
52
52
|
"@fastify/cors": "^11.2.0",
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* arrives from npm unpatched. Production ran it unpatched from the day the
|
|
14
14
|
* patch was written until 2026-09-12.
|
|
15
15
|
*
|
|
16
|
-
* The closure is an `npm install` into a staging dir
|
|
17
|
-
*
|
|
16
|
+
* The closure is an `npm install` into a staging dir, so this package's own
|
|
17
|
+
* `postinstall` is the natural place to do it — it is the one place that
|
|
18
18
|
* (a) runs inside the closure, (b) runs with the NEW release's patch files,
|
|
19
19
|
* and (c) needs no new dependency, no diff library and no change to the update
|
|
20
20
|
* service. Every node gets it identically — hub, Docker agent, Electron agent
|
|
@@ -29,6 +29,15 @@
|
|
|
29
29
|
* must never leave a package half-patched — every hunk is applied to an
|
|
30
30
|
* in-memory copy and the file is written once, or not at all.
|
|
31
31
|
*
|
|
32
|
+
* ## It is no longer the ONLY place, and must not be (D484)
|
|
33
|
+
*
|
|
34
|
+
* "the install passes no `--ignore-scripts`" was true and was the wrong
|
|
35
|
+
* question. npm 12 blocks a dependency's install scripts WITHOUT the flag, so
|
|
36
|
+
* on the Mac-mini agent this script was simply never called and the closure
|
|
37
|
+
* came out unpatched with exit 0. `stageClosure` therefore also invokes this
|
|
38
|
+
* applier directly, and the idempotence below is what makes running twice
|
|
39
|
+
* harmless rather than a double patch.
|
|
40
|
+
*
|
|
32
41
|
* The FAIL-CLOSED half lives elsewhere, in `@camstack/node-root`'s
|
|
33
42
|
* `findUnappliedClosurePatches`, which refuses to swap into a closure whose
|
|
34
43
|
* markers are absent. Silence here is therefore never silence overall: either
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.purgeRequireCacheUnder = purgeRequireCacheUnder;
|
|
7
|
-
exports.bundleGraphRootFor = bundleGraphRootFor;
|
|
8
|
-
/**
|
|
9
|
-
* require-cache — evicting an addon's own modules so a hot-updated bundle
|
|
10
|
-
* is actually re-read.
|
|
11
|
-
*
|
|
12
|
-
* Lives in its own file because the behaviour it encodes is a Node semantic
|
|
13
|
-
* that cannot be observed from inside the test runner: the runner transforms
|
|
14
|
-
* modules itself, so only a real `node` process re-executing a real CommonJS
|
|
15
|
-
* file proves anything. `require-cache.spec.ts` spawns one.
|
|
16
|
-
*/
|
|
17
|
-
const node_fs_1 = __importDefault(require("node:fs"));
|
|
18
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
19
|
-
/**
|
|
20
|
-
* Drop every `require`-cached module that lives under `dir`.
|
|
21
|
-
*
|
|
22
|
-
* Node's ESM loader serves a CommonJS module from the require cache, keyed by
|
|
23
|
-
* the resolved filename — the `?t=` query an ESM import uses to force a re-read
|
|
24
|
-
* is stripped before that lookup and therefore does nothing. Since every addon
|
|
25
|
-
* bundle here is CJS, evicting the addon's own entries is what makes a
|
|
26
|
-
* hot-updated bundle actually load.
|
|
27
|
-
*
|
|
28
|
-
* Matched against the REAL path as well as the given one. Node keys the cache
|
|
29
|
-
* by the resolved-and-symlink-followed filename, while an addon directory is
|
|
30
|
-
* assembled from the configured `dataDir` and never realpath'd — so a single
|
|
31
|
-
* symlink anywhere above it (a `dataDir` under macOS's `/tmp`, a relocated data
|
|
32
|
-
* root) makes a prefix match on the given path miss every entry and this
|
|
33
|
-
* function silently do nothing. Which is precisely the failure it exists to
|
|
34
|
-
* fix, wearing its clothes: the catalog would look refreshed and be stale.
|
|
35
|
-
*
|
|
36
|
-
* Scoped to one directory on purpose: a blanket cache clear would evict the
|
|
37
|
-
* hub's own modules and every other addon's, turning a catalog refresh into a
|
|
38
|
-
* process-wide reload. Best-effort — a cache that cannot be walked leaves the
|
|
39
|
-
* previous (stale) behaviour rather than failing the caller.
|
|
40
|
-
*
|
|
41
|
-
* **The directory must be the whole BUNDLE, never the entry's own folder** —
|
|
42
|
-
* see {@link bundleGraphRootFor}. A narrower scope purges half a module graph,
|
|
43
|
-
* which is worse than purging none of it.
|
|
44
|
-
*/
|
|
45
|
-
function purgeRequireCacheUnder(dir) {
|
|
46
|
-
try {
|
|
47
|
-
const cache = require.cache;
|
|
48
|
-
if (cache === undefined)
|
|
49
|
-
return;
|
|
50
|
-
const prefixes = candidatePrefixes(dir);
|
|
51
|
-
for (const key of Object.keys(cache)) {
|
|
52
|
-
if (prefixes.some((prefix) => key.startsWith(prefix)))
|
|
53
|
-
delete cache[key];
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
// Non-CJS host, or a frozen cache. The caller's import still runs.
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* The directory that holds one addon bundle's WHOLE module graph.
|
|
62
|
-
*
|
|
63
|
-
* A multi-entry bundle does not live under its entry's folder. Every addon in
|
|
64
|
-
* this repo builds through `tools/build/vite-lib.preset.ts`, whose entries are
|
|
65
|
-
* `dist/<addon>/index.js` — and whose SHARED CHUNKS (`dist/dist-<hash>.js`,
|
|
66
|
-
* `dist/node-<hash>.js`, …) sit one level up, in `dist/`. Purging
|
|
67
|
-
* `dirname(entryPath)` therefore evicts the entry and leaves the chunks, so the
|
|
68
|
-
* re-import RE-EXECUTES the entry against module state that was never reset.
|
|
69
|
-
*
|
|
70
|
-
* That is not a stale-cache bug, it is a corrupted one, and it took the hub's
|
|
71
|
-
* `stream-broker` down on 2026-08-29: the entry chunk carries
|
|
72
|
-
* `log-channels.ts`'s module-scope `declareLogChannel` calls while the
|
|
73
|
-
* `LogChannelRegistry` singleton they write to is inlined into the shared
|
|
74
|
-
* chunk. Half the graph re-ran, the registry did not, and the second
|
|
75
|
-
* declaration threw `log channel "stream-broker.webrtc" is already declared in
|
|
76
|
-
* this process` — a message that is correct, and that named the loader rather
|
|
77
|
-
* than the addon. `dist/` is the unit the bundler emits, so `dist/` is the unit
|
|
78
|
-
* the cache is purged in.
|
|
79
|
-
*
|
|
80
|
-
* `node_modules/` under the addon is deliberately OUTSIDE this root: a package
|
|
81
|
-
* that declares `camstack.nativeDependencies` installs `.node` bindings there,
|
|
82
|
-
* and re-`dlopen`ing a native addon is not a refresh, it is a coin flip.
|
|
83
|
-
*
|
|
84
|
-
* Falls back to the entry's directory when there is no `dist/` above it (a
|
|
85
|
-
* loose `loadFromPath` module in a test or a dev tree) — the historical
|
|
86
|
-
* behaviour, for the shape that has no bundle.
|
|
87
|
-
*/
|
|
88
|
-
function bundleGraphRootFor(addonDir, entryPath) {
|
|
89
|
-
const distDir = node_path_1.default.resolve(addonDir, 'dist');
|
|
90
|
-
const withSep = distDir.endsWith(node_path_1.default.sep) ? distDir : `${distDir}${node_path_1.default.sep}`;
|
|
91
|
-
return entryPath.startsWith(withSep) ? distDir : node_path_1.default.dirname(entryPath);
|
|
92
|
-
}
|
|
93
|
-
/** The directory as given and as Node sees it, both as trailing-separator prefixes. */
|
|
94
|
-
function candidatePrefixes(dir) {
|
|
95
|
-
const withSep = (d) => (d.endsWith(node_path_1.default.sep) ? d : `${d}${node_path_1.default.sep}`);
|
|
96
|
-
const given = withSep(dir);
|
|
97
|
-
let real = given;
|
|
98
|
-
try {
|
|
99
|
-
real = withSep(node_fs_1.default.realpathSync(dir));
|
|
100
|
-
}
|
|
101
|
-
catch {
|
|
102
|
-
// Directory gone between the caller's existsSync and here — `given` alone
|
|
103
|
-
// is still a correct (if narrower) match.
|
|
104
|
-
}
|
|
105
|
-
return real === given ? [given] : [given, real];
|
|
106
|
-
}
|