Skip to content

QVAC-13429 feat[api]: add electron-forge plugin for native addon tree-shaking#2480

Open
opaninakuffo wants to merge 1 commit into
tetherto:mainfrom
opaninakuffo:feat/electron-forge-plugin
Open

QVAC-13429 feat[api]: add electron-forge plugin for native addon tree-shaking#2480
opaninakuffo wants to merge 1 commit into
tetherto:mainfrom
opaninakuffo:feat/electron-forge-plugin

Conversation

@opaninakuffo

Copy link
Copy Markdown
Contributor

🎯 What problem does this PR solve?

  • Packaging a QVAC-powered Electron app required a manual two-step flow (qvac bundle sdk then qvac verify) before every electron-forge package, with no way to keep the addon manifest, packaged tree, and prebuilds in sync.
  • Default Electron Packager output ships every @qvac/* addon and every per-arch prebuild (~700 MB of dead weight on a darwin-arm64 build).
  • arch: "universal" silently produced broken packages because prebuilds are arch-specific.
  • asar: true (Forge default) breaks Bare worker loading.

📝 How does it solve it?

  • Adds @qvac/sdk/electron-forge — a build-time Electron Forge plugin (CJS) consumed from forge.config.cjs.
  • Hooks resolveForgeConfig: invokes bundleSdk + verifyBundle programmatically (no external CLI), composes packagerConfig.ignore to strip unused @qvac/* addons + mobile prebuilds, forces asar: false, hard-blocks arch: "universal" with a trimmed-stack error.
  • Hooks afterPrune: prunes prebuilds for non-target ${platform}-${arch} recursively (including nested node_modules, so pnpm/non-hoisted layouts work), and removes empty addon dir shells left by Packager's ignore filter.
  • Detects target hosts from --platform / --arch (CLI + packagerConfig), so cross-platform invocations (e.g. building win32 from a darwin host) verify against the right prebuild set instead of the host's.
  • Caches bundleSdk / verifyBundle results and the afterPrune hook across Forge's double-fired resolveForgeConfig (Loading configuration + Preparing to package).

🧪 How was it tested?

  • Unit (15 brittle tests, packages/sdk/test/unit/electron-forge.test.ts): diffAddons, createIgnore (function + array form, /out/ parity, mobile prebuild filters), detectTargetHosts (argv parsing, =value and space form, multi-arch expansion, packagerConfig override), QvacForgePluginError trimmed stack.
  • E2E (sample Electron app):
    • electron-forge package from darwin-arm64: ~520 MB / 134 prebuild dirs pruned, 8 unused addons excluded, app launches and runs llamacpp completion plus a custom qvac-echo-plugin at runtime.
    • electron-forge make (zip + dmg): dmg installs to /Applications and runs end-to-end. Confirmed resolveForgeConfig caching + afterPrune idempotency (no double-prune during make).
    • electron-forge package --platform=darwin --arch=x64 from arm64 host: verifyBundle targets darwin-x64, prune correctly drops darwin-arm64.
    • electron-forge package --arch=universal: hard-blocks with QvacForgePluginError.
  • bun run lint (eslint + tsc) clean.

🔌 API Changes

// forge.config.cjs
const QvacForgePlugin = require("@qvac/sdk/electron-forge");

module.exports = {
  packagerConfig: { name: "MyApp" },
  plugins: [
    new QvacForgePlugin({
      // all options optional:
      // configPath: "./qvac.config.json",        // auto-discovered if omitted
      // hosts: ["darwin-arm64", "darwin-x64"],   // derived from --platform/--arch if omitted
      // logLevel: "info",                        // off | error | warn | info | debug
    }),
  ],
};

Adds one new public export — @qvac/sdk/electron-forge — and @electron-forge/plugin-base as an optional peer dependency.

Adds @qvac/sdk/electron-forge: an Electron Forge plugin that bundles
the QVAC worker, verifies its native addons, then configures Electron
Packager to tree-shake unused @qvac/* addons and non-target prebuilds.
Replaces the prior two-step `qvac bundle sdk` + `qvac verify` flow with
a single `electron-forge package` invocation by calling bundleSdk and
verifyBundle directly via dynamic import of @qvac/sdk/commands.

Plugin behavior:
- Hard-blocks macOS universal builds (prebuilds are arch-specific).
- Forces asar: false (Bare worker can't load from asar).
- Detects target hosts from --platform / --arch (CLI + packagerConfig).
- Excludes unused @qvac/* addons via packagerConfig.ignore and cleans
  up empty addon dir shells in afterPrune.
- Prunes prebuilds for non-target platform/arch, including nested
  node_modules (covers pnpm/non-hoisted layouts).

Adds @electron-forge/plugin-base as devDep + optional peerDep, exports
./electron-forge from the SDK package, ignores *.tgz pack artifacts,
and ships 15 unit tests covering diffAddons, createIgnore (function +
array form), detectTargetHosts (argv parsing + multi-arch + override
semantics), and the trimmed-stack QvacForgePluginError.
@opaninakuffo opaninakuffo requested review from a team as code owners June 8, 2026 10:18
@opaninakuffo opaninakuffo changed the title feat[api|notask]: add electron-forge plugin for native addon tree-shaking QVAC-13429 feat[api]: add electron-forge plugin for native addon tree-shaking Jun 8, 2026

@simon-iribarren simon-iribarren left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  1. bundleSdk does not receive the detected target hosts.

The plugin detects target hosts from Forge config / CLI args and passes them into verifyBundle, but runBundleAndVerify calls bundleSdk before that without hosts:

const bundleOpts = { projectRoot: projectDir };
if (options.configPath) bundleOpts.configPath = options.configPath;
bundleResult = await bundleSdk(bundleOpts);

bundleSdk then falls back to its own default host list, while verifyBundle checks the detected target hosts. That makes targeted Forge invocations inconsistent: for example, electron-forge package --platform=darwin --arch=x64 verifies darwin-x64, but the Bare bundle was still produced with the SDK default host set. For targets outside that default list, verification can check a host the bundle was never built for; even for normal desktop targets, this weakens the target-specific packaging behavior this plugin is meant to provide.

Please compute the resolved hosts once and pass the same list into both bundleSdk({ ..., hosts }) and verifyBundle({ ..., hosts }). It would also be worth adding a wiring test around runBundleAndVerify / configurePackager, since the current tests cover detectTargetHosts in isolation but not the command boundary where this mismatch occurs.

  1. The new public API needs durable consumer docs.

This adds the public @qvac/sdk/electron-forge export and the title/body tagging looks correct for feat[api], including a usage snippet in the PR body. But the diff does not include SDK README or docs-site coverage for install requirements, forge.config.cjs usage, the forced asar: false behavior, unsupported universal builds, or how target host detection works.

Please add consumer-facing docs in this PR, or link the follow-up docs PR in the description before merge.

CI Status

The visible PR checks are green: SDK Pod Checks, validate-pr, label gate, and resolve-config pass. Release/publish jobs are skipped as expected for a PR, and the run-tests job is skipped.

API Surface & Tagging

This PR adds public API surface via @qvac/sdk/electron-forge. The [api] title tag and usage snippet are present. The remaining gap is the missing durable consumer docs noted above.

Local Verification

I ran the focused unit test file:

bun run test/unit/electron-forge.test.ts

Result: 15/15 tests passed, 32/32 assertions passed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants