# electron-builder config — LearnHarness desktop (macOS arm64 first). # # Shape (fact-checked 2026-09-17): # - the app's real server (.next/standalone, curated by # scripts/build-server.mjs) ships via extraResources — OUTSIDE the asar, # because native .node/dylib can't dlopen from a read-only archive and # server.js process.chdir()s into its own dir; # - the asar holds only the shell (electron/main.cjs + package.json); # - signing/notarization config is laid now but inert until a Developer # ID certificate exists (identity: null = unsigned local build, which # runs fine for personal use — Gatekeeper only gates quarantined apps). appId: com.learnharness.LearnHarness productName: LearnHarness directories: output: dist files: # Only the shell goes into the asar. ORDER IS LOAD-BEARING (payload # wave, 2026-09-24): electron-builder's dependency collector packs the # whole production dep tree into node_modules/ REGARDLESS of this # allow-list — normalizeFiles turns this array into one {filter: [...]} # fileSet, getNodeModuleFileMatcher feeds EVERY filter entry (positives # included) to the per-dep copier, and without a negation its implicit # '**/*' passes everything (the measured result: a 502MB asar holding # next/@ai-sdk/pdfjs/mermaid/puppeteer… while the launcher requires # nothing but electron + node builtins — the server ships via # extraResources). # # The fix is gitignore-style ordering (verified against the installed # app-builder-lib 26.15.3 + a DEBUG-mode rebuild): '!**/*' first excludes # EVERYTHING, the two positives after it re-include only the shell. # The per-dep collector then contributes ZERO files — app-builder-lib's # filter evaluates each dep file's DESTINATION path (the # stat.moduleFullFilePath quirk in util/filter.js getRelativePath), so # '!**/*' matches every 'node_modules//...' path and the bare # positives can never re-include one (DEBUG logs confirm filesCount=0 # for all 566 modules). If a future electron-builder fixes that quirk, # ~1MB of package.json stubs would start landing — harmless. The # two-package.json layout avoids the collector entirely but # restructures the repo. app.asar.unpacked dies with it: the natives # were only unpacked because their packages were packed. - "!**/*" - electron/**/* - package.json extraResources: # Copy from the PARENT dir (.next), not .next/standalone itself: # electron-builder hard-excludes a copy's root node_modules and the # default no-filter pattern is a literal path match (empirically both # node_modules/ and .next/ were silently dropped → server died on # "Cannot find module 'next'"). With `from: .next` the filter's # relative paths start with standalone/, dodging the root-node_modules # rule, and the explicit wildcard (minimatch dot:true) reaches # standalone/.next and standalone/node_modules. - from: .next to: server filter: - "standalone/**/*" mac: target: - target: dmg arch: - arm64 # Locale trim (payload wave, 2026-09-24): English-only app — cuts # every non-en .lproj from the frameworks (the STT/mic entitlements # don't depend on locales). Revisit only if localization ever lands. electronLanguages: - en category: public.app-category.education # Laid for the STT milestone (researcher-verified): the TCC mic prompt # reads this string; macOS asks regardless of Electron's own handlers. extendInfo: NSMicrophoneUsageDescription: "LearnHarness uses the microphone only for speech-to-text input into your sessions." hardenedRuntime: true entitlements: build/entitlements.mac.plist # Unsigned until a Developer ID certificate exists (research-confirmed: # local builds run fine; distribution needs signing + notarization). identity: null dmg: title: "${productName} ${version}"