@camstack/system 1.2.179 → 1.2.180
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/addon-runner.js +1 -1
- package/dist/addon-runner.mjs +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.d.ts +56 -10
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +657 -62
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +658 -63
- package/dist/builtins/backup-orchestrator/run-queue.d.ts +103 -0
- package/dist/builtins/backup-orchestrator/staging-paths.d.ts +36 -0
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
- package/dist/builtins/device-manager/device-manager.addon.js +146 -5
- package/dist/builtins/device-manager/device-manager.addon.mjs +146 -5
- package/dist/builtins/device-manager/device-meta-actions.d.ts +1 -1
- package/dist/builtins/device-manager/migrate-device.d.ts +33 -0
- package/dist/builtins/device-manager/migrate-hardware-state.d.ts +24 -0
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +1 -1
- package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
- package/dist/builtins/local-network/local-network.addon.js +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-backup/system-backup.service.d.ts +55 -2
- package/dist/builtins/system-config/system-config.addon.js +1 -1
- package/dist/builtins/system-config/system-config.addon.mjs +1 -1
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{dist-L_39vRmH.mjs → dist-DqNTelT4.mjs} +167 -1
- package/dist/{dist-C7qWwrK1.js → dist-TEwQZL3g.js} +167 -1
- package/dist/index.js +530 -2
- package/dist/index.mjs +533 -5
- package/dist/kernel/addon-installer.d.ts +69 -0
- package/dist/kernel/deps/closure-dedupe.d.ts +105 -0
- package/dist/{manifest-system-deps-PAIfmNun.js → manifest-system-deps-BVIr3MYJ.js} +1 -1
- package/dist/{manifest-system-deps-CMOGc5nC.mjs → manifest-system-deps-Ci-jhrxY.mjs} +1 -1
- package/dist/{retired-settings-keys-CnbKAxmX.mjs → retired-settings-keys-CeSITpIX.mjs} +1 -1
- package/dist/{retired-settings-keys-3soU9pfP.js → retired-settings-keys-DauPtbFR.js} +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as __toCommonJS, i as __require, n as __esmMin, o as __toESM$1, r as __exportAll, t as __commonJSMin$1 } from "./chunk-CNf5ZN-e.mjs";
|
|
2
|
-
import { $ as isArrayOutputSchema, Ct as scopesAllowDeviceCap, D as addonSettingsCapability, Gt as asJsonObject$1, Ht as ReadinessRegistry, It as DATAPLANE_SECRET_HEADER$1, Jt as createEvent, Kt as asNumber, Lt as DEVICE_SETTINGS_CONTRIBUTION_METHODS, Pt as errMsg$1, Rt as DEVICE_STATUS_METHOD, St as scopesAllowAddon, Ut as ReadinessTimeoutError, X as extractNestedAddonId, Yt as emitDownForOwnedCaps, Zt as expandCapMethods, an as scopeKey, at as kebabToCamel, bt as resolveMethodAuth, et as isCollectionArrayMethod, g as RUNTIME_DEFAULTS, gt as procedureAuthKey, in as resolveCapMount, it as isVoidInput, lt as logLevelAtMost, m as METHOD_ACCESS_MAP, nn as parseJsonUnknown, ot as lifecycleJobSchema, p as LOG_LEVEL_RANK, pt as objectInputDeclaresAddonId, qt as asString$1, rn as readinessKey, sn as EventCategory$1, t as ALL_CAPABILITY_DEFINITIONS, tn as parseJsonObject, tt as isObjectInput, ut as looseSchema } from "./dist-
|
|
2
|
+
import { $ as isArrayOutputSchema, Ct as scopesAllowDeviceCap, D as addonSettingsCapability, Gt as asJsonObject$1, Ht as ReadinessRegistry, It as DATAPLANE_SECRET_HEADER$1, Jt as createEvent, Kt as asNumber, Lt as DEVICE_SETTINGS_CONTRIBUTION_METHODS, Pt as errMsg$1, Rt as DEVICE_STATUS_METHOD, St as scopesAllowAddon, Ut as ReadinessTimeoutError, X as extractNestedAddonId, Yt as emitDownForOwnedCaps, Zt as expandCapMethods, an as scopeKey, at as kebabToCamel, bt as resolveMethodAuth, et as isCollectionArrayMethod, g as RUNTIME_DEFAULTS, gt as procedureAuthKey, in as resolveCapMount, it as isVoidInput, lt as logLevelAtMost, m as METHOD_ACCESS_MAP, nn as parseJsonUnknown, ot as lifecycleJobSchema, p as LOG_LEVEL_RANK, pt as objectInputDeclaresAddonId, qt as asString$1, rn as readinessKey, sn as EventCategory$1, t as ALL_CAPABILITY_DEFINITIONS, tn as parseJsonObject, tt as isObjectInput, ut as looseSchema } from "./dist-DqNTelT4.mjs";
|
|
3
3
|
import { AlertCenterAddon } from "./builtins/alerts/alerts.addon.mjs";
|
|
4
4
|
import "./builtins/alerts/index.mjs";
|
|
5
5
|
import { t as formatLogLine } from "./formatter-B7qW8bPJ.mjs";
|
|
@@ -26,7 +26,7 @@ import "./builtins/system-config/index.mjs";
|
|
|
26
26
|
import { WinstonDestination, WinstonLoggingAddon } from "./builtins/winston-logging/index.mjs";
|
|
27
27
|
import { a as parseTokenizedUrl, c as collectModelFiles, d as downloadModel, f as ensureModel, h as isModelDownloaded, i as parseRangeHeader, l as deleteModelFromDisk, m as getModelFilePath, n as contentTypeFor, o as resolveFilePath, p as fetchJson, r as createAuthenticatedFileServer, s as ModelDownloadService, t as createFileDataPlaneHandler, u as downloadFile } from "./file-data-plane-BhKdxJgf.mjs";
|
|
28
28
|
import { C as registerLanHttpHandler, S as readLanHttpState, _ as DEFAULT_LAN_HTTP_PORT, a as writeTlsMode, b as bindPendingLanHttp, c as loadTlsCert, d as collectCertIdentity, f as CA_VALIDITY_DAYS, g as evaluateExistingCert, h as SERVER_AUTH_OID, i as writeExtraSans, l as reissueTlsLeaf, m as MAX_LEAF_VALIDITY_DAYS, n as readTlsAccessStatus, o as validateUploadedTls, p as LEAF_RENEWAL_WINDOW_DAYS, r as readTlsMode, s as ensureTlsCert, t as readExtraSans, u as CA_COMMON_NAME, v as allFamiliesListenHost, x as closeLanHttp, y as applyLanHttp } from "./tls-DOTmtLCW.mjs";
|
|
29
|
-
import { $ as buildUdsNativeCapProxy, $t as formatHeapSpaces, A as createUdsLoggerWithControl, At as HUB_MAIN_RSS_BUDGET_MB, B as readMoleculerFanoutMode, Bt as createV8Reclaimer, C as ipcParentLink, Ct as createAddonDataPlaneFacility, D as createUdsEventBus, Dt as HEAP_WATCH_INTERVAL_MS, E as createUdsEventBridge, Et as HEAP_RECLAIM_TRIGGER_MB, F as AGENT_CAP_FWD_SERVICE, Ft as RUNNER_RSS_BUDGET_ENV, G as createLocalTransport, Gt as reclaimIntervalMs, H as PARENT_ROUTED_PROVIDER_ADDON_ID, Ht as hubMainRssBudget, I as CapRouteResolver, It as ZERO_LOOP_DELAY, J as SocketChannel, Jt as startRunnerHeapWatch, K as UdsLocalTransportClient, Kt as shouldReclaim, L as CapRouteError, Lt as buildHeapSample, M as LocalChildRegistry, Mt as RSS_BUDGET_REANNOUNCE_MIN_MS, N as UDS_NO_ROUTE_PREFIX, Nt as RSS_BUDGET_RELEASE_RATIO, O as udsChildLogToWorkerEntry, Ot as HEAP_WATCH_WARN_RATIO, P as AGENT_CAP_FWD_ACTION, Pt as RUNNER_HEAP_WATCH_INTERVAL_MS, Q as buildNativeCapProxy, Qt as emitHeapDiagnosticReport, R as classifyCapRoute, Rt as createDeferredHeapWatchSink, S as buildLinkChain, St as runNpm, T as createParentUnownedCallHandler, Tt as HEAP_RECLAIM_STEADY_STATE_INTERVAL_MS, U as __resetCapUsageRegistryForTests, Ut as nextRssBudgetState, V as CapUsageRegistry, Vt as describeRss, W as getCapUsageRegistry, Wt as parseRssBudgetMb, X as FrameDecoder, Xt as DECISIVE_HEAP_SPACES, Y as localEndpointPath, Yt as strandedMb, Z as encodeFrame, Zt as HEAP_SPACE_REPORT_MIN_MB, _ as AddonDepsManager, _n as createEventPlaneReader, _t as CapabilityUnavailableError, a as createUdsAddonContext, an as createSocketPlaneMeter, at as createAddonService, b as brokerCallForCap, bt as resolveAddonClass, c as getOrInitReadinessRegistryForClient, cn as formatSocketPlane, ct as capActionName, d as clusterEventTopic, dn as recordSocketFrame, dt as capServiceName, en as heapSpaceField, et as createBrokerDeviceManagerApi, f as getBrokerEventBus, fn as sampleSocketDirection, ft as parseCapAction, g as subscribePassthrough, gn as createEventPlaneMeter, gt as CapabilityHandle, h as setNodeEventInterest, hn as EVENT_PLANE_TOP_N, ht as DeviceRegistry, i as createAddonContext, in as SOCKET_PLANE_TOP_N, j as LocalChildClient, jt as HUB_RSS_BUDGET_ENV, k as createUdsLogger, kt as HUB_MAIN_HEAP_WATCH_LABEL, l as setHubConnected, ln as EMPTY_SOCKET_DIRECTION, lt as capActionSuffix, m as registerEventBusService, mn as socketDirectionMessages, mt as serializeTypedArrays, n as installManifestPythonDeps, nn as selectReportedSpaces, o as getWorkerDeviceRegistry, on as createSocketPlaneReader, ot as validateProviderRegistrations, p as getMoleculerEventStats, pn as socketDirectionBytes, pt as deserializeTypedArrays, q as UdsLocalTransportServer, qt as startHeapWatch, r as adaptBrokerToCluster, rn as EMPTY_SOCKET_PLANE_BASELINE, rt as mountNativeCapService, s as getOrInitReadinessRegistry, sn as diffSocketPlane, st as NATIVE_PROVIDER_SERVICE_INFIX, t as installManifestSystemDeps, tn as readHeapSpaces, u as EVENT_TOPIC_PREFIX, un as createSocketDirectionCounters, ut as capBareAction, v as createKernelHwAccel, vn as diffEventPlane, vt as copyBundledNativeModules, w as localProviderLink, wt as HEAP_RECLAIM_MIN_INTERVAL_MS, x as brokerTransportLink, xt as resolveNpmInvocation, y as resolveHwAccel, yn as formatEventPlane, yt as installManifestNativeDeps, z as callWithServiceDiscovery, zt as createLoopDelayMeter } from "./manifest-system-deps-
|
|
29
|
+
import { $ as buildUdsNativeCapProxy, $t as formatHeapSpaces, A as createUdsLoggerWithControl, At as HUB_MAIN_RSS_BUDGET_MB, B as readMoleculerFanoutMode, Bt as createV8Reclaimer, C as ipcParentLink, Ct as createAddonDataPlaneFacility, D as createUdsEventBus, Dt as HEAP_WATCH_INTERVAL_MS, E as createUdsEventBridge, Et as HEAP_RECLAIM_TRIGGER_MB, F as AGENT_CAP_FWD_SERVICE, Ft as RUNNER_RSS_BUDGET_ENV, G as createLocalTransport, Gt as reclaimIntervalMs, H as PARENT_ROUTED_PROVIDER_ADDON_ID, Ht as hubMainRssBudget, I as CapRouteResolver, It as ZERO_LOOP_DELAY, J as SocketChannel, Jt as startRunnerHeapWatch, K as UdsLocalTransportClient, Kt as shouldReclaim, L as CapRouteError, Lt as buildHeapSample, M as LocalChildRegistry, Mt as RSS_BUDGET_REANNOUNCE_MIN_MS, N as UDS_NO_ROUTE_PREFIX, Nt as RSS_BUDGET_RELEASE_RATIO, O as udsChildLogToWorkerEntry, Ot as HEAP_WATCH_WARN_RATIO, P as AGENT_CAP_FWD_ACTION, Pt as RUNNER_HEAP_WATCH_INTERVAL_MS, Q as buildNativeCapProxy, Qt as emitHeapDiagnosticReport, R as classifyCapRoute, Rt as createDeferredHeapWatchSink, S as buildLinkChain, St as runNpm, T as createParentUnownedCallHandler, Tt as HEAP_RECLAIM_STEADY_STATE_INTERVAL_MS, U as __resetCapUsageRegistryForTests, Ut as nextRssBudgetState, V as CapUsageRegistry, Vt as describeRss, W as getCapUsageRegistry, Wt as parseRssBudgetMb, X as FrameDecoder, Xt as DECISIVE_HEAP_SPACES, Y as localEndpointPath, Yt as strandedMb, Z as encodeFrame, Zt as HEAP_SPACE_REPORT_MIN_MB, _ as AddonDepsManager, _n as createEventPlaneReader, _t as CapabilityUnavailableError, a as createUdsAddonContext, an as createSocketPlaneMeter, at as createAddonService, b as brokerCallForCap, bt as resolveAddonClass, c as getOrInitReadinessRegistryForClient, cn as formatSocketPlane, ct as capActionName, d as clusterEventTopic, dn as recordSocketFrame, dt as capServiceName, en as heapSpaceField, et as createBrokerDeviceManagerApi, f as getBrokerEventBus, fn as sampleSocketDirection, ft as parseCapAction, g as subscribePassthrough, gn as createEventPlaneMeter, gt as CapabilityHandle, h as setNodeEventInterest, hn as EVENT_PLANE_TOP_N, ht as DeviceRegistry, i as createAddonContext, in as SOCKET_PLANE_TOP_N, j as LocalChildClient, jt as HUB_RSS_BUDGET_ENV, k as createUdsLogger, kt as HUB_MAIN_HEAP_WATCH_LABEL, l as setHubConnected, ln as EMPTY_SOCKET_DIRECTION, lt as capActionSuffix, m as registerEventBusService, mn as socketDirectionMessages, mt as serializeTypedArrays, n as installManifestPythonDeps, nn as selectReportedSpaces, o as getWorkerDeviceRegistry, on as createSocketPlaneReader, ot as validateProviderRegistrations, p as getMoleculerEventStats, pn as socketDirectionBytes, pt as deserializeTypedArrays, q as UdsLocalTransportServer, qt as startHeapWatch, r as adaptBrokerToCluster, rn as EMPTY_SOCKET_PLANE_BASELINE, rt as mountNativeCapService, s as getOrInitReadinessRegistry, sn as diffSocketPlane, st as NATIVE_PROVIDER_SERVICE_INFIX, t as installManifestSystemDeps, tn as readHeapSpaces, u as EVENT_TOPIC_PREFIX, un as createSocketDirectionCounters, ut as capBareAction, v as createKernelHwAccel, vn as diffEventPlane, vt as copyBundledNativeModules, w as localProviderLink, wt as HEAP_RECLAIM_MIN_INTERVAL_MS, x as brokerTransportLink, xt as resolveNpmInvocation, y as resolveHwAccel, yn as formatEventPlane, yt as installManifestNativeDeps, z as callWithServiceDiscovery, zt as createLoopDelayMeter } from "./manifest-system-deps-Ci-jhrxY.mjs";
|
|
30
30
|
import { n as getSinglePidStats, t as getPidStats } from "./resource-monitor-BWmQ5i-o.mjs";
|
|
31
31
|
import { t as CustomActionRegistry } from "./custom-action-registry-F__gp_VX.mjs";
|
|
32
32
|
import { z } from "zod";
|
|
@@ -3671,8 +3671,362 @@ var AddonManifest = class {
|
|
|
3671
3671
|
}
|
|
3672
3672
|
};
|
|
3673
3673
|
//#endregion
|
|
3674
|
+
//#region src/kernel/deps/closure-dedupe.ts
|
|
3675
|
+
/**
|
|
3676
|
+
* Closure dedupe — stop copying into every addon the libraries the server
|
|
3677
|
+
* closure already provides.
|
|
3678
|
+
*
|
|
3679
|
+
* `installRuntimeDependencies` materialises each addon's declared
|
|
3680
|
+
* `camstack.runtimeDependencies` (and their whole transitive tree) under
|
|
3681
|
+
* `<addonsDir>/<pkg>/node_modules`. Measured on the live hub (2026-09-04):
|
|
3682
|
+
* 196 MB across 23 addons, largely re-copies of libraries sitting one
|
|
3683
|
+
* directory away in `/data/server-root/current/node_modules` — `mediabunny`
|
|
3684
|
+
* (11 MB) twice, `werift` (~5 MB) three times, plus their shared subtrees
|
|
3685
|
+
* (`@noble`, `@peculiar`, `werift-*`).
|
|
3686
|
+
*
|
|
3687
|
+
* The fix: after an addon tree's npm install completes, every top-level
|
|
3688
|
+
* `node_modules` entry the closure ALSO provides at a compatible version is
|
|
3689
|
+
* replaced by a SYMLINK into the closure's `node_modules`. Symlinks preserve
|
|
3690
|
+
* standard Node resolution for BOTH module systems at ANY depth (verified on
|
|
3691
|
+
* the hub: `require()` and `import()` through a symlink on `/data/addons`
|
|
3692
|
+
* both resolve), and a dependency the closure later drops fails LOUDLY
|
|
3693
|
+
* (dangling link → `Cannot find module`) instead of silently degrading.
|
|
3694
|
+
*
|
|
3695
|
+
* Version compatibility is the sharp edge, measured live: the closure ships
|
|
3696
|
+
* `zod@4.5.4` while `@apocaliss92/nodelink-js` (provider-reolink) requires
|
|
3697
|
+
* `zod@^3` and carries `3.25.76`. A link there would silently downgrade a
|
|
3698
|
+
* dependent to an incompatible major — worse than the disk it saves. So a
|
|
3699
|
+
* link happens ONLY when the closure version satisfies EVERY requirement
|
|
3700
|
+
* range recorded for that package in the addon's own tree, under a
|
|
3701
|
+
* deliberately conservative range parser (exact / caret / tilde; anything
|
|
3702
|
+
* else refuses). Identical versions always link.
|
|
3703
|
+
*
|
|
3704
|
+
* Everything here is dependency-free on purpose (no `semver` package in the
|
|
3705
|
+
* kernel): the parser handles only the forms it can prove, and refusal keeps
|
|
3706
|
+
* the addon's own copy — never a wrong resolution.
|
|
3707
|
+
*/
|
|
3708
|
+
function parseVersion(value) {
|
|
3709
|
+
const m = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(value.trim());
|
|
3710
|
+
if (!m) return null;
|
|
3711
|
+
return {
|
|
3712
|
+
major: Number(m[1]),
|
|
3713
|
+
minor: Number(m[2]),
|
|
3714
|
+
patch: Number(m[3]),
|
|
3715
|
+
prerelease: m[4] ?? null
|
|
3716
|
+
};
|
|
3717
|
+
}
|
|
3718
|
+
/** -1 / 0 / 1 over the numeric triple only (callers gate prereleases first). */
|
|
3719
|
+
function compareTriple(a, b) {
|
|
3720
|
+
if (a.major !== b.major) return a.major < b.major ? -1 : 1;
|
|
3721
|
+
if (a.minor !== b.minor) return a.minor < b.minor ? -1 : 1;
|
|
3722
|
+
if (a.patch !== b.patch) return a.patch < b.patch ? -1 : 1;
|
|
3723
|
+
return 0;
|
|
3724
|
+
}
|
|
3725
|
+
/**
|
|
3726
|
+
* Does `version` satisfy `range`, for the range forms this module is willing
|
|
3727
|
+
* to vouch for?
|
|
3728
|
+
*
|
|
3729
|
+
* Supported: exact (`1.2.3`, `=1.2.3`), caret (`^1.2.3`, incl. the 0.x rules)
|
|
3730
|
+
* and tilde (`~1.2.3`). EVERYTHING else — `*`, `>=`, unions, hyphen ranges,
|
|
3731
|
+
* dist-tags, bases with prereleases — returns false: an unprovable range must
|
|
3732
|
+
* keep the addon's own copy, never borrow the closure's.
|
|
3733
|
+
*
|
|
3734
|
+
* A prerelease `version` satisfies only an exact string match — semver range
|
|
3735
|
+
* semantics around prereleases are precisely the kind of subtlety a
|
|
3736
|
+
* hand-rolled parser must refuse rather than approximate.
|
|
3737
|
+
*/
|
|
3738
|
+
function closureSatisfiesRange(range, version) {
|
|
3739
|
+
const trimmedRange = range.trim();
|
|
3740
|
+
const v = parseVersion(version);
|
|
3741
|
+
if (v === null) return false;
|
|
3742
|
+
if (v.prerelease !== null) return trimmedRange === version.trim() || trimmedRange === `=${version.trim()}`;
|
|
3743
|
+
const exactBase = parseVersion(trimmedRange.startsWith("=") ? trimmedRange.slice(1) : trimmedRange);
|
|
3744
|
+
if (exactBase !== null) return exactBase.prerelease === null && compareTriple(v, exactBase) === 0;
|
|
3745
|
+
const operator = trimmedRange[0];
|
|
3746
|
+
if (operator !== "^" && operator !== "~") return false;
|
|
3747
|
+
const base = parseVersion(trimmedRange.slice(1));
|
|
3748
|
+
if (base === null || base.prerelease !== null) return false;
|
|
3749
|
+
if (operator === "~") return v.major === base.major && v.minor === base.minor && compareTriple(v, base) >= 0;
|
|
3750
|
+
if (base.major > 0) return v.major === base.major && compareTriple(v, base) >= 0;
|
|
3751
|
+
if (base.minor > 0) return v.major === 0 && v.minor === base.minor && compareTriple(v, base) >= 0;
|
|
3752
|
+
return compareTriple(v, base) === 0;
|
|
3753
|
+
}
|
|
3754
|
+
/**
|
|
3755
|
+
* Belt over the range check: the closure copy must live in the same major
|
|
3756
|
+
* line as the copy npm actually materialised (same minor while major is 0).
|
|
3757
|
+
* The range check should already guarantee this; if the two ever disagree
|
|
3758
|
+
* the safe answer is "do not link".
|
|
3759
|
+
*/
|
|
3760
|
+
function sameCompatLine(a, b) {
|
|
3761
|
+
const pa = parseVersion(a);
|
|
3762
|
+
const pb = parseVersion(b);
|
|
3763
|
+
if (pa === null || pb === null) return false;
|
|
3764
|
+
if (pa.major !== pb.major) return false;
|
|
3765
|
+
if (pa.major === 0) return pa.minor === pb.minor;
|
|
3766
|
+
return true;
|
|
3767
|
+
}
|
|
3768
|
+
/**
|
|
3769
|
+
* Decide, per top-level entry, whether the closure's copy can serve in place
|
|
3770
|
+
* of the addon's own. Identical versions always link; otherwise every
|
|
3771
|
+
* recorded requirement range must accept the closure version, and the two
|
|
3772
|
+
* copies must share a compat line. Anything unprovable keeps the local copy.
|
|
3773
|
+
*/
|
|
3774
|
+
function planClosureDedupe(input) {
|
|
3775
|
+
const links = [];
|
|
3776
|
+
const keeps = [];
|
|
3777
|
+
for (const candidate of input.candidates) {
|
|
3778
|
+
if (candidate.name.startsWith("@camstack/")) {
|
|
3779
|
+
keeps.push({
|
|
3780
|
+
name: candidate.name,
|
|
3781
|
+
reason: "camstack-package"
|
|
3782
|
+
});
|
|
3783
|
+
continue;
|
|
3784
|
+
}
|
|
3785
|
+
if (input.neverLink.has(candidate.name)) {
|
|
3786
|
+
keeps.push({
|
|
3787
|
+
name: candidate.name,
|
|
3788
|
+
reason: "never-link"
|
|
3789
|
+
});
|
|
3790
|
+
continue;
|
|
3791
|
+
}
|
|
3792
|
+
const closureVersion = input.closureVersions.get(candidate.name);
|
|
3793
|
+
if (closureVersion === void 0) {
|
|
3794
|
+
keeps.push({
|
|
3795
|
+
name: candidate.name,
|
|
3796
|
+
reason: "not-in-closure"
|
|
3797
|
+
});
|
|
3798
|
+
continue;
|
|
3799
|
+
}
|
|
3800
|
+
if (closureVersion === candidate.version) {
|
|
3801
|
+
links.push({
|
|
3802
|
+
name: candidate.name,
|
|
3803
|
+
closureVersion
|
|
3804
|
+
});
|
|
3805
|
+
continue;
|
|
3806
|
+
}
|
|
3807
|
+
const ranges = input.requirements.get(candidate.name) ?? [];
|
|
3808
|
+
if (ranges.length > 0 && ranges.every((range) => closureSatisfiesRange(range, closureVersion)) && sameCompatLine(closureVersion, candidate.version)) links.push({
|
|
3809
|
+
name: candidate.name,
|
|
3810
|
+
closureVersion
|
|
3811
|
+
});
|
|
3812
|
+
else keeps.push({
|
|
3813
|
+
name: candidate.name,
|
|
3814
|
+
reason: "version-incompatible"
|
|
3815
|
+
});
|
|
3816
|
+
}
|
|
3817
|
+
return {
|
|
3818
|
+
links,
|
|
3819
|
+
keeps
|
|
3820
|
+
};
|
|
3821
|
+
}
|
|
3822
|
+
function readTreeManifest(pkgJsonPath) {
|
|
3823
|
+
let raw;
|
|
3824
|
+
try {
|
|
3825
|
+
raw = asJsonObject$1(parseJsonUnknown(fs$17.readFileSync(pkgJsonPath, "utf-8")));
|
|
3826
|
+
} catch {
|
|
3827
|
+
return null;
|
|
3828
|
+
}
|
|
3829
|
+
if (raw === null) return null;
|
|
3830
|
+
const ranges = /* @__PURE__ */ new Map();
|
|
3831
|
+
for (const key of [
|
|
3832
|
+
"dependencies",
|
|
3833
|
+
"optionalDependencies",
|
|
3834
|
+
"peerDependencies"
|
|
3835
|
+
]) {
|
|
3836
|
+
const deps = asJsonObject$1(raw[key]);
|
|
3837
|
+
if (deps === null) continue;
|
|
3838
|
+
for (const [name, range] of Object.entries(deps)) if (typeof range === "string" && !ranges.has(name)) ranges.set(name, range);
|
|
3839
|
+
}
|
|
3840
|
+
const version = asString$1(raw["version"]);
|
|
3841
|
+
return {
|
|
3842
|
+
version: version.length > 0 ? version : null,
|
|
3843
|
+
ranges
|
|
3844
|
+
};
|
|
3845
|
+
}
|
|
3846
|
+
/** Top-level entries of a node_modules dir (scoped names flattened). */
|
|
3847
|
+
function listTopLevelEntries(nodeModulesDir) {
|
|
3848
|
+
let entries;
|
|
3849
|
+
try {
|
|
3850
|
+
entries = fs$17.readdirSync(nodeModulesDir, { withFileTypes: true });
|
|
3851
|
+
} catch {
|
|
3852
|
+
return [];
|
|
3853
|
+
}
|
|
3854
|
+
const names = [];
|
|
3855
|
+
for (const entry of entries) {
|
|
3856
|
+
if (entry.name.startsWith(".")) continue;
|
|
3857
|
+
if (entry.name.startsWith("@")) {
|
|
3858
|
+
let inner;
|
|
3859
|
+
try {
|
|
3860
|
+
inner = fs$17.readdirSync(path$39.join(nodeModulesDir, entry.name), { withFileTypes: true });
|
|
3861
|
+
} catch {
|
|
3862
|
+
continue;
|
|
3863
|
+
}
|
|
3864
|
+
for (const sub of inner) if (!sub.name.startsWith(".")) names.push(`${entry.name}/${sub.name}`);
|
|
3865
|
+
} else names.push(entry.name);
|
|
3866
|
+
}
|
|
3867
|
+
return names;
|
|
3868
|
+
}
|
|
3869
|
+
/**
|
|
3870
|
+
* Read an addon tree's dedupe-relevant state: which top-level packages exist
|
|
3871
|
+
* (and their versions), and which requirement ranges apply to each. A
|
|
3872
|
+
* dependent's range for `X` is recorded only when that dependent resolves to
|
|
3873
|
+
* the TOP-LEVEL `X` — a nested override copy (`dep/node_modules/X`) means the
|
|
3874
|
+
* flat copy is not what that dependent loads, so its range must not block the
|
|
3875
|
+
* flat copy's dedupe.
|
|
3876
|
+
*/
|
|
3877
|
+
function collectAddonTreeState(addonDir) {
|
|
3878
|
+
const nodeModulesDir = path$39.join(addonDir, "node_modules");
|
|
3879
|
+
const requirements = /* @__PURE__ */ new Map();
|
|
3880
|
+
const addRange = (name, range) => {
|
|
3881
|
+
const existing = requirements.get(name);
|
|
3882
|
+
if (existing === void 0) requirements.set(name, [range]);
|
|
3883
|
+
else existing.push(range);
|
|
3884
|
+
};
|
|
3885
|
+
const rootManifest = readTreeManifest(path$39.join(addonDir, "package.json"));
|
|
3886
|
+
if (rootManifest !== null) for (const [name, range] of rootManifest.ranges) addRange(name, range);
|
|
3887
|
+
const candidates = [];
|
|
3888
|
+
const existingLinks = [];
|
|
3889
|
+
for (const name of listTopLevelEntries(nodeModulesDir)) {
|
|
3890
|
+
const entryDir = path$39.join(nodeModulesDir, name);
|
|
3891
|
+
let isLink = false;
|
|
3892
|
+
try {
|
|
3893
|
+
isLink = fs$17.lstatSync(entryDir).isSymbolicLink();
|
|
3894
|
+
} catch {
|
|
3895
|
+
continue;
|
|
3896
|
+
}
|
|
3897
|
+
if (isLink) {
|
|
3898
|
+
let target = "";
|
|
3899
|
+
try {
|
|
3900
|
+
target = fs$17.readlinkSync(entryDir);
|
|
3901
|
+
} catch {}
|
|
3902
|
+
existingLinks.push({
|
|
3903
|
+
name,
|
|
3904
|
+
target
|
|
3905
|
+
});
|
|
3906
|
+
}
|
|
3907
|
+
const manifest = readTreeManifest(path$39.join(entryDir, "package.json"));
|
|
3908
|
+
if (manifest === null) continue;
|
|
3909
|
+
if (!isLink && manifest.version !== null) candidates.push({
|
|
3910
|
+
name,
|
|
3911
|
+
version: manifest.version
|
|
3912
|
+
});
|
|
3913
|
+
for (const [depName, range] of manifest.ranges) {
|
|
3914
|
+
if (fs$17.existsSync(path$39.join(entryDir, "node_modules", depName))) continue;
|
|
3915
|
+
addRange(depName, range);
|
|
3916
|
+
}
|
|
3917
|
+
}
|
|
3918
|
+
return {
|
|
3919
|
+
candidates,
|
|
3920
|
+
requirements,
|
|
3921
|
+
existingLinks
|
|
3922
|
+
};
|
|
3923
|
+
}
|
|
3924
|
+
/** Read `name → version` from a closure node_modules for the given names. */
|
|
3925
|
+
function readClosureVersions(closureNodeModulesDir, names) {
|
|
3926
|
+
const versions = /* @__PURE__ */ new Map();
|
|
3927
|
+
for (const name of names) {
|
|
3928
|
+
const manifest = readTreeManifest(path$39.join(closureNodeModulesDir, name, "package.json"));
|
|
3929
|
+
if (manifest !== null && manifest.version !== null) versions.set(name, manifest.version);
|
|
3930
|
+
}
|
|
3931
|
+
return versions;
|
|
3932
|
+
}
|
|
3933
|
+
/**
|
|
3934
|
+
* Where the running node's server closure keeps its node_modules, or null
|
|
3935
|
+
* when there is none to dedupe against (dev workspace, tests without a
|
|
3936
|
+
* closure). `CAMSTACK_FRAMEWORK_DIR` is set by the starter/launcher on every
|
|
3937
|
+
* deployed node — the same directory the framework resolver hook and
|
|
3938
|
+
* NODE_PATH extension already point at.
|
|
3939
|
+
*/
|
|
3940
|
+
function resolveClosureNodeModulesDir(explicitDir) {
|
|
3941
|
+
const frameworkDir = explicitDir ?? process.env["CAMSTACK_FRAMEWORK_DIR"];
|
|
3942
|
+
if (frameworkDir === void 0 || frameworkDir.length === 0) return null;
|
|
3943
|
+
const nodeModulesDir = explicitDir ?? path$39.join(frameworkDir, "node_modules");
|
|
3944
|
+
return fs$17.existsSync(nodeModulesDir) ? nodeModulesDir : null;
|
|
3945
|
+
}
|
|
3946
|
+
function symlinkType() {
|
|
3947
|
+
return process.platform === "win32" ? "junction" : "dir";
|
|
3948
|
+
}
|
|
3949
|
+
/**
|
|
3950
|
+
* Replace each planned entry with a symlink into the closure. Probes symlink
|
|
3951
|
+
* support once (Windows without developer mode cannot create them — in that
|
|
3952
|
+
* case every copy is kept, which is exactly today's behaviour). Each link
|
|
3953
|
+
* re-verifies the closure version right before the swap so a closure updated
|
|
3954
|
+
* mid-flight can only cause a KEEP, never a wrong link.
|
|
3955
|
+
*
|
|
3956
|
+
* Returns the names actually linked.
|
|
3957
|
+
*/
|
|
3958
|
+
function executeClosureDedupe(addonDir, closureNodeModulesDir, plan, logger) {
|
|
3959
|
+
if (plan.links.length === 0) return [];
|
|
3960
|
+
const nodeModulesDir = path$39.join(addonDir, "node_modules");
|
|
3961
|
+
const probePath = path$39.join(nodeModulesDir, ".closure-link-probe");
|
|
3962
|
+
try {
|
|
3963
|
+
fs$17.mkdirSync(nodeModulesDir, { recursive: true });
|
|
3964
|
+
fs$17.rmSync(probePath, { force: true });
|
|
3965
|
+
fs$17.symlinkSync(closureNodeModulesDir, probePath, symlinkType());
|
|
3966
|
+
fs$17.rmSync(probePath, { force: true });
|
|
3967
|
+
} catch (err) {
|
|
3968
|
+
logger.warn("closure dedupe skipped — symlinks unavailable on this volume", { meta: {
|
|
3969
|
+
addonDir,
|
|
3970
|
+
error: errMsg$1(err)
|
|
3971
|
+
} });
|
|
3972
|
+
return [];
|
|
3973
|
+
}
|
|
3974
|
+
const linked = [];
|
|
3975
|
+
for (const link of plan.links) {
|
|
3976
|
+
const target = path$39.join(closureNodeModulesDir, link.name);
|
|
3977
|
+
const manifest = readTreeManifest(path$39.join(target, "package.json"));
|
|
3978
|
+
if (manifest === null || manifest.version !== link.closureVersion) {
|
|
3979
|
+
logger.warn("closure dedupe — closure changed under the plan; keeping own copy", { meta: {
|
|
3980
|
+
name: link.name,
|
|
3981
|
+
planned: link.closureVersion,
|
|
3982
|
+
found: manifest?.version ?? null
|
|
3983
|
+
} });
|
|
3984
|
+
continue;
|
|
3985
|
+
}
|
|
3986
|
+
const entryDir = path$39.join(nodeModulesDir, link.name);
|
|
3987
|
+
try {
|
|
3988
|
+
fs$17.rmSync(entryDir, {
|
|
3989
|
+
recursive: true,
|
|
3990
|
+
force: true
|
|
3991
|
+
});
|
|
3992
|
+
fs$17.mkdirSync(path$39.dirname(entryDir), { recursive: true });
|
|
3993
|
+
fs$17.symlinkSync(target, entryDir, symlinkType());
|
|
3994
|
+
linked.push(link.name);
|
|
3995
|
+
} catch (err) {
|
|
3996
|
+
logger.warn("closure dedupe — link failed; entry removed copy may need reinstall", { meta: {
|
|
3997
|
+
name: link.name,
|
|
3998
|
+
error: errMsg$1(err)
|
|
3999
|
+
} });
|
|
4000
|
+
}
|
|
4001
|
+
}
|
|
4002
|
+
return linked;
|
|
4003
|
+
}
|
|
4004
|
+
/**
|
|
4005
|
+
* Is `linkTarget` (as read from `readlink`) a path inside the closure's
|
|
4006
|
+
* node_modules? Only such links belong to this mechanism — workspace/npm
|
|
4007
|
+
* `file:` links must never be touched by dedupe maintenance.
|
|
4008
|
+
*/
|
|
4009
|
+
function isClosureLinkTarget(linkTarget, closureNodeModulesDir) {
|
|
4010
|
+
if (linkTarget.length === 0) return false;
|
|
4011
|
+
const normalized = path$39.resolve(linkTarget);
|
|
4012
|
+
const root = path$39.resolve(closureNodeModulesDir);
|
|
4013
|
+
return normalized === root || normalized.startsWith(`${root}${path$39.sep}`);
|
|
4014
|
+
}
|
|
4015
|
+
//#endregion
|
|
3674
4016
|
//#region src/kernel/addon-installer.ts
|
|
3675
4017
|
var execFileAsync = promisify(execFile);
|
|
4018
|
+
/**
|
|
4019
|
+
* Names declared in `camstack.nativeDependencies` — per-addon natives that
|
|
4020
|
+
* the closure dedupe must never replace with a link (their install path owns
|
|
4021
|
+
* ABI/prebuild handling; see `deps/manifest-native-deps.ts`).
|
|
4022
|
+
*/
|
|
4023
|
+
function readNativeDependencyNames(pkg) {
|
|
4024
|
+
if (pkg === void 0) return /* @__PURE__ */ new Set();
|
|
4025
|
+
const camstack = asJsonObject$1(pkg["camstack"]);
|
|
4026
|
+
if (camstack === null) return /* @__PURE__ */ new Set();
|
|
4027
|
+
const native = asJsonObject$1(camstack["nativeDependencies"]);
|
|
4028
|
+
return native === null ? /* @__PURE__ */ new Set() : new Set(Object.keys(native));
|
|
4029
|
+
}
|
|
3676
4030
|
function parseInstallSource(value) {
|
|
3677
4031
|
switch (value) {
|
|
3678
4032
|
case "npm":
|
|
@@ -3769,6 +4123,8 @@ var AddonInstaller = class AddonInstaller {
|
|
|
3769
4123
|
installSource;
|
|
3770
4124
|
logger;
|
|
3771
4125
|
isPackageResolvable;
|
|
4126
|
+
/** Test seam — production resolves via `CAMSTACK_FRAMEWORK_DIR`. */
|
|
4127
|
+
closureNodeModulesDir;
|
|
3772
4128
|
/**
|
|
3773
4129
|
* Central manifest of installed addons. Tracks version + source +
|
|
3774
4130
|
* timestamps + last-backup pointer per package, written atomically
|
|
@@ -3790,6 +4146,7 @@ var AddonInstaller = class AddonInstaller {
|
|
|
3790
4146
|
this.logger = logger;
|
|
3791
4147
|
this.manifest = new AddonManifest(config.addonsDir);
|
|
3792
4148
|
this.isPackageResolvable = config.isPackageResolvable;
|
|
4149
|
+
this.closureNodeModulesDir = config.closureNodeModulesDir;
|
|
3793
4150
|
if (config.installSource) this.installSource = config.installSource;
|
|
3794
4151
|
else this.installSource = "npm";
|
|
3795
4152
|
}
|
|
@@ -3977,6 +4334,140 @@ var AddonInstaller = class AddonInstaller {
|
|
|
3977
4334
|
return changes;
|
|
3978
4335
|
}
|
|
3979
4336
|
/**
|
|
4337
|
+
* Make every INSTALLED addon's runtime dependencies resolvable — doing the
|
|
4338
|
+
* least work that achieves it. Runs at boot on hub and agent, which is what
|
|
4339
|
+
* turns a restore back into a working node: a system backup archives no
|
|
4340
|
+
* node_modules (D338), so before this pass every restored non-bootstrap
|
|
4341
|
+
* addon failed its first import until someone redeployed it by hand.
|
|
4342
|
+
*
|
|
4343
|
+
* Per addon, in order:
|
|
4344
|
+
* 1. drop closure links that no longer hold (dangling, or the closure
|
|
4345
|
+
* moved to a version the addon tree's recorded ranges refuse) — those
|
|
4346
|
+
* addons get a reinstall;
|
|
4347
|
+
* 2. a declared dep that already resolves in the addon tree costs nothing;
|
|
4348
|
+
* 3. a missing declared dep the closure provides at a COMPATIBLE version
|
|
4349
|
+
* becomes a link — no npm, no network;
|
|
4350
|
+
* 4. only an addon left with a genuinely unresolvable dep gets an npm
|
|
4351
|
+
* install (then dedupe + native ensure, same tail as a fresh install).
|
|
4352
|
+
*
|
|
4353
|
+
* "Only when necessary" is the contract: an addon whose deps are all
|
|
4354
|
+
* satisfied is not touched at all. Failures are per-addon and non-fatal —
|
|
4355
|
+
* a dep that cannot be re-fetched leaves that addon exactly as broken as
|
|
4356
|
+
* it already was, but LOUDLY, and never blocks the rest of the boot.
|
|
4357
|
+
*/
|
|
4358
|
+
async ensureAddonRuntimeDeps() {
|
|
4359
|
+
const closureNodeModules = resolveClosureNodeModulesDir(this.closureNodeModulesDir);
|
|
4360
|
+
const linked = [];
|
|
4361
|
+
const reinstalled = [];
|
|
4362
|
+
const failed = [];
|
|
4363
|
+
const installedPackages = this.listInstalled();
|
|
4364
|
+
for (const pkg of installedPackages) {
|
|
4365
|
+
try {
|
|
4366
|
+
if (fs$17.lstatSync(pkg.dir).isSymbolicLink()) continue;
|
|
4367
|
+
} catch {
|
|
4368
|
+
continue;
|
|
4369
|
+
}
|
|
4370
|
+
try {
|
|
4371
|
+
const outcome = this.ensureOneAddonDeps(pkg.dir, pkg.name, closureNodeModules);
|
|
4372
|
+
for (const dep of outcome.linked) linked.push(`${pkg.name}:${dep}`);
|
|
4373
|
+
if (outcome.needsInstall) {
|
|
4374
|
+
await this.reinstallAddonDeps(pkg.dir, pkg.name);
|
|
4375
|
+
reinstalled.push(pkg.name);
|
|
4376
|
+
}
|
|
4377
|
+
} catch (err) {
|
|
4378
|
+
failed.push(pkg.name);
|
|
4379
|
+
this.logger.error(`${pkg.name} — runtime dependencies could NOT be ensured; the addon will fail to load until its deps install`, { meta: {
|
|
4380
|
+
dir: pkg.dir,
|
|
4381
|
+
error: errMsg$1(err)
|
|
4382
|
+
} });
|
|
4383
|
+
}
|
|
4384
|
+
}
|
|
4385
|
+
return {
|
|
4386
|
+
checked: installedPackages.length,
|
|
4387
|
+
linked,
|
|
4388
|
+
reinstalled,
|
|
4389
|
+
failed
|
|
4390
|
+
};
|
|
4391
|
+
}
|
|
4392
|
+
/**
|
|
4393
|
+
* One addon's dependency audit (steps 1–3 of `ensureAddonRuntimeDeps`).
|
|
4394
|
+
* Pure decision + link creation; the npm fallback is the caller's.
|
|
4395
|
+
*/
|
|
4396
|
+
ensureOneAddonDeps(addonDir, packageName, closureNodeModules) {
|
|
4397
|
+
const pkgView = readPackageJson(path$39.join(addonDir, "package.json"));
|
|
4398
|
+
const declared = pkgView === null ? null : asJsonObject$1(pkgView.raw["dependencies"]);
|
|
4399
|
+
const declaredEntries = declared === null ? [] : Object.entries(declared);
|
|
4400
|
+
const state = collectAddonTreeState(addonDir);
|
|
4401
|
+
const nodeModulesDir = path$39.join(addonDir, "node_modules");
|
|
4402
|
+
let needsInstall = false;
|
|
4403
|
+
if (closureNodeModules !== null) for (const link of state.existingLinks) {
|
|
4404
|
+
if (!isClosureLinkTarget(link.target, closureNodeModules)) continue;
|
|
4405
|
+
const linkDir = path$39.join(nodeModulesDir, link.name);
|
|
4406
|
+
const targetManifest = fs$17.existsSync(path$39.join(linkDir, "package.json"));
|
|
4407
|
+
const ranges = state.requirements.get(link.name) ?? [];
|
|
4408
|
+
let holds = targetManifest;
|
|
4409
|
+
if (holds && ranges.length > 0) {
|
|
4410
|
+
const version = readClosureVersions(closureNodeModules, [link.name]).get(link.name);
|
|
4411
|
+
holds = version !== void 0 && ranges.every((range) => closureSatisfiesRange(range, version));
|
|
4412
|
+
}
|
|
4413
|
+
if (!holds) {
|
|
4414
|
+
this.logger.warn(`${packageName} — closure link for ${link.name} no longer holds; reinstalling this addon's deps`, { meta: { target: link.target } });
|
|
4415
|
+
fs$17.rmSync(linkDir, { force: true });
|
|
4416
|
+
needsInstall = true;
|
|
4417
|
+
}
|
|
4418
|
+
}
|
|
4419
|
+
const linked = [];
|
|
4420
|
+
for (const [depName, range] of declaredEntries) {
|
|
4421
|
+
if (typeof range !== "string") continue;
|
|
4422
|
+
if (fs$17.existsSync(path$39.join(nodeModulesDir, depName, "package.json"))) continue;
|
|
4423
|
+
const closureVersion = closureNodeModules === null ? void 0 : readClosureVersions(closureNodeModules, [depName]).get(depName);
|
|
4424
|
+
if (closureNodeModules !== null && closureVersion !== void 0 && closureSatisfiesRange(range, closureVersion)) {
|
|
4425
|
+
if (executeClosureDedupe(addonDir, closureNodeModules, {
|
|
4426
|
+
links: [{
|
|
4427
|
+
name: depName,
|
|
4428
|
+
closureVersion
|
|
4429
|
+
}],
|
|
4430
|
+
keeps: []
|
|
4431
|
+
}, this.logger).length === 1) {
|
|
4432
|
+
this.logger.info(`${packageName} — ${depName} linked from the server closure`, { meta: { closureVersion } });
|
|
4433
|
+
linked.push(depName);
|
|
4434
|
+
continue;
|
|
4435
|
+
}
|
|
4436
|
+
}
|
|
4437
|
+
needsInstall = true;
|
|
4438
|
+
}
|
|
4439
|
+
return {
|
|
4440
|
+
linked,
|
|
4441
|
+
needsInstall
|
|
4442
|
+
};
|
|
4443
|
+
}
|
|
4444
|
+
/**
|
|
4445
|
+
* npm-install an installed addon's runtime deps IN PLACE (the restore /
|
|
4446
|
+
* repair path — the tree is not yet running at boot), then the same tail
|
|
4447
|
+
* every fresh install gets: closure dedupe + native deps + prebuild ensure.
|
|
4448
|
+
* Closure links are stripped first so npm reifies against plain missing
|
|
4449
|
+
* dirs instead of symlinks it does not own.
|
|
4450
|
+
*/
|
|
4451
|
+
async reinstallAddonDeps(addonDir, packageName) {
|
|
4452
|
+
const closureNodeModules = resolveClosureNodeModulesDir(this.closureNodeModulesDir);
|
|
4453
|
+
if (closureNodeModules !== null) {
|
|
4454
|
+
const state = collectAddonTreeState(addonDir);
|
|
4455
|
+
for (const link of state.existingLinks) if (isClosureLinkTarget(link.target, closureNodeModules)) fs$17.rmSync(path$39.join(addonDir, "node_modules", link.name), { force: true });
|
|
4456
|
+
}
|
|
4457
|
+
this.logger.info(`${packageName} — reinstalling runtime dependencies`, { meta: { addonDir } });
|
|
4458
|
+
await runNpm([
|
|
4459
|
+
"install",
|
|
4460
|
+
"--omit=dev",
|
|
4461
|
+
"--omit=peer",
|
|
4462
|
+
"--no-audit",
|
|
4463
|
+
"--no-fund",
|
|
4464
|
+
"--no-package-lock",
|
|
4465
|
+
...this.registry ? ["--registry", this.registry] : []
|
|
4466
|
+
], this.npmRunOptions(addonDir, 24e4));
|
|
4467
|
+
this.dedupeTreeAgainstClosure(addonDir, packageName);
|
|
4468
|
+
await this.installNativeDependencies(addonDir, packageName);
|
|
4469
|
+
}
|
|
4470
|
+
/**
|
|
3980
4471
|
* Install a single addon — on-demand installer used by AddonPackageService
|
|
3981
4472
|
* (admin UI "Install addon" / "Reinstall" buttons).
|
|
3982
4473
|
*
|
|
@@ -4084,6 +4575,7 @@ var AddonInstaller = class AddonInstaller {
|
|
|
4084
4575
|
logger: this.logger,
|
|
4085
4576
|
bundledSourceDir: sourceDir
|
|
4086
4577
|
});
|
|
4578
|
+
this.dedupeTreeAgainstClosure(targetDir, packageName);
|
|
4087
4579
|
this.logger.info(`${packageName} — copied from ${sourceDir}`);
|
|
4088
4580
|
return {
|
|
4089
4581
|
name: pkgData.name,
|
|
@@ -4387,6 +4879,40 @@ var AddonInstaller = class AddonInstaller {
|
|
|
4387
4879
|
logger: this.logger
|
|
4388
4880
|
});
|
|
4389
4881
|
}
|
|
4882
|
+
/**
|
|
4883
|
+
* Stop copying what the server closure already provides: replace each
|
|
4884
|
+
* top-level `node_modules` entry the closure serves at a COMPATIBLE version
|
|
4885
|
+
* with a symlink into the closure (see `deps/closure-dedupe.ts` for the
|
|
4886
|
+
* plan rules and the live measurements). Runs LAST in tree
|
|
4887
|
+
* materialisation — after the runtime-dep npm install AND the native-dep
|
|
4888
|
+
* steps — so no later npm invocation can fight the links.
|
|
4889
|
+
*
|
|
4890
|
+
* Non-fatal by design: dedupe is an optimisation, and its failure mode is
|
|
4891
|
+
* "the addon keeps its own copies", which is exactly today's behaviour. A
|
|
4892
|
+
* refusal on version grounds is logged loudly — that line is the answer to
|
|
4893
|
+
* "why does this addon still carry its own zod".
|
|
4894
|
+
*/
|
|
4895
|
+
dedupeTreeAgainstClosure(pkgDir, packageName) {
|
|
4896
|
+
const closureNodeModules = resolveClosureNodeModulesDir(this.closureNodeModulesDir);
|
|
4897
|
+
if (closureNodeModules === null) return;
|
|
4898
|
+
try {
|
|
4899
|
+
const state = collectAddonTreeState(pkgDir);
|
|
4900
|
+
if (state.candidates.length === 0) return;
|
|
4901
|
+
const pkgView = readPackageJson(path$39.join(pkgDir, "package.json"));
|
|
4902
|
+
const plan = planClosureDedupe({
|
|
4903
|
+
candidates: state.candidates,
|
|
4904
|
+
closureVersions: readClosureVersions(closureNodeModules, state.candidates.map((c) => c.name)),
|
|
4905
|
+
requirements: state.requirements,
|
|
4906
|
+
neverLink: readNativeDependencyNames(pkgView?.raw)
|
|
4907
|
+
});
|
|
4908
|
+
const linked = executeClosureDedupe(pkgDir, closureNodeModules, plan, this.logger);
|
|
4909
|
+
if (linked.length > 0) this.logger.info(`${packageName} — ${linked.length} node_modules entr${linked.length === 1 ? "y" : "ies"} now resolve from the server closure`, { meta: { linked } });
|
|
4910
|
+
const refused = plan.keeps.filter((k) => k.reason === "version-incompatible");
|
|
4911
|
+
if (refused.length > 0) this.logger.info(`${packageName} — kept own copy where the closure version is incompatible`, { meta: { kept: refused.map((k) => k.name) } });
|
|
4912
|
+
} catch (err) {
|
|
4913
|
+
this.logger.warn(`${packageName} — closure dedupe failed (copies kept)`, { meta: { error: errMsg$1(err) } });
|
|
4914
|
+
}
|
|
4915
|
+
}
|
|
4390
4916
|
/** Install addon from a tgz file (uploaded or downloaded) */
|
|
4391
4917
|
async installFromTgz(tgzPath) {
|
|
4392
4918
|
const stagingRoot = path$39.join(this.addonsDir, ".staging");
|
|
@@ -4412,6 +4938,7 @@ var AddonInstaller = class AddonInstaller {
|
|
|
4412
4938
|
const targetDir = path$39.join(this.addonsDir, pkgView.name);
|
|
4413
4939
|
await this.installRuntimeDependencies(pkgDir, pkgView.name);
|
|
4414
4940
|
await this.installNativeDependencies(pkgDir, pkgView.name);
|
|
4941
|
+
this.dedupeTreeAgainstClosure(pkgDir, pkgView.name);
|
|
4415
4942
|
await this.evictInstallDir(targetDir);
|
|
4416
4943
|
ensureDir(path$39.dirname(targetDir));
|
|
4417
4944
|
await fs$17.promises.rename(pkgDir, targetDir);
|
|
@@ -4544,6 +5071,7 @@ var AddonInstaller = class AddonInstaller {
|
|
|
4544
5071
|
if (!fs$17.existsSync(addonDir)) throw new Error(`Cannot update ${packageName}: install dir ${addonDir} missing`);
|
|
4545
5072
|
await this.installRuntimeDependencies(stagedPath, packageName);
|
|
4546
5073
|
await this.installNativeDependencies(stagedPath, packageName);
|
|
5074
|
+
this.dedupeTreeAgainstClosure(stagedPath, packageName);
|
|
4547
5075
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
4548
5076
|
const backupRoot = path$39.join(this.addonsDir, ".backups", packageName);
|
|
4549
5077
|
ensureDir(backupRoot);
|
|
@@ -33208,7 +33736,7 @@ var require_transit = /* @__PURE__ */ __commonJSMin$1(((exports, module) => {
|
|
|
33208
33736
|
var P = require_packets();
|
|
33209
33737
|
var { Packet } = require_packets();
|
|
33210
33738
|
var E = require_errors$2();
|
|
33211
|
-
var { Transform } = __require("stream");
|
|
33739
|
+
var { Transform: Transform$1 } = __require("stream");
|
|
33212
33740
|
var { METRIC } = require_metrics$1();
|
|
33213
33741
|
var C = require_constants$7();
|
|
33214
33742
|
/**
|
|
@@ -33579,7 +34107,7 @@ var require_transit = /* @__PURE__ */ __commonJSMin$1(((exports, module) => {
|
|
|
33579
34107
|
if (!payload.stream && !stream && !payload.seq) return false;
|
|
33580
34108
|
if (!stream) {
|
|
33581
34109
|
this.logger.debug(`<= New stream is received from '${payload.sender}'. Seq: ${payload.seq}`);
|
|
33582
|
-
stream = new Transform({
|
|
34110
|
+
stream = new Transform$1({
|
|
33583
34111
|
objectMode: payload.headers?.$streamObjectMode,
|
|
33584
34112
|
transform: function(chunk, encoding, done) {
|
|
33585
34113
|
this.push(chunk);
|
|
@@ -33675,7 +34203,7 @@ var require_transit = /* @__PURE__ */ __commonJSMin$1(((exports, module) => {
|
|
|
33675
34203
|
if (!stream && !packet.stream && !packet.seq) return false;
|
|
33676
34204
|
if (!stream) {
|
|
33677
34205
|
this.logger.debug(`<= New stream is received from '${packet.sender}'. Seq: ${packet.seq}`);
|
|
33678
|
-
stream = new Transform({
|
|
34206
|
+
stream = new Transform$1({
|
|
33679
34207
|
objectMode: packet.headers?.$streamObjectMode,
|
|
33680
34208
|
transform: function(chunk, encoding, done) {
|
|
33681
34209
|
this.push(chunk);
|