fix(electron): restore packaged desktop app loading (#1095)

* fix(electron): restore packaged desktop app loading

Rebuild and verify Electron native modules before packaging and use the Vite base URL for packaged asset preloads.

* test(preload-utils): cover non-root base urls
This commit is contained in:
Tommaso Casaburi
2026-03-17 17:04:47 +08:00
committed by GitHub
parent f812fe1d18
commit c9bd6258b2
6 changed files with 93 additions and 12 deletions
+13
View File
@@ -252,6 +252,9 @@ jobs:
ls -la build/electron/preload.cjs || exit 1 ls -la build/electron/preload.cjs || exit 1
ls -la build/index.html || exit 1 ls -la build/index.html || exit 1
- name: Rebuild and verify Electron native modules
run: yarn electron:prepare-package
- name: Package Electron app - name: Package Electron app
timeout-minutes: 30 timeout-minutes: 30
run: | run: |
@@ -314,6 +317,9 @@ jobs:
ls -la build/electron/preload.cjs || exit 1 ls -la build/electron/preload.cjs || exit 1
ls -la build/index.html || exit 1 ls -la build/index.html || exit 1
- name: Rebuild and verify Electron native modules
run: yarn electron:prepare-package
- name: Package Electron app - name: Package Electron app
timeout-minutes: 30 timeout-minutes: 30
run: | run: |
@@ -376,6 +382,9 @@ jobs:
ls -la build/electron/preload.cjs || exit 1 ls -la build/electron/preload.cjs || exit 1
ls -la build/index.html || exit 1 ls -la build/index.html || exit 1
- name: Rebuild and verify Electron native modules
run: yarn electron:prepare-package
- name: Package Electron app - name: Package Electron app
timeout-minutes: 30 timeout-minutes: 30
run: | run: |
@@ -439,6 +448,10 @@ jobs:
ls -la build/electron/preload.cjs || exit 1 ls -la build/electron/preload.cjs || exit 1
ls -la build/index.html || exit 1 ls -la build/index.html || exit 1
- name: Rebuild and verify Electron native modules
shell: bash
run: yarn electron:prepare-package
- name: Package Electron app - name: Package Electron app
shell: bash shell: bash
timeout-minutes: 30 timeout-minutes: 30
+10
View File
@@ -47,3 +47,13 @@ If uncertain, ask the developer before adding an entry.
- **Impact:** Agents may “fix” the unlisted import by adding `@plebbit/plebbit-js` to `package.json`, which violates project policy. - **Impact:** Agents may “fix” the unlisted import by adding `@plebbit/plebbit-js` to `package.json`, which violates project policy.
- **Mitigation:** Do not add `@plebbit/plebbit-js` to `package.json` for this repo. If `knip` flags `electron/start-plebbit-rpc.js`, handle it with a targeted `ignoreIssues` entry instead. - **Mitigation:** Do not add `@plebbit/plebbit-js` to `package.json` for this repo. If `knip` flags `electron/start-plebbit-rpc.js`, handle it with a targeted `ignoreIssues` entry instead.
- **Status:** confirmed - **Status:** confirmed
### Electron packaging can ship a broken `better-sqlite3` binary
- **Date:** 2026-03-17
- **Observed by:** Codex
- **Context:** Investigating the `v0.7.1` macOS arm64 DMG after the app showed a live IPFS node but never loaded boards or comments.
- **What was surprising:** The packaged app can start IPFS successfully while `electron/start-plebbit-rpc.js` loops forever because `/Applications/5chan.app/.../better_sqlite3.node` was built for plain Node 22 (`NODE_MODULE_VERSION 127`) instead of Electron 36 (`NODE_MODULE_VERSION 135`).
- **Impact:** The local RPC server on `ws://localhost:9138` never starts, so the desktop app cannot load boards, posts, or comments even though node stats look healthy.
- **Mitigation:** Before any Electron package/build job, run `yarn electron:prepare-package` so `better-sqlite3` is rebuilt for Electron and immediately verified via `ELECTRON_RUN_AS_NODE=1 electron`.
- **Status:** confirmed
+12 -9
View File
@@ -62,15 +62,18 @@
"electron:no-delete-data": "yarn build:preload && electron .", "electron:no-delete-data": "yarn build:preload && electron .",
"electron:start": "concurrently \"cross-env BROWSER=none yarn start\" \"wait-on http://5chan.localhost:1355 && yarn electron\"", "electron:start": "concurrently \"cross-env BROWSER=none yarn start\" \"wait-on http://5chan.localhost:1355 && yarn electron\"",
"electron:start:no-delete-data": "concurrently \"cross-env BROWSER=none yarn start\" \"wait-on http://5chan.localhost:1355 && yarn electron:no-delete-data\"", "electron:start:no-delete-data": "concurrently \"cross-env BROWSER=none yarn start\" \"wait-on http://5chan.localhost:1355 && yarn electron:no-delete-data\"",
"electron:package": "yarn build && yarn build:preload && electron-forge package", "electron:rebuild-native": "electron-rebuild -f -w better-sqlite3",
"electron:build": "yarn build && yarn build:preload && electron-forge make", "electron:verify-native": "node scripts/verify-electron-native-modules.js",
"electron:build:linux": "yarn build && yarn build:preload && electron-forge make --platform=linux", "electron:prepare-package": "yarn electron:rebuild-native && yarn electron:verify-native",
"electron:build:mac": "yarn build && yarn build:preload && electron-forge make --platform=darwin", "electron:package": "yarn build && yarn build:preload && yarn electron:prepare-package && electron-forge package",
"electron:build:windows": "yarn build && yarn build:preload && electron-forge make --platform=win32", "electron:build": "yarn build && yarn build:preload && yarn electron:prepare-package && electron-forge make",
"electron:build:linux:x64": "yarn build && yarn build:preload && electron-forge make --platform=linux --arch=x64", "electron:build:linux": "yarn build && yarn build:preload && yarn electron:prepare-package && electron-forge make --platform=linux",
"electron:build:linux:arm64": "yarn build && yarn build:preload && electron-forge make --platform=linux --arch=arm64", "electron:build:mac": "yarn build && yarn build:preload && yarn electron:prepare-package && electron-forge make --platform=darwin",
"electron:build:mac:x64": "yarn build && yarn build:preload && electron-forge make --platform=darwin --arch=x64", "electron:build:windows": "yarn build && yarn build:preload && yarn electron:prepare-package && electron-forge make --platform=win32",
"electron:build:mac:arm64": "yarn build && yarn build:preload && electron-forge make --platform=darwin --arch=arm64", "electron:build:linux:x64": "yarn build && yarn build:preload && yarn electron:prepare-package && electron-forge make --platform=linux --arch=x64",
"electron:build:linux:arm64": "yarn build && yarn build:preload && yarn electron:prepare-package && electron-forge make --platform=linux --arch=arm64",
"electron:build:mac:x64": "yarn build && yarn build:preload && yarn electron:prepare-package && electron-forge make --platform=darwin --arch=x64",
"electron:build:mac:arm64": "yarn build && yarn build:preload && yarn electron:prepare-package && electron-forge make --platform=darwin --arch=arm64",
"electron:before": "yarn electron:before:delete-data", "electron:before": "yarn electron:before:delete-data",
"electron:before:delete-data": "node -e \"fs.rmSync('.plebbit', { recursive: true, force: true })\"", "electron:before:delete-data": "node -e \"fs.rmSync('.plebbit', { recursive: true, force: true })\"",
"android:build:icons": "cordova-res android --skip-config --copy --resources /tmp/plebbit-react-android-icons --icon-source ./android/icons/icon.png --splash-source ./android/icons/splash.png --icon-foreground-source ./android/icons/icon-foreground.png --icon-background-source '#ffffee'", "android:build:icons": "cordova-res android --skip-config --copy --resources /tmp/plebbit-react-android-icons --icon-source ./android/icons/icon.png --splash-source ./android/icons/splash.png --icon-foreground-source ./android/icons/icon-foreground.png --icon-background-source '#ffffee'",
+44
View File
@@ -0,0 +1,44 @@
import { spawnSync } from 'node:child_process';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const electronBinary = require('electron');
if (typeof electronBinary !== 'string' || electronBinary.length === 0) {
throw new Error('Failed to resolve the Electron executable for native-module verification');
}
const nativeModules = ['better-sqlite3'];
const verificationScript = `
console.log('electron modules', process.versions.modules);
for (const moduleName of ${JSON.stringify(nativeModules)}) {
try {
require(moduleName);
console.log('native-ok', moduleName);
} catch (error) {
console.error('native-fail', moduleName);
console.error(error && error.stack ? error.stack : String(error));
process.exit(1);
}
}
`;
const result = spawnSync(electronBinary, ['-e', verificationScript], {
encoding: 'utf8',
env: {
...process.env,
ELECTRON_RUN_AS_NODE: '1',
},
});
if (result.stdout) {
process.stdout.write(result.stdout);
}
if (result.stderr) {
process.stderr.write(result.stderr);
}
if (result.status !== 0) {
throw new Error(`Electron native-module verification failed with exit code ${result.status ?? 'unknown'}`);
}
+11 -2
View File
@@ -1,7 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { copyToClipboard } from '../clipboard-utils'; import { copyToClipboard } from '../clipboard-utils';
import { hashStringToColor, getTextColorForBackground, removeMarkdown } from '../post-utils'; import { hashStringToColor, getTextColorForBackground, removeMarkdown } from '../post-utils';
import { preloadReplyModal, preloadThemeAssets } from '../preload-utils'; import { preloadReplyModal, preloadThemeAssets, resolveAssetUrl } from '../preload-utils';
import { computeOmittedCount, filterRepliesForDisplay, getPreviewDisplayReplies, getTotalReplyCount } from '../replies-preview-utils'; import { computeOmittedCount, filterRepliesForDisplay, getPreviewDisplayReplies, getTotalReplyCount } from '../replies-preview-utils';
import { getQuotedCidsFromContent, mergeQuotedCids } from '../reply-quote-utils'; import { getQuotedCidsFromContent, mergeQuotedCids } from '../reply-quote-utils';
import { formatUserIDForDisplay, truncateWithEllipsisInMiddle } from '../string-utils'; import { formatUserIDForDisplay, truncateWithEllipsisInMiddle } from '../string-utils';
@@ -116,7 +116,16 @@ describe('misc utils', () => {
preloadThemeAssets(); preloadThemeAssets();
expect(loadedSources).toEqual(['/buttons/default.png', '/buttons/hover.png', '/backgrounds/wallpaper.png']); expect(loadedSources).toEqual([
`${import.meta.env.BASE_URL}buttons/default.png`,
`${import.meta.env.BASE_URL}buttons/hover.png`,
`${import.meta.env.BASE_URL}backgrounds/wallpaper.png`,
]);
});
it('resolves theme asset URLs against a non-root base URL', () => {
expect(resolveAssetUrl('buttons/default.png', 'file:///app/')).toBe('file:///app/buttons/default.png');
expect(resolveAssetUrl('backgrounds/wallpaper.png', 'file:///app/')).toBe('file:///app/backgrounds/wallpaper.png');
}); });
it('schedules reply modal preload with requestIdleCallback when available', () => { it('schedules reply modal preload with requestIdleCallback when available', () => {
+3 -1
View File
@@ -1,5 +1,7 @@
import { THEME_BUTTON_IMAGES, THEME_BACKGROUND_IMAGES } from '../../generated/asset-manifest'; import { THEME_BUTTON_IMAGES, THEME_BACKGROUND_IMAGES } from '../../generated/asset-manifest';
export const resolveAssetUrl = (path: string, baseUrl = import.meta.env.BASE_URL): string => `${baseUrl}${path}`;
/** /**
* Preloads an array of image URLs by creating Image objects. * Preloads an array of image URLs by creating Image objects.
* Images are loaded in the background and cached by the browser. * Images are loaded in the background and cached by the browser.
@@ -7,7 +9,7 @@ import { THEME_BUTTON_IMAGES, THEME_BACKGROUND_IMAGES } from '../../generated/as
const preloadImages = (imagePaths: readonly string[]): void => { const preloadImages = (imagePaths: readonly string[]): void => {
imagePaths.forEach((path) => { imagePaths.forEach((path) => {
const img = new Image(); const img = new Image();
img.src = `/${path}`; img.src = resolveAssetUrl(path);
}); });
}; };