From effdcd704bff8e87b01e31cbed33bf38c1943ec9 Mon Sep 17 00:00:00 2001 From: atharva9167j Date: Sun, 6 Sep 2026 16:18:47 +0530 Subject: [PATCH 1/5] perf(build): reduce package size and optimize V8 heap memory footprint - Exclude dead-weight `node_modules` from `app.asar` packaging: all frontend and electron main process modules are already bundled into `dist/` and `dist-electron/` by Vite, saving ~238 MB of redundant files. - Deduplicate assets in `electron-builder.json5`: exclude `wallpapers/`, `cursors/`, and `mediapipe/` from `app.asar` as they are already provisioned via `extraResources` for raw filesystem access by the native compositor, saving ~17 MB. - Exclude documentation assets (`demo.gif`, `preview*.png`) from `app.asar` packaging (~8 MB saved). - Exclude unused `ffmpeg-shared.exe` from Windows `extraResources`. - Restrict `electronLanguages` to the 13 supported locales, removing 42 unneeded Chromium locale `.pak` files (~20 MB saved). - Set `compression: "maximum"` in `electron-builder.json5` to enable solid LZMA2 compression for Windows installers and release archives. - Add `--optimize-for-size` V8 engine flag in `electron/main.ts` to reduce heap footprint across renderer and background processes. --- electron-builder.json5 | 30 ++++++++++++++++++++++++++++-- electron/main.ts | 3 +++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/electron-builder.json5 b/electron-builder.json5 index dafe8c690..e874060ea 100644 --- a/electron-builder.json5 +++ b/electron-builder.json5 @@ -58,7 +58,23 @@ // needs a system libvips we don't provide and breaks on CI/local ("vips-cpp.42 not found"), so we // let electron-builder use the prebuilt instead of recompiling. "buildDependenciesFromSource": false, - "compression": "normal", + "compression": "maximum", + // Strip unused Chromium locale pak files (saves ~20 MB). Only package locales supported by OpenScreen. + "electronLanguages": [ + "en-US", + "fr", + "ar", + "es", + "it", + "ja", + "ko", + "pt-BR", + "ru", + "tr", + "vi", + "zh-CN", + "zh-TW" + ], "directories": { "output": "release/${version}", // Electron-builder's default, spelled out because the appx target depends on it: @@ -72,6 +88,16 @@ "files": [ "dist", "dist-electron", + // Exclude documentation assets that Vite copied from public/ (saves ~8 MB) + "!dist/demo.gif", + "!dist/preview*.png", + // Exclude assets already bundled into extraResources (saves ~17 MB duplicate in asar) + "!dist/wallpapers/**", + "!dist/cursors/**", + "!dist/mediapipe/**", + // Exclude dead-weight node_modules: all frontend and electron code is pre-bundled by Vite (saves ~238 MB) + "!node_modules/**", + "!**/node_modules/**", "!*.png", "!preview*.png", "!*.md", @@ -369,7 +395,7 @@ // in the app spawns. Shipping it added a large binary to every Windows // installer, plus an LGPL redistribution obligation, for a file no // shipped code opens. - "filter": ["win32-*/*", "!win32-*/ffmpeg.exe"] + "filter": ["win32-*/*", "!win32-*/ffmpeg.exe", "!win32-*/ffmpeg-shared.exe"] } ], // The D3D11 compositor addon deliberately does NOT travel through `files` into diff --git a/electron/main.ts b/electron/main.ts index ec961aead..f9840840c 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -76,6 +76,9 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); // HUD/tray/menu. Parsed before any GUI side effects; see electron/cli/. const cliCommand = parseCliArgs(process.argv, app.isPackaged ? 1 : 2); +// Optimize V8 heap allocation to reduce memory footprint across processes. +app.commandLine.appendSwitch("js-flags", "--optimize-for-size"); + // Use Screen & System Audio Recording permissions instead of the CoreAudio Tap API on macOS. // Tap needs NSAudioCaptureUsageDescription in the parent app's Info.plist, which breaks when // running from a terminal/IDE during dev. From c098a1e7558bde18fb7e9524af2c716695200e32 Mon Sep 17 00:00:00 2001 From: atharva9167j Date: Mon, 7 Sep 2026 00:31:42 +0530 Subject: [PATCH 2/5] fix: update mediapipe notice path and clarify renderer v8 flag scope --- THIRD-PARTY-NOTICES.md | 2 +- electron/main.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/THIRD-PARTY-NOTICES.md b/THIRD-PARTY-NOTICES.md index 6bfe88fb4..d53cfc019 100644 --- a/THIRD-PARTY-NOTICES.md +++ b/THIRD-PARTY-NOTICES.md @@ -71,7 +71,7 @@ distributed by their own registries, not redistributed inside our binaries. - **Components**: `selfie_segmentation.tflite`, `selfie_segmentation_landscape.tflite` and the `selfie_segmentation_landscape.onnx` - derived from them, shipped inside `app.asar` under `dist/mediapipe/`. + derived from them, shipped under `resources/mediapipe/`. - **License**: Apache-2.0 — . Copyright The MediaPipe Authors. - The `.onnx` is a **derived work**, generated from the vendored `.tflite` by diff --git a/electron/main.ts b/electron/main.ts index f9840840c..88703f7bd 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -76,7 +76,9 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); // HUD/tray/menu. Parsed before any GUI side effects; see electron/cli/. const cliCommand = parseCliArgs(process.argv, app.isPackaged ? 1 : 2); -// Optimize V8 heap allocation to reduce memory footprint across processes. +// Optimize V8 heap allocation to reduce memory footprint across renderer processes. +// Note: app.commandLine.appendSwitch('js-flags') does not affect the main process V8 isolate, +// which is already initialized before main.ts executes. app.commandLine.appendSwitch("js-flags", "--optimize-for-size"); // Use Screen & System Audio Recording permissions instead of the CoreAudio Tap API on macOS. From c9b7aa202ff5a67c560a060a9deb6b88587f138f Mon Sep 17 00:00:00 2001 From: atharva9167j Date: Sun, 13 Sep 2026 01:06:41 +0530 Subject: [PATCH 3/5] fix(packaging): restore Windows ffmpeg-shared.exe and address review findings - Unblock Windows installer by retaining ffmpeg-shared.exe in extraResources filter, required by audioPeaks, extractAudio, and extensionClip. - Add cross-platform test in audioPeaks.test.ts asserting that the Windows installer filter packages ffmpeg-shared.exe and excludes static ffmpeg.exe. - Retain macOS regional locales (pt_BR, zh_CN, zh_TW) in electronLanguages alongside Windows pak variants. - Extend scripts/i18n-check.mjs to enforce synchronization between SUPPORTED_LOCALES, appx.languages, and electronLanguages. - Revert electron-builder compression back to normal. - Move demo.gif and preview*.png from public/ to docs/assets/, update README.md and metainfo.xml, and drop redundant files exclusions. - Remove dead asarUnpack and redundant node_modules pattern from electron-builder.json5. - Remove unmeasured optimize-for-size js-flags from electron/main.ts. - Update technical-documentation/engineering/build-and-packaging.md with ASAR optimization, asset layout, and packaging footprint reference. --- README.md | 2 +- .../com.getopenscreen.OpenScreen.metainfo.xml | 4 +- {public => docs/assets}/demo.gif | Bin {public => docs/assets}/preview3.png | Bin {public => docs/assets}/preview4.png | Bin electron-builder.json5 | 22 ++-- electron/main.ts | 5 - electron/media/audioPeaks.test.ts | 71 +++++++++++- scripts/i18n-check.mjs | 108 +++++++++++++++++- scripts/macos-floor.mjs | 4 +- .../engineering/build-and-packaging.md | 48 ++++++++ 11 files changed, 238 insertions(+), 26 deletions(-) rename {public => docs/assets}/demo.gif (100%) rename {public => docs/assets}/preview3.png (100%) rename {public => docs/assets}/preview4.png (100%) diff --git a/README.md b/README.md index 0b4fe33d0..508393905 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@

