@qvac/sdk 0.12.2 → 0.13.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/NOTICE +55 -1225
- package/README.md +12 -0
- package/dist/client/api/bci-transcribe.d.ts +57 -0
- package/dist/client/api/bci-transcribe.js +172 -0
- package/dist/client/api/get-model-info.d.ts +1 -1
- package/dist/client/api/index.d.ts +1 -0
- package/dist/client/api/index.js +1 -0
- package/dist/client/api/video.d.ts +26 -6
- package/dist/client/api/video.js +30 -7
- package/dist/client/api/vla.d.ts +11 -7
- package/dist/client/api/vla.js +11 -7
- package/dist/client/rpc/bare-client.js +2 -2
- package/dist/commands/bundle/constants.js +2 -0
- package/dist/examples/bci/bci-filesystem-streaming.d.ts +2 -0
- package/dist/examples/bci/bci-filesystem-streaming.js +77 -0
- package/dist/examples/bci/bci-filesystem.d.ts +2 -0
- package/dist/examples/bci/bci-filesystem.js +63 -0
- package/dist/examples/cache-management.js +0 -1
- package/dist/examples/cancel-by-request-id.js +0 -1
- package/dist/examples/classification/classify-image.js +1 -1
- package/dist/examples/completion-events.js +6 -2
- package/dist/examples/completion-stop-reason.d.ts +25 -0
- package/dist/examples/completion-stop-reason.js +65 -0
- package/dist/examples/concurrent-completion-collision.d.ts +2 -0
- package/dist/examples/concurrent-completion-collision.js +98 -0
- package/dist/examples/config-reload.js +1 -2
- package/dist/examples/default-config-usage.js +0 -1
- package/dist/examples/delegated-inference/composite.js +0 -1
- package/dist/examples/delegated-inference/consumer-profiled.js +0 -1
- package/dist/examples/delegated-inference/consumer.js +0 -1
- package/dist/examples/diffusion-esrgan-upscale.js +1 -3
- package/dist/examples/diffusion-flux2-klein-img2img.js +1 -1
- package/dist/examples/diffusion-flux2-klein.js +2 -2
- package/dist/examples/diffusion-img2img.js +1 -1
- package/dist/examples/diffusion-img2vid.d.ts +2 -0
- package/dist/examples/diffusion-img2vid.js +78 -0
- package/dist/examples/diffusion-simple.js +1 -1
- package/dist/examples/diffusion-txt2img.js +8 -3
- package/dist/examples/diffusion-txt2vid.js +2 -3
- package/dist/examples/download-with-blind-relays.js +0 -1
- package/dist/examples/embed-multi-gpu.js +1 -2
- package/dist/examples/embed-p2p.js +4 -2
- package/dist/examples/finetune/llamacpp-finetune.js +5 -3
- package/dist/examples/kv-cache-cleanup.js +0 -1
- package/dist/examples/kv-cache-custom-key.js +0 -1
- package/dist/examples/kv-cache-example.js +4 -2
- package/dist/examples/llamacpp-cache.js +0 -1
- package/dist/examples/llamacpp-dynamic-tools.js +4 -2
- package/dist/examples/llamacpp-filesystem.js +1 -1
- package/dist/examples/llamacpp-http.js +1 -1
- package/dist/examples/llamacpp-multi-gpu.js +2 -2
- package/dist/examples/llamacpp-multimodal.js +0 -1
- package/dist/examples/llamacpp-p2p.js +0 -1
- package/dist/examples/llamacpp-sharded.js +1 -1
- package/dist/examples/llamacpp-structured-output.js +0 -1
- package/dist/examples/logging-file-transport.js +0 -1
- package/dist/examples/logging-streaming.js +0 -2
- package/dist/examples/mcp-websearch.js +0 -1
- package/dist/examples/multi-model-demo.js +0 -2
- package/dist/examples/ocr-fasttext.js +0 -1
- package/dist/examples/plugins.js +1 -4
- package/dist/examples/profiling/basic.js +3 -2
- package/dist/examples/profiling/per-call.js +0 -1
- package/dist/examples/quickstart.js +0 -1
- package/dist/examples/rag/rag-chromadb.js +0 -1
- package/dist/examples/rag/rag-hyperdb/cancellation.js +0 -1
- package/dist/examples/rag/rag-hyperdb/ingest.js +0 -1
- package/dist/examples/rag/rag-hyperdb/pipeline.js +0 -1
- package/dist/examples/rag/rag-hyperdb/workspaces.js +0 -1
- package/dist/examples/rag/rag-lancedb.js +0 -1
- package/dist/examples/rag/rag-sqlite.js +0 -1
- package/dist/examples/suspend-resume.js +0 -1
- package/dist/examples/tools/llamacpp-native-tools.js +0 -1
- package/dist/examples/tools/llamacpp-tools-gemma4.js +1 -1
- package/dist/examples/tools/llamacpp-tools-pythonic.js +2 -2
- package/dist/examples/tools/llamacpp-tools-qwen35.js +1 -1
- package/dist/examples/transcription/parakeet-ctc-filesystem.js +1 -1
- package/dist/examples/transcription/parakeet-microphone-record.js +0 -1
- package/dist/examples/transcription/parakeet-microphone-stream.js +0 -1
- package/dist/examples/transcription/parakeet-sortformer.js +1 -2
- package/dist/examples/transcription/parakeet-tdt-filesystem.js +1 -1
- package/dist/examples/transcription/whispercpp-filesystem-streaming.js +10 -6
- package/dist/examples/transcription/whispercpp-filesystem.js +4 -2
- package/dist/examples/transcription/whispercpp-microphone-conversation.js +0 -1
- package/dist/examples/transcription/whispercpp-microphone-record.js +0 -1
- package/dist/examples/transcription/whispercpp-prompt.js +0 -1
- package/dist/examples/translation/translation-bergamot-batch.js +1 -2
- package/dist/examples/translation/translation-bergamot-pivot.js +3 -4
- package/dist/examples/translation/translation-bergamot.js +1 -2
- package/dist/examples/translation/translation-indic.js +1 -2
- package/dist/examples/translation/translation-llm-afriquegemma.js +3 -4
- package/dist/examples/translation/translation-llm-context.js +1 -2
- package/dist/examples/translation/translation-llm.js +2 -3
- package/dist/examples/translation/translation-stream.js +2 -3
- package/dist/examples/tts/chatterbox.js +1 -2
- package/dist/examples/tts/llm-to-tts-streaming.js +2 -4
- package/dist/examples/tts/supertonic-multilingual.js +1 -2
- package/dist/examples/tts/supertonic.js +1 -2
- package/dist/examples/vla-pi05.d.ts +2 -0
- package/dist/examples/vla-pi05.js +84 -0
- package/dist/examples/vla-smolvla.js +1 -1
- package/dist/examples/voice-assistant/voice-assistant.js +1 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +9 -7
- package/dist/models/registry/models.d.ts +869 -186
- package/dist/models/registry/models.js +3160 -2453
- package/dist/models/update-models/companions.d.ts +7 -0
- package/dist/models/update-models/companions.js +56 -0
- package/dist/models/update-models/naming.js +10 -0
- package/dist/pear/pre.js +2 -0
- package/dist/schemas/bci-config.d.ts +36 -0
- package/dist/schemas/bci-config.js +56 -0
- package/dist/schemas/bci.d.ts +177 -0
- package/dist/schemas/bci.js +95 -0
- package/dist/schemas/common.d.ts +395 -51
- package/dist/schemas/common.js +5 -0
- package/dist/schemas/completion-event.d.ts +1 -0
- package/dist/schemas/download-asset.d.ts +7 -3
- package/dist/schemas/embed.d.ts +2 -0
- package/dist/schemas/embed.js +1 -0
- package/dist/schemas/engine-addon-map.d.ts +1 -0
- package/dist/schemas/engine-addon-map.js +4 -1
- package/dist/schemas/get-model-info.d.ts +2 -0
- package/dist/schemas/get-model-info.js +1 -0
- package/dist/schemas/index.d.ts +2 -0
- package/dist/schemas/index.js +2 -0
- package/dist/schemas/llamacpp-config.d.ts +6 -2
- package/dist/schemas/load-model.d.ts +1841 -267
- package/dist/schemas/load-model.js +34 -1
- package/dist/schemas/model-src-utils.d.ts +10 -2
- package/dist/schemas/model-types.d.ts +19 -1
- package/dist/schemas/model-types.js +11 -0
- package/dist/schemas/ocr.d.ts +2 -0
- package/dist/schemas/plugin.d.ts +13 -5
- package/dist/schemas/plugin.js +13 -4
- package/dist/schemas/registry.d.ts +18 -0
- package/dist/schemas/registry.js +2 -0
- package/dist/schemas/reload-config.d.ts +10 -2
- package/dist/schemas/sdcpp-config.d.ts +75 -8
- package/dist/schemas/sdcpp-config.js +56 -10
- package/dist/schemas/sdk-config.d.ts +342 -0
- package/dist/schemas/text-to-speech.d.ts +8 -0
- package/dist/schemas/transcription-config.d.ts +2 -0
- package/dist/schemas/transcription.d.ts +12 -0
- package/dist/schemas/transcription.js +8 -0
- package/dist/schemas/translation-config.d.ts +44 -16
- package/dist/schemas/translation-config.js +2 -0
- package/dist/schemas/vla.d.ts +14 -0
- package/dist/schemas/vla.js +26 -3
- package/dist/server/bare/delegate-connect-diagnostics.d.ts +9 -0
- package/dist/server/bare/delegate-connect-diagnostics.js +44 -0
- package/dist/server/bare/delegate-rpc-client.js +10 -12
- package/dist/server/bare/hyperswarm.js +1 -0
- package/dist/server/bare/ops/bci-transcribe.d.ts +13 -0
- package/dist/server/bare/ops/bci-transcribe.js +248 -0
- package/dist/server/bare/ops/embed.js +2 -1
- package/dist/server/bare/ops/kv-cache-utils.js +4 -3
- package/dist/server/bare/ops/transcribe.js +6 -2
- package/dist/server/bare/ops/translate.js +1 -1
- package/dist/server/bare/plugins/bci-whispercpp-transcription/plugin.d.ts +130 -0
- package/dist/server/bare/plugins/bci-whispercpp-transcription/plugin.js +116 -0
- package/dist/server/bare/plugins/ggml-vla/ops/vla-run.js +4 -0
- package/dist/server/bare/plugins/ggml-vla/plugin.d.ts +7 -0
- package/dist/server/bare/plugins/ggml-vla/plugin.js +1 -1
- package/dist/server/bare/plugins/index.d.ts +1 -0
- package/dist/server/bare/plugins/index.js +1 -0
- package/dist/server/bare/plugins/llamacpp-completion/ops/finetune.js +22 -29
- package/dist/server/bare/plugins/llamacpp-completion/plugin.d.ts +2 -0
- package/dist/server/bare/plugins/llamacpp-completion/plugin.js +38 -7
- package/dist/server/bare/plugins/llamacpp-embedding/plugin.d.ts +1 -0
- package/dist/server/bare/plugins/llamacpp-embedding/plugin.js +9 -1
- package/dist/server/bare/plugins/nmtcpp-translation/plugin.d.ts +14 -0
- package/dist/server/bare/plugins/onnx-ocr/plugin.d.ts +2 -0
- package/dist/server/bare/plugins/parakeet-transcription/plugin.d.ts +8 -0
- package/dist/server/bare/plugins/sdcpp-generation/ops/video.js +7 -1
- package/dist/server/bare/plugins/sdcpp-generation/plugin.d.ts +58 -4
- package/dist/server/bare/plugins/sdcpp-generation/plugin.js +9 -4
- package/dist/server/bare/plugins/whispercpp-transcription/plugin.d.ts +10 -0
- package/dist/server/bare/registry/model-config-utils.js +3 -0
- package/dist/server/bare/runtime/request-registry-singleton.js +11 -4
- package/dist/server/bare/runtime/request-registry.d.ts +60 -16
- package/dist/server/bare/runtime/request-registry.js +258 -15
- package/dist/server/bare/runtime/with-request-context.d.ts +1 -1
- package/dist/server/bare/runtime/with-request-context.js +1 -1
- package/dist/server/bare/types/addon-responses.d.ts +5 -0
- package/dist/server/rpc/handler-registry.js +4 -0
- package/dist/server/rpc/handlers/bci-transcribe-stream.d.ts +3 -0
- package/dist/server/rpc/handlers/bci-transcribe-stream.js +4 -0
- package/dist/server/rpc/handlers/bci-transcribe.d.ts +3 -0
- package/dist/server/rpc/handlers/bci-transcribe.js +4 -0
- package/dist/server/rpc/handlers/download-asset.js +1 -1
- package/dist/server/rpc/handlers/get-model-info.js +3 -3
- package/dist/server/rpc/handlers/index.d.ts +4 -0
- package/dist/server/rpc/handlers/index.js +4 -0
- package/dist/server/rpc/handlers/load-model/download-manager.d.ts +1 -1
- package/dist/server/rpc/handlers/load-model/handler.js +1 -1
- package/dist/server/rpc/handlers/load-model/http.js +6 -6
- package/dist/server/rpc/handlers/load-model/hyperdrive.js +1 -0
- package/dist/server/rpc/handlers/load-model/registry-companion-set.d.ts +1 -0
- package/dist/server/rpc/handlers/load-model/registry-companion-set.js +2 -2
- package/dist/server/rpc/handlers/load-model/registry.js +1 -0
- package/dist/server/rpc/handlers/load-model/resolve-session.d.ts +1 -1
- package/dist/server/rpc/handlers/rag.js +5 -5
- package/dist/server/utils/audio/decoder.js +1 -1
- package/dist/server/utils/checksum.js +2 -1
- package/dist/server/utils/formatting.js +2 -1
- package/dist/server/utils/multi-gpu-mobile.d.ts +5 -0
- package/dist/server/utils/multi-gpu-mobile.js +10 -0
- package/dist/server/worker.js +2 -1
- package/dist/utils/aggregate-events.d.ts +2 -1
- package/dist/utils/aggregate-events.js +11 -3
- package/dist/utils/errors-server.d.ts +6 -4
- package/dist/utils/errors-server.js +6 -4
- package/electron-forge/index.cjs +820 -0
- package/package.json +70 -37
- package/dist/scripts/run-security-tests-bare.d.ts +0 -2
- package/dist/scripts/run-security-tests-bare.js +0 -78
|
@@ -0,0 +1,820 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @qvac/sdk/electron-forge
|
|
3
|
+
*
|
|
4
|
+
* Electron Forge plugin: bundles the QVAC worker, verifies its native addons,
|
|
5
|
+
* then configures Electron Packager to tree-shake unused @qvac/* addons and
|
|
6
|
+
* non-target prebuilds.
|
|
7
|
+
*
|
|
8
|
+
* macOS universal (`arch: "universal"`) is not supported — native addon
|
|
9
|
+
* prebuilds are arch-specific. Build darwin-arm64 and darwin-x64 separately.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
"use strict";
|
|
13
|
+
|
|
14
|
+
const { PluginBase } = require("@electron-forge/plugin-base");
|
|
15
|
+
const { createRequire } = require("module");
|
|
16
|
+
const path = require("path");
|
|
17
|
+
const fs = require("fs");
|
|
18
|
+
|
|
19
|
+
// ============================================
|
|
20
|
+
// Errors
|
|
21
|
+
// ============================================
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Plugin-originated error. Stack is trimmed to `name: message` so Forge's
|
|
25
|
+
* "unhandled rejection" block stays focused on the cause.
|
|
26
|
+
*/
|
|
27
|
+
class QvacForgePluginError extends Error {
|
|
28
|
+
constructor(message) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = "QvacForgePluginError";
|
|
31
|
+
this.stack = `${this.name}: ${this.message}`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ============================================
|
|
36
|
+
// Logger
|
|
37
|
+
// ============================================
|
|
38
|
+
|
|
39
|
+
const PREFIX = "[qvac:electron-forge]";
|
|
40
|
+
|
|
41
|
+
const EXPECTED_FS_ERROR_CODES = new Set([
|
|
42
|
+
"ENOENT",
|
|
43
|
+
"EACCES",
|
|
44
|
+
"EPERM",
|
|
45
|
+
"ENOTDIR",
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
const LOG_LEVELS = {
|
|
49
|
+
off: 0,
|
|
50
|
+
error: 1,
|
|
51
|
+
warn: 2,
|
|
52
|
+
info: 3,
|
|
53
|
+
debug: 4,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function getDefaultLevel() {
|
|
57
|
+
const level = process.env.QVAC_LOG_LEVEL?.toLowerCase();
|
|
58
|
+
return level && level in LOG_LEVELS ? level : "info";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let currentLevel = LOG_LEVELS[getDefaultLevel()];
|
|
62
|
+
|
|
63
|
+
function setLogLevel(level) {
|
|
64
|
+
if (!(level in LOG_LEVELS)) {
|
|
65
|
+
console.warn(
|
|
66
|
+
`${PREFIX} Invalid log level "${level}", using "info". Valid: ${Object.keys(LOG_LEVELS).join(", ")}`,
|
|
67
|
+
);
|
|
68
|
+
currentLevel = LOG_LEVELS.info;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
currentLevel = LOG_LEVELS[level];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const logger = {
|
|
75
|
+
error(msg) {
|
|
76
|
+
if (currentLevel >= LOG_LEVELS.error) console.error(PREFIX, msg);
|
|
77
|
+
},
|
|
78
|
+
warn(msg) {
|
|
79
|
+
if (currentLevel >= LOG_LEVELS.warn) console.warn(PREFIX, msg);
|
|
80
|
+
},
|
|
81
|
+
info(msg) {
|
|
82
|
+
if (currentLevel >= LOG_LEVELS.info) console.log(PREFIX, msg);
|
|
83
|
+
},
|
|
84
|
+
debug(msg) {
|
|
85
|
+
if (currentLevel >= LOG_LEVELS.debug) console.log(PREFIX, msg);
|
|
86
|
+
},
|
|
87
|
+
fsError(context, err) {
|
|
88
|
+
if (err && EXPECTED_FS_ERROR_CODES.has(err.code)) return;
|
|
89
|
+
this.warn(`Unexpected error in ${context}: ${err?.message || err}`);
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// ============================================
|
|
94
|
+
// SDK Package Resolution (for addon discovery)
|
|
95
|
+
// ============================================
|
|
96
|
+
|
|
97
|
+
const SDK_PACKAGE_NAMES = ["@qvac/sdk"];
|
|
98
|
+
|
|
99
|
+
function resolveSDKPackage(startDir) {
|
|
100
|
+
for (const name of SDK_PACKAGE_NAMES) {
|
|
101
|
+
try {
|
|
102
|
+
const pkgPath = require.resolve(`${name}/package`, { paths: [startDir] });
|
|
103
|
+
return { name, path: pkgPath };
|
|
104
|
+
} catch {
|
|
105
|
+
// Try next package name
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isDir(dirPath) {
|
|
112
|
+
try {
|
|
113
|
+
return fs.statSync(dirPath).isDirectory();
|
|
114
|
+
} catch (err) {
|
|
115
|
+
logger.fsError("isDir", err);
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Finds the @qvac scope directory using Node's module resolution.
|
|
122
|
+
* Handles monorepos, workspaces, and hoisted layouts.
|
|
123
|
+
*/
|
|
124
|
+
function findQvacScopeDir(startDir) {
|
|
125
|
+
const sdkPkg = resolveSDKPackage(startDir);
|
|
126
|
+
if (!sdkPkg) {
|
|
127
|
+
throw new QvacForgePluginError(
|
|
128
|
+
`Could not find QVAC SDK. ` +
|
|
129
|
+
`Ensure one of [${SDK_PACKAGE_NAMES.join(", ")}] is installed.`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
logger.debug(`Resolved SDK package: ${sdkPkg.name}`);
|
|
134
|
+
|
|
135
|
+
const baseDir = path.resolve(startDir);
|
|
136
|
+
const req = createRequire(path.join(baseDir, "package.json"));
|
|
137
|
+
const nodeModulesDirs = req.resolve.paths(sdkPkg.name) || [];
|
|
138
|
+
|
|
139
|
+
for (const nodeModulesDir of nodeModulesDirs) {
|
|
140
|
+
const scopeDir = path.join(nodeModulesDir, "@qvac");
|
|
141
|
+
if (isDir(scopeDir)) return scopeDir;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Fallback: derive from SDK path for flat node_modules layouts.
|
|
145
|
+
const derived = path.dirname(path.dirname(sdkPkg.path));
|
|
146
|
+
if (path.basename(derived) === "@qvac" && isDir(derived)) return derived;
|
|
147
|
+
|
|
148
|
+
throw new QvacForgePluginError(
|
|
149
|
+
`Could not find @qvac packages. ` +
|
|
150
|
+
`Ensure dependencies are installed under node_modules (PnP is not supported).`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Discovers installed @qvac addon packages by scanning node_modules/@qvac
|
|
156
|
+
* for packages that have `addon: true` in package.json.
|
|
157
|
+
*/
|
|
158
|
+
function discoverQvacAddonPackages(projectDir) {
|
|
159
|
+
let scopeDir;
|
|
160
|
+
try {
|
|
161
|
+
scopeDir = findQvacScopeDir(projectDir);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
logger.warn(err.message);
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let entries;
|
|
168
|
+
try {
|
|
169
|
+
entries = fs.readdirSync(scopeDir);
|
|
170
|
+
} catch (err) {
|
|
171
|
+
logger.fsError("discoverQvacAddonPackages", err);
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const discovered = [];
|
|
176
|
+
|
|
177
|
+
for (const name of entries) {
|
|
178
|
+
const pkgDir = path.join(scopeDir, name);
|
|
179
|
+
if (!isDir(pkgDir)) continue;
|
|
180
|
+
|
|
181
|
+
const pkgJsonPath = path.join(pkgDir, "package.json");
|
|
182
|
+
if (!fs.existsSync(pkgJsonPath)) continue;
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
|
|
186
|
+
if (pkg.addon === true) {
|
|
187
|
+
discovered.push(`@qvac/${name}`);
|
|
188
|
+
}
|
|
189
|
+
} catch (err) {
|
|
190
|
+
logger.warn(`Failed to parse ${pkgJsonPath}: ${err.message}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
discovered.sort();
|
|
195
|
+
return discovered;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Pure diff: addons that are installed but not in the required set.
|
|
200
|
+
* Exposed for unit testing.
|
|
201
|
+
*/
|
|
202
|
+
function diffAddons(installed, required) {
|
|
203
|
+
const requiredSet = new Set(required);
|
|
204
|
+
const exclusions = [];
|
|
205
|
+
for (const pkg of installed) {
|
|
206
|
+
if (!requiredSet.has(pkg)) exclusions.push(pkg);
|
|
207
|
+
}
|
|
208
|
+
return exclusions;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Computes the list of installed @qvac addons that aren't in `required`.
|
|
213
|
+
* Logs include/exclude decisions for each discovered package.
|
|
214
|
+
*/
|
|
215
|
+
function computeExclusions(required, projectDir) {
|
|
216
|
+
const installed = discoverQvacAddonPackages(projectDir);
|
|
217
|
+
|
|
218
|
+
if (installed.length === 0) {
|
|
219
|
+
logger.warn(
|
|
220
|
+
"No @qvac addon packages discovered. Skipping addon exclusions.",
|
|
221
|
+
);
|
|
222
|
+
return [];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const requiredSet = new Set(required);
|
|
226
|
+
const exclusions = [];
|
|
227
|
+
for (const pkg of installed) {
|
|
228
|
+
if (requiredSet.has(pkg)) {
|
|
229
|
+
logger.info(`Including required addon: ${pkg}`);
|
|
230
|
+
} else {
|
|
231
|
+
logger.info(`Excluding unused addon: ${pkg}`);
|
|
232
|
+
exclusions.push(pkg);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return exclusions;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ============================================
|
|
239
|
+
// Ignore Patterns
|
|
240
|
+
// ============================================
|
|
241
|
+
|
|
242
|
+
/** Mobile prebuild patterns to always exclude in desktop builds. */
|
|
243
|
+
const MOBILE_PREBUILD_PATTERNS = [
|
|
244
|
+
/[\\/]prebuilds[\\/]android-/,
|
|
245
|
+
/[\\/]prebuilds[\\/]ios-/,
|
|
246
|
+
];
|
|
247
|
+
|
|
248
|
+
/** Forge's default output dir — always exclude to avoid recursive packaging. */
|
|
249
|
+
const OUT_DIR_PATTERN = /^\/out\//;
|
|
250
|
+
|
|
251
|
+
function toArray(value) {
|
|
252
|
+
if (value === undefined) return [];
|
|
253
|
+
return Array.isArray(value) ? value : [value];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function escapeRegExp(str) {
|
|
257
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function createAddonIgnorePatterns(exclusions) {
|
|
261
|
+
const patterns = [];
|
|
262
|
+
for (const addon of exclusions) {
|
|
263
|
+
const parts = addon.split("/").map(escapeRegExp);
|
|
264
|
+
patterns.push(
|
|
265
|
+
new RegExp(
|
|
266
|
+
`[\\\\/]node_modules[\\\\/]${parts.join("[\\\\/]")}([\\\\/]|$)`,
|
|
267
|
+
),
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
return patterns;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Builds an Electron Packager `ignore` value that excludes the given addon
|
|
275
|
+
* packages and mobile prebuilds, while composing with any user-provided
|
|
276
|
+
* ignore (function or regex array).
|
|
277
|
+
*/
|
|
278
|
+
function createIgnore(exclusions, existingIgnore) {
|
|
279
|
+
const addonIgnorePatterns = createAddonIgnorePatterns(exclusions);
|
|
280
|
+
|
|
281
|
+
if (typeof existingIgnore === "function") {
|
|
282
|
+
return (filePath) => {
|
|
283
|
+
if (existingIgnore(filePath)) return true;
|
|
284
|
+
if (OUT_DIR_PATTERN.test(filePath)) return true;
|
|
285
|
+
for (const pattern of addonIgnorePatterns) {
|
|
286
|
+
if (pattern.test(filePath)) return true;
|
|
287
|
+
}
|
|
288
|
+
for (const pattern of MOBILE_PREBUILD_PATTERNS) {
|
|
289
|
+
if (pattern.test(filePath)) return true;
|
|
290
|
+
}
|
|
291
|
+
return false;
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return [
|
|
296
|
+
...toArray(existingIgnore),
|
|
297
|
+
OUT_DIR_PATTERN,
|
|
298
|
+
...MOBILE_PREBUILD_PATTERNS,
|
|
299
|
+
...addonIgnorePatterns,
|
|
300
|
+
];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ============================================
|
|
304
|
+
// Prebuild Pruning
|
|
305
|
+
// ============================================
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Recursively finds all prebuilds directories under a root path, including
|
|
309
|
+
* inside nested node_modules. Nested node_modules show up in real-world npm
|
|
310
|
+
* trees whenever a transitive dep can't be hoisted (e.g. bare-tls under
|
|
311
|
+
* bare-fetch when version constraints conflict). Skipping those misses real
|
|
312
|
+
* cross-platform prebuilds that would otherwise ship in the packaged app.
|
|
313
|
+
*/
|
|
314
|
+
function findPrebuildsDirs(rootPath) {
|
|
315
|
+
const results = [];
|
|
316
|
+
|
|
317
|
+
function walk(dir) {
|
|
318
|
+
let entries;
|
|
319
|
+
try {
|
|
320
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
321
|
+
} catch (err) {
|
|
322
|
+
logger.fsError("findPrebuildsDirs", err);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
for (const entry of entries) {
|
|
327
|
+
if (!entry.isDirectory()) continue;
|
|
328
|
+
const fullPath = path.join(dir, entry.name);
|
|
329
|
+
|
|
330
|
+
if (entry.name === "prebuilds") {
|
|
331
|
+
results.push(fullPath);
|
|
332
|
+
// Don't recurse into prebuilds — its children are arch-keyed leaves.
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
walk(fullPath);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
walk(rootPath);
|
|
341
|
+
return results;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function getDirSize(dirPath) {
|
|
345
|
+
let size = 0;
|
|
346
|
+
|
|
347
|
+
function walk(dir) {
|
|
348
|
+
let entries;
|
|
349
|
+
try {
|
|
350
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
351
|
+
} catch (err) {
|
|
352
|
+
logger.fsError("getDirSize", err);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
for (const entry of entries) {
|
|
357
|
+
const fullPath = path.join(dir, entry.name);
|
|
358
|
+
if (entry.isDirectory()) {
|
|
359
|
+
walk(fullPath);
|
|
360
|
+
} else {
|
|
361
|
+
try {
|
|
362
|
+
size += fs.statSync(fullPath).size;
|
|
363
|
+
} catch (err) {
|
|
364
|
+
logger.fsError("getDirSize.stat", err);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
walk(dirPath);
|
|
371
|
+
return size;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** Prunes prebuilds for a given path, keeping only target platform-arch. */
|
|
375
|
+
function prunePrebuildsForPath(buildPath, platform, arch) {
|
|
376
|
+
const nodeModulesPath = path.join(buildPath, "node_modules");
|
|
377
|
+
|
|
378
|
+
if (!fs.existsSync(nodeModulesPath)) {
|
|
379
|
+
logger.debug("No node_modules found, skipping prebuild pruning.");
|
|
380
|
+
return { deleted: 0, bytes: 0 };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const keepPrefix = `${platform}-${arch}`;
|
|
384
|
+
logger.debug(`Keeping prefix: ${keepPrefix}`);
|
|
385
|
+
|
|
386
|
+
const prebuildsDirs = findPrebuildsDirs(nodeModulesPath);
|
|
387
|
+
let totalDeleted = 0;
|
|
388
|
+
let totalBytes = 0;
|
|
389
|
+
|
|
390
|
+
for (const prebuildsDir of prebuildsDirs) {
|
|
391
|
+
let entries;
|
|
392
|
+
try {
|
|
393
|
+
entries = fs.readdirSync(prebuildsDir, { withFileTypes: true });
|
|
394
|
+
} catch (err) {
|
|
395
|
+
logger.fsError("prunePrebuildsForPath", err);
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
for (const entry of entries) {
|
|
400
|
+
if (!entry.isDirectory()) continue;
|
|
401
|
+
|
|
402
|
+
// Prefix match catches musl variants etc. (e.g. linux-x64-musl).
|
|
403
|
+
const shouldKeep = entry.name.startsWith(keepPrefix);
|
|
404
|
+
|
|
405
|
+
if (!shouldKeep) {
|
|
406
|
+
const fullPath = path.join(prebuildsDir, entry.name);
|
|
407
|
+
try {
|
|
408
|
+
const size = getDirSize(fullPath);
|
|
409
|
+
totalBytes += size;
|
|
410
|
+
fs.rmSync(fullPath, { recursive: true, force: true });
|
|
411
|
+
totalDeleted++;
|
|
412
|
+
logger.debug(`Deleted: ${entry.name}`);
|
|
413
|
+
} catch (err) {
|
|
414
|
+
logger.warn(`Failed to delete ${fullPath}: ${err.message}`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return { deleted: totalDeleted, bytes: totalBytes };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Removes excluded addon directories from the packaged node_modules.
|
|
425
|
+
*
|
|
426
|
+
* Electron Packager's `ignore` filter excludes file contents but leaves the
|
|
427
|
+
* empty parent directory in the output. This sweep removes those shells so
|
|
428
|
+
* the packaged tree matches the manifest exactly. Idempotent.
|
|
429
|
+
*/
|
|
430
|
+
function removeExcludedAddonDirs(buildPath, exclusions) {
|
|
431
|
+
const nodeModulesPath = path.join(buildPath, "node_modules");
|
|
432
|
+
|
|
433
|
+
if (!fs.existsSync(nodeModulesPath)) {
|
|
434
|
+
return { removed: 0, bytes: 0 };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
let removed = 0;
|
|
438
|
+
let bytes = 0;
|
|
439
|
+
|
|
440
|
+
for (const name of exclusions) {
|
|
441
|
+
const pkgDir = path.join(nodeModulesPath, name);
|
|
442
|
+
if (!fs.existsSync(pkgDir)) continue;
|
|
443
|
+
|
|
444
|
+
try {
|
|
445
|
+
const size = getDirSize(pkgDir);
|
|
446
|
+
bytes += size;
|
|
447
|
+
fs.rmSync(pkgDir, { recursive: true, force: true });
|
|
448
|
+
removed += 1;
|
|
449
|
+
logger.debug(`Removed excluded addon dir: ${name}`);
|
|
450
|
+
} catch (err) {
|
|
451
|
+
logger.warn(`Failed to remove ${pkgDir}: ${err.message}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return { removed, bytes };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ============================================
|
|
459
|
+
// SDK Commands Bridge (CJS plugin → ESM SDK commands)
|
|
460
|
+
// ============================================
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Lazy dynamic-imports `@qvac/sdk/commands`. Plugin file is CJS for Forge's
|
|
464
|
+
* `require()`-based loader; the SDK's commands module is ESM-only via the
|
|
465
|
+
* package's "import" condition. Dynamic `import()` bridges them.
|
|
466
|
+
*
|
|
467
|
+
* @returns {Promise<{
|
|
468
|
+
* bundleSdk: Function,
|
|
469
|
+
* verifyBundle: Function,
|
|
470
|
+
* hasErrors: Function,
|
|
471
|
+
* formatVerifyBundleResult: Function,
|
|
472
|
+
* }>}
|
|
473
|
+
*/
|
|
474
|
+
async function loadSdkCommands() {
|
|
475
|
+
try {
|
|
476
|
+
return await import("@qvac/sdk/commands");
|
|
477
|
+
} catch (err) {
|
|
478
|
+
throw new QvacForgePluginError(
|
|
479
|
+
`Could not load @qvac/sdk/commands: ${err?.message || err}. ` +
|
|
480
|
+
`Ensure @qvac/sdk is installed and exposes the ./commands subpath ` +
|
|
481
|
+
`(requires SDK >= 0.12.0).`,
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** Defaults to verifying against the host running Forge. */
|
|
487
|
+
function defaultHosts() {
|
|
488
|
+
return [`${process.platform}-${process.arch}`];
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Single source of truth for the hosts list used by both bundleSdk and
|
|
493
|
+
* verifyBundle. Keeping these in sync matters: bundleSdk's `hosts` drives
|
|
494
|
+
* the bare-pack content (which prebuilds end up in the bundle), and
|
|
495
|
+
* verifyBundle checks the same set. Passing different lists silently
|
|
496
|
+
* produces inconsistent builds — bundle for one set, verify for another.
|
|
497
|
+
*
|
|
498
|
+
* @param {string[]|null|undefined} explicitHosts
|
|
499
|
+
* @returns {string[]}
|
|
500
|
+
*/
|
|
501
|
+
function resolveHosts(explicitHosts) {
|
|
502
|
+
return Array.isArray(explicitHosts) && explicitHosts.length > 0
|
|
503
|
+
? explicitHosts
|
|
504
|
+
: defaultHosts();
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Detects target packaging hosts from Forge config + CLI argv. Returns null
|
|
509
|
+
* when nothing's specified so callers can fall back to defaultHosts().
|
|
510
|
+
*
|
|
511
|
+
* Forge populates packagerConfig from the config file but threads CLI
|
|
512
|
+
* `--platform`/`--arch` separately into Packager — at resolveForgeConfig time
|
|
513
|
+
* argv is the only signal we have. Mirrors checkForUniversalArch's parsing.
|
|
514
|
+
*
|
|
515
|
+
* Comma-separated `--arch=arm64,x64` expands to one host per arch.
|
|
516
|
+
*
|
|
517
|
+
* @param {object} forgeConfig
|
|
518
|
+
* @param {string[]} [argv] Override for testing (default: process.argv.slice(2)).
|
|
519
|
+
* @returns {string[]|null}
|
|
520
|
+
*/
|
|
521
|
+
function detectTargetHosts(forgeConfig, argv = process.argv.slice(2)) {
|
|
522
|
+
let platform = forgeConfig?.packagerConfig?.platform || null;
|
|
523
|
+
let archSpec = forgeConfig?.packagerConfig?.arch || null;
|
|
524
|
+
|
|
525
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
526
|
+
const arg = argv[i];
|
|
527
|
+
|
|
528
|
+
if (arg === "--platform" || arg === "-p") {
|
|
529
|
+
platform = argv[i + 1] || platform;
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
if (typeof arg === "string" && arg.startsWith("--platform=")) {
|
|
533
|
+
platform = arg.slice("--platform=".length) || platform;
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
if (arg === "--arch" || arg === "-a") {
|
|
537
|
+
archSpec = argv[i + 1] || archSpec;
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
if (typeof arg === "string" && arg.startsWith("--arch=")) {
|
|
541
|
+
archSpec = arg.slice("--arch=".length) || archSpec;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (!platform && !archSpec) return null;
|
|
546
|
+
|
|
547
|
+
const p = platform || process.platform;
|
|
548
|
+
const archs = (archSpec || process.arch)
|
|
549
|
+
.split(",")
|
|
550
|
+
.map((s) => s.trim())
|
|
551
|
+
.filter(Boolean);
|
|
552
|
+
|
|
553
|
+
return archs.length > 0 ? archs.map((a) => `${p}-${a}`) : null;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Runs `bundleSdk` then `verifyBundle` with the same resolved `hosts` list.
|
|
558
|
+
* Throws QvacForgePluginError on bundle failure or verify errors.
|
|
559
|
+
*
|
|
560
|
+
* Commands are injected (rather than dynamically imported here) so tests
|
|
561
|
+
* can assert the host-list wiring without touching the SDK runtime.
|
|
562
|
+
*
|
|
563
|
+
* @param {{ bundleSdk: Function, verifyBundle: Function, hasErrors: Function, formatVerifyBundleResult: Function }} commands
|
|
564
|
+
* @param {string} projectDir
|
|
565
|
+
* @param {{ configPath?: string|null, hosts?: string[]|null }} options
|
|
566
|
+
* @returns {Promise<{ addons: string[], bundlePath: string, manifestPath: string }>}
|
|
567
|
+
*/
|
|
568
|
+
async function runBundleAndVerify(commands, projectDir, options) {
|
|
569
|
+
const { bundleSdk, verifyBundle, hasErrors, formatVerifyBundleResult } =
|
|
570
|
+
commands;
|
|
571
|
+
|
|
572
|
+
const hosts = resolveHosts(options.hosts);
|
|
573
|
+
|
|
574
|
+
logger.info(`Running bundleSdk (hosts: ${hosts.join(", ")})...`);
|
|
575
|
+
let bundleResult;
|
|
576
|
+
try {
|
|
577
|
+
const bundleOpts = { projectRoot: projectDir, hosts };
|
|
578
|
+
if (options.configPath) bundleOpts.configPath = options.configPath;
|
|
579
|
+
bundleResult = await bundleSdk(bundleOpts);
|
|
580
|
+
} catch (err) {
|
|
581
|
+
throw new QvacForgePluginError(
|
|
582
|
+
`bundleSdk failed: ${err?.message || err}`,
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const addonCount = bundleResult.addons.length;
|
|
587
|
+
logger.info(
|
|
588
|
+
`bundleSdk: ${addonCount} native addon${addonCount === 1 ? "" : "s"} in bundle`,
|
|
589
|
+
);
|
|
590
|
+
|
|
591
|
+
logger.info(`Running verifyBundle (hosts: ${hosts.join(", ")})...`);
|
|
592
|
+
let verifyResult;
|
|
593
|
+
try {
|
|
594
|
+
const verifyOpts = {
|
|
595
|
+
projectRoot: projectDir,
|
|
596
|
+
addonsSource: bundleResult.bundlePath,
|
|
597
|
+
hosts,
|
|
598
|
+
};
|
|
599
|
+
if (options.configPath) verifyOpts.configPath = options.configPath;
|
|
600
|
+
verifyResult = await verifyBundle(verifyOpts);
|
|
601
|
+
} catch (err) {
|
|
602
|
+
throw new QvacForgePluginError(
|
|
603
|
+
`verifyBundle threw: ${err?.message || err}`,
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (hasErrors(verifyResult)) {
|
|
608
|
+
throw new QvacForgePluginError(
|
|
609
|
+
`verifyBundle reported errors:\n${formatVerifyBundleResult(verifyResult)}`,
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (verifyResult.issues.length > 0) {
|
|
614
|
+
logger.warn(
|
|
615
|
+
`verifyBundle produced warnings:\n${formatVerifyBundleResult(verifyResult)}`,
|
|
616
|
+
);
|
|
617
|
+
} else {
|
|
618
|
+
logger.info(
|
|
619
|
+
`verifyBundle: ${verifyResult.addons.length} addon${verifyResult.addons.length === 1 ? "" : "s"} OK`,
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return bundleResult;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// ============================================
|
|
627
|
+
// QVAC Forge Plugin
|
|
628
|
+
// ============================================
|
|
629
|
+
|
|
630
|
+
class QvacForgePlugin extends PluginBase {
|
|
631
|
+
name = "qvac";
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* @param {object} [config]
|
|
635
|
+
* @param {string} [config.projectDir] Project root (default: process.cwd()).
|
|
636
|
+
* @param {string} [config.configPath] Path to a qvac config file (`.ts`, `.mjs`, `.js`, or `.json`). Forwarded to bundleSdk + verifyBundle. Default: auto-discover in `projectDir`.
|
|
637
|
+
* @param {string[]} [config.hosts] Hosts to verify against (default: `${process.platform}-${process.arch}`).
|
|
638
|
+
* @param {"off"|"error"|"warn"|"info"|"debug"} [config.logLevel]
|
|
639
|
+
*/
|
|
640
|
+
constructor(config = {}) {
|
|
641
|
+
super(config);
|
|
642
|
+
this.projectDir = config.projectDir || process.cwd();
|
|
643
|
+
this.configPath = config.configPath || null;
|
|
644
|
+
this.hosts = Array.isArray(config.hosts) ? config.hosts : null;
|
|
645
|
+
|
|
646
|
+
if (config.logLevel) setLogLevel(config.logLevel);
|
|
647
|
+
|
|
648
|
+
// Cache for resolveForgeConfig double-fire (package + make).
|
|
649
|
+
this._cache = null;
|
|
650
|
+
this._pruneHook = null;
|
|
651
|
+
|
|
652
|
+
logger.debug("QvacForgePlugin initialized");
|
|
653
|
+
logger.debug(`Project directory: ${this.projectDir}`);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
getHooks() {
|
|
657
|
+
return {
|
|
658
|
+
resolveForgeConfig: this.configurePackager.bind(this),
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
async configurePackager(forgeConfig) {
|
|
663
|
+
logger.info("Configuring packager for QVAC...");
|
|
664
|
+
|
|
665
|
+
if (!forgeConfig.packagerConfig) {
|
|
666
|
+
forgeConfig.packagerConfig = {};
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// 1. Block macOS universal builds early.
|
|
670
|
+
this.checkForUniversalArch(forgeConfig);
|
|
671
|
+
|
|
672
|
+
// 2. Force asar: false (Bare worker can't load from asar). Truthy check
|
|
673
|
+
// catches both `asar: true` and `asar: { unpack: ... }` object configs.
|
|
674
|
+
if (forgeConfig.packagerConfig.asar) {
|
|
675
|
+
logger.warn(
|
|
676
|
+
"asar is enabled — Bare worker may fail to load. Overriding to false.",
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
forgeConfig.packagerConfig.asar = false;
|
|
680
|
+
|
|
681
|
+
// 3. Bundle + verify (cached across resolveForgeConfig invocations).
|
|
682
|
+
// `hosts` resolution: explicit config wins, then CLI/config-derived
|
|
683
|
+
// target, then host fallback inside runBundleAndVerify. Hosts are
|
|
684
|
+
// threaded into BOTH bundleSdk (drives bare-pack content) and
|
|
685
|
+
// verifyBundle (asserts prebuild availability) — passing different
|
|
686
|
+
// sets silently produces inconsistent builds.
|
|
687
|
+
if (this._cache === null) {
|
|
688
|
+
const detected = this.hosts ? null : detectTargetHosts(forgeConfig);
|
|
689
|
+
if (detected && !this.hosts) {
|
|
690
|
+
logger.info(`Detected target hosts from CLI/config: ${detected.join(", ")}`);
|
|
691
|
+
}
|
|
692
|
+
const commands = await loadSdkCommands();
|
|
693
|
+
const bundleResult = await runBundleAndVerify(
|
|
694
|
+
commands,
|
|
695
|
+
this.projectDir,
|
|
696
|
+
{
|
|
697
|
+
configPath: this.configPath,
|
|
698
|
+
hosts: this.hosts || detected,
|
|
699
|
+
},
|
|
700
|
+
);
|
|
701
|
+
const exclusions = computeExclusions(
|
|
702
|
+
bundleResult.addons,
|
|
703
|
+
this.projectDir,
|
|
704
|
+
);
|
|
705
|
+
this._cache = { bundleResult, exclusions };
|
|
706
|
+
} else {
|
|
707
|
+
logger.debug("Reusing cached bundleSdk result.");
|
|
708
|
+
}
|
|
709
|
+
const { exclusions } = this._cache;
|
|
710
|
+
|
|
711
|
+
// 4. Merge ignore patterns to exclude unused addons + mobile prebuilds.
|
|
712
|
+
const existingIgnore = forgeConfig.packagerConfig.ignore;
|
|
713
|
+
forgeConfig.packagerConfig.ignore = createIgnore(
|
|
714
|
+
exclusions,
|
|
715
|
+
existingIgnore,
|
|
716
|
+
);
|
|
717
|
+
|
|
718
|
+
// 5. afterPrune hook for prebuild pruning + excluded addon dir cleanup.
|
|
719
|
+
// Forge calls resolveForgeConfig multiple times during `make` (once for
|
|
720
|
+
// `Loading configuration`, then again for `Preparing to package`). We
|
|
721
|
+
// must not append the hook on each pass, otherwise Packager's afterPrune
|
|
722
|
+
// fires it N times and we walk the already-pruned tree on every repeat.
|
|
723
|
+
if (this._pruneHook === null) {
|
|
724
|
+
this._pruneHook = this.createPruneHook(exclusions);
|
|
725
|
+
}
|
|
726
|
+
const existingAfterPrune = toArray(forgeConfig.packagerConfig.afterPrune);
|
|
727
|
+
if (!existingAfterPrune.includes(this._pruneHook)) {
|
|
728
|
+
forgeConfig.packagerConfig.afterPrune = [
|
|
729
|
+
...existingAfterPrune,
|
|
730
|
+
this._pruneHook,
|
|
731
|
+
];
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
logger.debug("Packager configuration complete");
|
|
735
|
+
return forgeConfig;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Blocks macOS universal builds (darwin/universal).
|
|
740
|
+
*
|
|
741
|
+
* Native addon prebuilds are arch-specific (e.g. darwin-arm64 vs
|
|
742
|
+
* darwin-x64) and are not produced as a single "universal" prebuild. A
|
|
743
|
+
* universal Electron app would need to ship both arch prebuild sets,
|
|
744
|
+
* which is incompatible with this plugin's single-arch pruning model.
|
|
745
|
+
* Forge expands universal targets before Packager hooks run, so we must
|
|
746
|
+
* detect it before pruning runs.
|
|
747
|
+
*/
|
|
748
|
+
checkForUniversalArch(forgeConfig) {
|
|
749
|
+
const universalMessage =
|
|
750
|
+
`macOS universal packaging is not supported by @qvac/sdk/electron-forge. ` +
|
|
751
|
+
`Native addon prebuilds are architecture-specific and this plugin currently only supports single-arch packaging. ` +
|
|
752
|
+
`Build separate darwin-arm64 and darwin-x64 packages instead.`;
|
|
753
|
+
|
|
754
|
+
if (forgeConfig?.packagerConfig?.arch === "universal") {
|
|
755
|
+
throw new QvacForgePluginError(universalMessage);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const args = process.argv.slice(2);
|
|
759
|
+
let arch = null;
|
|
760
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
761
|
+
const arg = args[i];
|
|
762
|
+
|
|
763
|
+
if (arg === "--arch" || arg === "-a") {
|
|
764
|
+
arch = args[i + 1] || null;
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (typeof arg === "string" && arg.startsWith("--arch=")) {
|
|
769
|
+
arch = arg.slice("--arch=".length) || null;
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if (arch === "universal") {
|
|
775
|
+
throw new QvacForgePluginError(universalMessage);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
createPruneHook(exclusions = []) {
|
|
780
|
+
return (buildPath, electronVersion, platform, arch, done) => {
|
|
781
|
+
logger.info(`Pruning prebuilds for ${platform}-${arch}...`);
|
|
782
|
+
|
|
783
|
+
try {
|
|
784
|
+
const prebuildResult = prunePrebuildsForPath(buildPath, platform, arch);
|
|
785
|
+
const prebuildMb = (prebuildResult.bytes / 1024 / 1024).toFixed(1);
|
|
786
|
+
logger.info(
|
|
787
|
+
`Pruned ${prebuildResult.deleted} prebuild dirs (~${prebuildMb} MB reclaimed)`,
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
if (exclusions.length > 0) {
|
|
791
|
+
const cleanup = removeExcludedAddonDirs(buildPath, exclusions);
|
|
792
|
+
const cleanupMb = (cleanup.bytes / 1024 / 1024).toFixed(1);
|
|
793
|
+
logger.info(
|
|
794
|
+
`Removed ${cleanup.removed} excluded addon dir${cleanup.removed === 1 ? "" : "s"} (~${cleanupMb} MB reclaimed)`,
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
done();
|
|
799
|
+
} catch (err) {
|
|
800
|
+
logger.error(`Prebuild pruning failed: ${err.message}`);
|
|
801
|
+
done(err);
|
|
802
|
+
}
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// ============================================
|
|
808
|
+
// Exports
|
|
809
|
+
// ============================================
|
|
810
|
+
|
|
811
|
+
module.exports = QvacForgePlugin;
|
|
812
|
+
module.exports.setLogLevel = setLogLevel;
|
|
813
|
+
module.exports.QvacForgePluginError = QvacForgePluginError;
|
|
814
|
+
|
|
815
|
+
// Internal helpers exposed for unit tests. Not part of the stable API.
|
|
816
|
+
module.exports.createIgnore = createIgnore;
|
|
817
|
+
module.exports.diffAddons = diffAddons;
|
|
818
|
+
module.exports.detectTargetHosts = detectTargetHosts;
|
|
819
|
+
module.exports.resolveHosts = resolveHosts;
|
|
820
|
+
module.exports.runBundleAndVerify = runBundleAndVerify;
|