A free, open-source desktop app for recording your screen and turning the result into polished product demos and walkthroughs.

- Editing a recording in OpenScreen: wallpaper and video effects, an AI-assisted cut driven from the chat, then export + Editing a recording in OpenScreen: wallpaper and video effects, an AI-assisted cut driven from the chat, then export

diff --git a/build/com.getopenscreen.OpenScreen.metainfo.xml b/build/com.getopenscreen.OpenScreen.metainfo.xml index 4281aafa4..dec91da2b 100644 --- a/build/com.getopenscreen.OpenScreen.metainfo.xml +++ b/build/com.getopenscreen.OpenScreen.metainfo.xml @@ -87,11 +87,11 @@ --> - https://raw.githubusercontent.com/getopenscreen/openscreen/main/public/preview4.png + https://raw.githubusercontent.com/getopenscreen/openscreen/main/docs/assets/preview4.png Editing a recording: zoom regions, video effects and export settings - https://raw.githubusercontent.com/getopenscreen/openscreen/main/public/preview3.png + https://raw.githubusercontent.com/getopenscreen/openscreen/main/docs/assets/preview3.png The timeline, with trim, speed and zoom regions on separate tracks diff --git a/public/demo.gif b/docs/assets/demo.gif similarity index 100% rename from public/demo.gif rename to docs/assets/demo.gif diff --git a/public/preview3.png b/docs/assets/preview3.png similarity index 100% rename from public/preview3.png rename to docs/assets/preview3.png diff --git a/public/preview4.png b/docs/assets/preview4.png similarity index 100% rename from public/preview4.png rename to docs/assets/preview4.png diff --git a/electron-builder.json5 b/electron-builder.json5 index e874060ea..9127f0a9a 100644 --- a/electron-builder.json5 +++ b/electron-builder.json5 @@ -3,12 +3,6 @@ "$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json", "appId": "com.etiennelescot.openscreen", "asar": true, - // .node binaries can't be dlopen'd from inside an asar — must live unpacked. - // onnxruntime-node distributes as a `.node` shared object that whisper.cpp - // helpers run from process spawn, so they all unpack via the same glob. - "asarUnpack": [ - "**/*.node" - ], "productName": "Openscreen", // There is deliberately no `electronVersion` here. electron-builder needs an EXACT // version because it downloads the binaries for one release, and it takes that from @@ -58,8 +52,11 @@ // needs a system libvips we don't provide and breaks on CI/local ("vips-cpp.42 not found"), so we // let electron-builder use the prebuilt instead of recompiling. "buildDependenciesFromSource": false, - "compression": "maximum", + "compression": "normal", // Strip unused Chromium locale pak files (saves ~20 MB). Only package locales supported by OpenScreen. + // Note: macOS uses underscore variants (e.g. pt_BR.lproj, zh_CN.lproj, zh_TW.lproj), while Windows + // and Linux use hyphenated pak files (e.g. pt-BR.pak, zh-CN.pak, zh-TW.pak). Both forms are listed + // so app-builder-lib retains the correct locale assets across all platforms. "electronLanguages": [ "en-US", "fr", @@ -69,11 +66,14 @@ "ja", "ko", "pt-BR", + "pt_BR", "ru", "tr", "vi", "zh-CN", - "zh-TW" + "zh_CN", + "zh-TW", + "zh_TW" ], "directories": { "output": "release/${version}", @@ -88,16 +88,12 @@ "files": [ "dist", "dist-electron", - // Exclude documentation assets that Vite copied from public/ (saves ~8 MB) - "!dist/demo.gif", - "!dist/preview*.png", // Exclude assets already bundled into extraResources (saves ~17 MB duplicate in asar) "!dist/wallpapers/**", "!dist/cursors/**", "!dist/mediapipe/**", // Exclude dead-weight node_modules: all frontend and electron code is pre-bundled by Vite (saves ~238 MB) "!node_modules/**", - "!**/node_modules/**", "!*.png", "!preview*.png", "!*.md", @@ -395,7 +391,7 @@ // in the app spawns. Shipping it added a large binary to every Windows // installer, plus an LGPL redistribution obligation, for a file no // shipped code opens. - "filter": ["win32-*/*", "!win32-*/ffmpeg.exe", "!win32-*/ffmpeg-shared.exe"] + "filter": ["win32-*/*", "!win32-*/ffmpeg.exe"] } ], // The D3D11 compositor addon deliberately does NOT travel through `files` into diff --git a/electron/main.ts b/electron/main.ts index 88703f7bd..ec961aead 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -76,11 +76,6 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); // HUD/tray/menu. Parsed before any GUI side effects; see electron/cli/. const cliCommand = parseCliArgs(process.argv, app.isPackaged ? 1 : 2); -// Optimize V8 heap allocation to reduce memory footprint across renderer processes. -// Note: app.commandLine.appendSwitch('js-flags') does not affect the main process V8 isolate, -// which is already initialized before main.ts executes. -app.commandLine.appendSwitch("js-flags", "--optimize-for-size"); - // Use Screen & System Audio Recording permissions instead of the CoreAudio Tap API on macOS. // Tap needs NSAudioCaptureUsageDescription in the parent app's Info.plist, which breaks when // running from a terminal/IDE during dev. diff --git a/electron/media/audioPeaks.test.ts b/electron/media/audioPeaks.test.ts index 2a455bfc3..485fbb034 100644 --- a/electron/media/audioPeaks.test.ts +++ b/electron/media/audioPeaks.test.ts @@ -1,5 +1,5 @@ // @vitest-environment node -import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; @@ -7,6 +7,48 @@ import { ffmpegCandidates, peakBlockCount, resolveFfmpeg } from "./audioPeaks"; const ROOT = path.resolve(__dirname, "..", ".."); +function stripJson5Comments(source: string): string { + let out = ""; + let inString = false; + for (let i = 0; i < source.length; i++) { + const ch = source[i]; + if (inString) { + out += ch; + if (ch === "\\") { + out += source[++i] ?? ""; + } else if (ch === '"') { + inString = false; + } + continue; + } + if (ch === '"') { + inString = true; + out += ch; + continue; + } + if (ch === "/" && source[i + 1] === "/") { + while (i < source.length && source[i] !== "\n") i++; + out += "\n"; + continue; + } + out += ch; + } + return out; +} + +function objectBody(source: string, key: string): string | null { + const opener = new RegExp(`"${key}"\\s*:\\s*{`).exec(source); + if (!opener) return null; + let depth = 0; + for (let i = opener.index + opener[0].length - 1; i < source.length; i++) { + if (source[i] === "{") depth++; + else if (source[i] === "}" && --depth === 0) { + return source.slice(opener.index + opener[0].length, i); + } + } + return null; +} + describe("peakBlockCount", () => { it("matches the browser pipelines' block maths", () => { // Same formula as audioPeaksWorker.ts / streamingAudioPeaks.ts: a clip must @@ -41,6 +83,33 @@ describe("ffmpeg resolution", () => { } }); + it("ensures the Windows installer filter packages ffmpeg-shared.exe", () => { + const configSource = readFileSync(path.join(ROOT, "electron-builder.json5"), "utf8"); + const stripped = stripJson5Comments(configSource); + const winBlock = objectBody(stripped, "win"); + expect(winBlock, "electron-builder.json5 must declare a win block").toBeTruthy(); + const winFilterMatch = winBlock!.match(/"filter"\s*:\s*\[([^\]]+)\]/); + expect(winFilterMatch, "win.extraResources must declare a filter").toBeTruthy(); + const filters = winFilterMatch![1] + .split(",") + .map((f: string) => f.trim().replace(/^["']|["']$/g, "")); + // Static ffmpeg.exe must be excluded + expect(filters).toContain("!win32-*/ffmpeg.exe"); + // ffmpeg-shared.exe must NOT be excluded + expect(filters).not.toContain("!win32-*/ffmpeg-shared.exe"); + // Verify win32 candidate survives the filter rules + const relativePath = "win32-x64/ffmpeg-shared.exe"; + const isIncluded = filters.some( + (f: string) => + !f.startsWith("!") && new RegExp(`^${f.replace(/\*/g, ".*")}$`).test(relativePath), + ); + const isExcluded = filters.some( + (f: string) => + f.startsWith("!") && new RegExp(`^${f.slice(1).replace(/\*/g, ".*")}$`).test(relativePath), + ); + expect(isIncluded && !isExcluded).toBe(true); + }); + it("honours the env override first", () => { process.env.OPENSCREEN_FFMPEG_PATH = "/custom/ffmpeg"; try { diff --git a/scripts/i18n-check.mjs b/scripts/i18n-check.mjs index b37af68c9..0dd765e8d 100644 --- a/scripts/i18n-check.mjs +++ b/scripts/i18n-check.mjs @@ -7,6 +7,7 @@ */ import fs from "node:fs"; import path from "node:path"; +import { objectBody, stripJson5Comments } from "./macos-floor.mjs"; const LOCALES_DIR = path.resolve("src/i18n/locales"); const BASE_LOCALE = "en"; @@ -77,11 +78,114 @@ for (const namespace of namespaces) { } } +// Validate that SUPPORTED_LOCALES (src/i18n/config.ts), appx.languages, and +// electronLanguages (electron-builder.json5) all agree on the supported locales. +function readSupportedLocales() { + const configContent = fs.readFileSync(path.resolve("src/i18n/config.ts"), "utf-8"); + const match = configContent.match(/SUPPORTED_LOCALES\s*=\s*\[([\s\S]*?)\]\s*as\s*const/); + if (!match) throw new Error("Could not find SUPPORTED_LOCALES in src/i18n/config.ts"); + return match[1] + .split(",") + .map((s) => s.trim().replace(/^["']|["']$/g, "")) + .filter(Boolean); +} + +function readBuilderLanguages() { + const content = fs.readFileSync(path.resolve("electron-builder.json5"), "utf-8"); + const stripped = stripJson5Comments(content); + + const electronLanguagesMatch = stripped.match(/"electronLanguages"\s*:\s*\[([\s\S]*?)\]/); + if (!electronLanguagesMatch) + throw new Error("Could not find electronLanguages in electron-builder.json5"); + const electronLanguages = electronLanguagesMatch[1] + .split(",") + .map((s) => s.trim().replace(/^["']|["']$/g, "")) + .filter(Boolean); + + const appxBlock = objectBody(stripped, "appx"); + if (!appxBlock) throw new Error("Could not find appx block in electron-builder.json5"); + const appxLanguagesMatch = appxBlock.match(/"languages"\s*:\s*\[([\s\S]*?)\]/); + if (!appxLanguagesMatch) + throw new Error("Could not find appx.languages in electron-builder.json5"); + const appxLanguages = appxLanguagesMatch[1] + .split(",") + .map((s) => s.trim().replace(/^["']|["']$/g, "")) + .filter(Boolean); + + return { electronLanguages, appxLanguages }; +} + +const supportedLocales = readSupportedLocales(); +const { electronLanguages, appxLanguages } = readBuilderLanguages(); + +// 1. Check all disk locales in src/i18n/locales match SUPPORTED_LOCALES +const diskLocales = [BASE_LOCALE, ...compareLocales].sort(); +const sortedSupported = [...supportedLocales].sort(); + +const missingOnDisk = sortedSupported.filter((l) => !diskLocales.includes(l)); +const extraOnDisk = diskLocales.filter((l) => !sortedSupported.includes(l)); + +if (missingOnDisk.length > 0) { + console.error( + `MISSING on disk (declared in SUPPORTED_LOCALES but no folder in src/i18n/locales): ${missingOnDisk.join(", ")}`, + ); + hasErrors = true; +} +if (extraOnDisk.length > 0) { + console.error( + `EXTRA on disk (folder in src/i18n/locales but missing in SUPPORTED_LOCALES): ${extraOnDisk.join(", ")}`, + ); + hasErrors = true; +} + +// 2. Check appx.languages matches SUPPORTED_LOCALES +// AppX uses BCP-47 / Windows store language tags: en-US for en, fr-FR for fr, and bare/region tags for the rest. +for (const locale of supportedLocales) { + const expectedAppx = locale === "en" ? "en-US" : locale === "fr" ? "fr-FR" : locale; + if (!appxLanguages.includes(expectedAppx)) { + console.error( + `MISSING in electron-builder.json5 appx.languages: "${expectedAppx}" (for ${locale})`, + ); + hasErrors = true; + } +} + +// 3. Check electronLanguages matches SUPPORTED_LOCALES +// Electron/Chromium pak files: +// en -> en-US +// ja-JP -> ja (Chromium ja.pak / macOS ja.lproj) +// ko-KR -> ko (Chromium ko.pak / macOS ko.lproj) +// pt-BR -> requires both "pt-BR" (Windows/Linux) and "pt_BR" (macOS) +// zh-CN -> requires both "zh-CN" and "zh_CN" +// zh-TW -> requires both "zh-TW" and "zh_TW" +// ar, es, it, ru, tr, vi, fr -> matches bare tag +for (const locale of supportedLocales) { + let expectedElectron = []; + if (locale === "en") expectedElectron = ["en-US"]; + else if (locale === "ja-JP") expectedElectron = ["ja"]; + else if (locale === "ko-KR") expectedElectron = ["ko"]; + else if (locale === "pt-BR") expectedElectron = ["pt-BR", "pt_BR"]; + else if (locale === "zh-CN") expectedElectron = ["zh-CN", "zh_CN"]; + else if (locale === "zh-TW") expectedElectron = ["zh-TW", "zh_TW"]; + else expectedElectron = [locale]; + + for (const expected of expectedElectron) { + if (!electronLanguages.includes(expected)) { + console.error( + `MISSING in electron-builder.json5 electronLanguages: "${expected}" (for ${locale})`, + ); + hasErrors = true; + } + } +} + if (hasErrors) { - console.error("\ni18n check FAILED — translation files are out of sync."); + console.error( + "\ni18n check FAILED — translation files or packaging locale lists are out of sync.", + ); process.exit(1); } else { console.log( - `i18n check PASSED — all ${compareLocales.length} locales match ${BASE_LOCALE} across ${namespaces.length} namespaces.`, + `i18n check PASSED — all ${compareLocales.length} locales match ${BASE_LOCALE} across ${namespaces.length} namespaces, and all ${supportedLocales.length} SUPPORTED_LOCALES align with appx.languages and electronLanguages.`, ); } diff --git a/scripts/macos-floor.mjs b/scripts/macos-floor.mjs index 347685bc6..4edcb6e0a 100644 --- a/scripts/macos-floor.mjs +++ b/scripts/macos-floor.mjs @@ -17,7 +17,7 @@ * String-aware rather than a plain `s.replace(/\/\/.*$/gm, "")` because the config also * carries URLs, whose `//` a naive strip would eat. */ -function stripJson5Comments(source) { +export function stripJson5Comments(source) { let out = ""; let inString = false; for (let i = 0; i < source.length; i++) { @@ -47,7 +47,7 @@ function stripJson5Comments(source) { } /** The body of a top-level `"": { ... }` object, brace-matched. */ -function objectBody(source, key) { +export function objectBody(source, key) { const opener = new RegExp(`"${key}"\\s*:\\s*{`).exec(source); if (!opener) { return null; diff --git a/technical-documentation/engineering/build-and-packaging.md b/technical-documentation/engineering/build-and-packaging.md index 65a437712..81007212e 100644 --- a/technical-documentation/engineering/build-and-packaging.md +++ b/technical-documentation/engineering/build-and-packaging.md @@ -223,6 +223,54 @@ The check compares modification times, so `git checkout` (which restamps source Diagnosing a suspected stale addon: serde embeds its field-name literals in the compiled binary, so `grep -c compositor_view.node` returning 0 means the binary predates that contract. +## ASAR layout and packaging optimization + +`electron-builder.json5` configures package contents, file exclusions, and resource distribution. Several deliberate optimizations reduce installer payload size and eliminate redundant file copies across platforms. + +### Node modules excluded from ASAR + +All renderer application code and Electron main/preload entry points are pre-bundled by Vite into `dist/` and `dist-electron/`. The bundled main process relies solely on Node built-ins and `electron`. As a result, `node_modules/**` is excluded from `files` in `electron-builder.json5`, eliminating ~238 MB of redundant dependencies from `app.asar`. + +### Asset deduplication and extraResources + +Assets requiring direct filesystem access are distributed via `extraResources` rather than bundled inside `app.asar`: + +- Dynamic scene assets (`wallpapers/`, `cursors/`) resolve via `ASSET_BASE_DIR` (`process.resourcesPath`) in the renderer and `sceneAssetBaseDirs()` in the main process. +- Background segmentation models (`dist/mediapipe/`) are placed in `resources/mediapipe/` for access by the segmentation worker. + +These directories are excluded from `files` (`!dist/wallpapers/**`, `!dist/cursors/**`, `!dist/mediapipe/**`), avoiding ~17 MB of duplicate files inside the ASAR archive. + +### Native addons ship outside ASAR + +Native `.node` addons cannot be loaded directly from an ASAR archive. The native compositor addon (`compositor_view.node`) and ONNX Runtime libraries are colocated with their linked FFmpeg/shared libraries under `electron/native/bin/-/` and distributed via `extraResources`. Because no `.node` binary travels through `files`, `asarUnpack: ["**/*.node"]` is unnecessary. + +### Windows FFmpeg runtime binaries + +On Windows, `scripts/fetch-ffmpeg.mjs` stages two artifacts: the shared `av*.dll` set required by the D3D11 compositor addon, and `ffmpeg-shared.exe` (~1 MB), which links against those same DLLs. + +`electron-builder.json5` explicitly excludes the standalone static `ffmpeg.exe` (`!win32-*/ffmpeg.exe`, ~109 MB), while retaining `ffmpeg-shared.exe` via `win32-*/*`. This shared executable is spawned at runtime by: +- `electron/media/audioPeaks.ts` (`getAudioPeaks`): waveform peak extraction for timeline audio. +- `electron/stt/extractAudio.ts`: 16 kHz mono WAV audio extraction for Whisper speech-to-text. +- `electron/media/extensionClip.ts`: silence padding generation for AI word extension clips. + +`electron/media/audioPeaks.test.ts` validates that the `win.extraResources` filter in `electron-builder.json5` continues to package `ffmpeg-shared.exe`. + +### Locale pruning + +Chromium packages ~55 locale `.pak` files and macOS `.lproj` directories, totaling over 20 MB of unneeded translations. `electronLanguages` restricts the packaged locales to OpenScreen's 13 supported languages. + +Because macOS `ElectronFramework` directory matching uses underscores (`pt_BR.lproj`, `zh_CN.lproj`, `zh_TW.lproj`) while Windows and Linux `.pak` files use hyphens (`pt-BR.pak`, `zh-CN.pak`, `zh-TW.pak`), both forms are declared in `electronLanguages` to prevent `removeUnusedLanguagesIfNeeded` from deleting supported locales on macOS. + +`scripts/i18n-check.mjs` (`npm run i18n:check`) verifies that `SUPPORTED_LOCALES` in `src/i18n/config.ts`, `appx.languages`, and `electronLanguages` in `electron-builder.json5` remain synchronized. + +### Documentation assets outside `public/` + +Marketing and documentation assets (`demo.gif`, `preview*.png`) reside in `docs/assets/` rather than Vite's `public/` directory. This prevents Vite's dev server and build step from copying ~8 MB of documentation media into `dist/`. + +### Compression configuration + +`electron-builder.json5` declares `compression: "normal"`. For Windows NSIS installers, electron-builder's differential packaging options enforce normal, non-solid compression with a 1 MB dictionary (`dictSize = 1`, `solid = false`). Setting `"maximum"` has no effect on Windows installers and only increases build times for Linux AppImage targets without meaningful size reductions. + ## Platform packaging ### Windows From 9b9e47e9bb789a9d67347bce4ec6760b87d99bbf Mon Sep 17 00:00:00 2001 From: atharva9167j Date: Sun, 13 Sep 2026 01:17:43 +0530 Subject: [PATCH 4/5] test(i18n): reject unsupported extra locale tags in packaging config --- scripts/i18n-check.mjs | 56 ++++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/scripts/i18n-check.mjs b/scripts/i18n-check.mjs index 0dd765e8d..e2e91aa6f 100644 --- a/scripts/i18n-check.mjs +++ b/scripts/i18n-check.mjs @@ -140,11 +140,20 @@ if (extraOnDisk.length > 0) { // 2. Check appx.languages matches SUPPORTED_LOCALES // AppX uses BCP-47 / Windows store language tags: en-US for en, fr-FR for fr, and bare/region tags for the rest. -for (const locale of supportedLocales) { - const expectedAppx = locale === "en" ? "en-US" : locale === "fr" ? "fr-FR" : locale; - if (!appxLanguages.includes(expectedAppx)) { +const expectedAppxLanguages = supportedLocales.map((locale) => + locale === "en" ? "en-US" : locale === "fr" ? "fr-FR" : locale, +); + +for (const expected of expectedAppxLanguages) { + if (!appxLanguages.includes(expected)) { + console.error(`MISSING in electron-builder.json5 appx.languages: "${expected}"`); + hasErrors = true; + } +} +for (const configured of appxLanguages) { + if (!expectedAppxLanguages.includes(configured)) { console.error( - `MISSING in electron-builder.json5 appx.languages: "${expectedAppx}" (for ${locale})`, + `EXTRA in electron-builder.json5 appx.languages: "${configured}" (not in SUPPORTED_LOCALES)`, ); hasErrors = true; } @@ -159,23 +168,28 @@ for (const locale of supportedLocales) { // zh-CN -> requires both "zh-CN" and "zh_CN" // zh-TW -> requires both "zh-TW" and "zh_TW" // ar, es, it, ru, tr, vi, fr -> matches bare tag -for (const locale of supportedLocales) { - let expectedElectron = []; - if (locale === "en") expectedElectron = ["en-US"]; - else if (locale === "ja-JP") expectedElectron = ["ja"]; - else if (locale === "ko-KR") expectedElectron = ["ko"]; - else if (locale === "pt-BR") expectedElectron = ["pt-BR", "pt_BR"]; - else if (locale === "zh-CN") expectedElectron = ["zh-CN", "zh_CN"]; - else if (locale === "zh-TW") expectedElectron = ["zh-TW", "zh_TW"]; - else expectedElectron = [locale]; - - for (const expected of expectedElectron) { - if (!electronLanguages.includes(expected)) { - console.error( - `MISSING in electron-builder.json5 electronLanguages: "${expected}" (for ${locale})`, - ); - hasErrors = true; - } +const expectedElectronLanguages = supportedLocales.flatMap((locale) => { + if (locale === "en") return ["en-US"]; + if (locale === "ja-JP") return ["ja"]; + if (locale === "ko-KR") return ["ko"]; + if (locale === "pt-BR") return ["pt-BR", "pt_BR"]; + if (locale === "zh-CN") return ["zh-CN", "zh_CN"]; + if (locale === "zh-TW") return ["zh-TW", "zh_TW"]; + return [locale]; +}); + +for (const expected of expectedElectronLanguages) { + if (!electronLanguages.includes(expected)) { + console.error(`MISSING in electron-builder.json5 electronLanguages: "${expected}"`); + hasErrors = true; + } +} +for (const configured of electronLanguages) { + if (!expectedElectronLanguages.includes(configured)) { + console.error( + `EXTRA in electron-builder.json5 electronLanguages: "${configured}" (not in SUPPORTED_LOCALES)`, + ); + hasErrors = true; } } From 690f610fa9bcb801febf3df9f366a641fc81a26c Mon Sep 17 00:00:00 2001 From: atharva9167j Date: Sun, 13 Sep 2026 01:26:57 +0530 Subject: [PATCH 5/5] refactor(i18n): consolidate packaging locale mapping into declarative table --- scripts/i18n-check.mjs | 87 ++++++++++++++++++------------------------ 1 file changed, 38 insertions(+), 49 deletions(-) diff --git a/scripts/i18n-check.mjs b/scripts/i18n-check.mjs index e2e91aa6f..cbe2ad5f7 100644 --- a/scripts/i18n-check.mjs +++ b/scripts/i18n-check.mjs @@ -138,61 +138,50 @@ if (extraOnDisk.length > 0) { hasErrors = true; } -// 2. Check appx.languages matches SUPPORTED_LOCALES -// AppX uses BCP-47 / Windows store language tags: en-US for en, fr-FR for fr, and bare/region tags for the rest. -const expectedAppxLanguages = supportedLocales.map((locale) => - locale === "en" ? "en-US" : locale === "fr" ? "fr-FR" : locale, -); - -for (const expected of expectedAppxLanguages) { - if (!appxLanguages.includes(expected)) { - console.error(`MISSING in electron-builder.json5 appx.languages: "${expected}"`); - hasErrors = true; - } -} -for (const configured of appxLanguages) { - if (!expectedAppxLanguages.includes(configured)) { - console.error( - `EXTRA in electron-builder.json5 appx.languages: "${configured}" (not in SUPPORTED_LOCALES)`, - ); - hasErrors = true; - } +// Packaging requirements differ across distribution channels: +// - Microsoft Store (AppX) requires BCP-47 tags (en-US, fr-FR) +// - Chromium pak files use bare tags for ja/ko (ja.pak, ko.pak) and hyphens (zh-CN, pt-BR) +// - macOS .lproj folders require underscore variants (zh_CN, pt_BR, zh_TW) +// Any locale not in this table defaults to its bare tag for both channels. +const LOCALE_PACKAGING_OVERRIDES = { + en: { appx: "en-US", electron: ["en-US"] }, + fr: { appx: "fr-FR", electron: ["fr"] }, + "ja-JP": { appx: "ja-JP", electron: ["ja"] }, + "ko-KR": { appx: "ko-KR", electron: ["ko"] }, + "pt-BR": { appx: "pt-BR", electron: ["pt-BR", "pt_BR"] }, + "zh-CN": { appx: "zh-CN", electron: ["zh-CN", "zh_CN"] }, + "zh-TW": { appx: "zh-TW", electron: ["zh-TW", "zh_TW"] }, +}; + +function getPackagingTags(locale) { + return LOCALE_PACKAGING_OVERRIDES[locale] ?? { appx: locale, electron: [locale] }; } -// 3. Check electronLanguages matches SUPPORTED_LOCALES -// Electron/Chromium pak files: -// en -> en-US -// ja-JP -> ja (Chromium ja.pak / macOS ja.lproj) -// ko-KR -> ko (Chromium ko.pak / macOS ko.lproj) -// pt-BR -> requires both "pt-BR" (Windows/Linux) and "pt_BR" (macOS) -// zh-CN -> requires both "zh-CN" and "zh_CN" -// zh-TW -> requires both "zh-TW" and "zh_TW" -// ar, es, it, ru, tr, vi, fr -> matches bare tag -const expectedElectronLanguages = supportedLocales.flatMap((locale) => { - if (locale === "en") return ["en-US"]; - if (locale === "ja-JP") return ["ja"]; - if (locale === "ko-KR") return ["ko"]; - if (locale === "pt-BR") return ["pt-BR", "pt_BR"]; - if (locale === "zh-CN") return ["zh-CN", "zh_CN"]; - if (locale === "zh-TW") return ["zh-TW", "zh_TW"]; - return [locale]; -}); - -for (const expected of expectedElectronLanguages) { - if (!electronLanguages.includes(expected)) { - console.error(`MISSING in electron-builder.json5 electronLanguages: "${expected}"`); - hasErrors = true; +function assertListsMatch(actual, expected, label) { + for (const tag of expected) { + if (!actual.includes(tag)) { + console.error(`MISSING in electron-builder.json5 ${label}: "${tag}"`); + hasErrors = true; + } } -} -for (const configured of electronLanguages) { - if (!expectedElectronLanguages.includes(configured)) { - console.error( - `EXTRA in electron-builder.json5 electronLanguages: "${configured}" (not in SUPPORTED_LOCALES)`, - ); - hasErrors = true; + for (const tag of actual) { + if (!expected.includes(tag)) { + console.error( + `EXTRA in electron-builder.json5 ${label}: "${tag}" (not in SUPPORTED_LOCALES)`, + ); + hasErrors = true; + } } } +// 2. Check appx.languages matches SUPPORTED_LOCALES +const expectedAppxLanguages = supportedLocales.map((l) => getPackagingTags(l).appx); +assertListsMatch(appxLanguages, expectedAppxLanguages, "appx.languages"); + +// 3. Check electronLanguages matches SUPPORTED_LOCALES +const expectedElectronLanguages = supportedLocales.flatMap((l) => getPackagingTags(l).electron); +assertListsMatch(electronLanguages, expectedElectronLanguages, "electronLanguages"); + if (hasErrors) { console.error( "\ni18n check FAILED — translation files or packaging locale lists are out of sync.",