From aa91b3a3cfb412941b1012787131832db1f82559 Mon Sep 17 00:00:00 2001 From: Tommaso Casaburi Date: Tue, 17 Mar 2026 16:08:49 +0800 Subject: [PATCH 1/2] 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. --- .github/workflows/ci.yml | 13 +++++++ docs/agent-playbooks/known-surprises.md | 10 +++++ package.json | 21 ++++++----- scripts/verify-electron-native-modules.js | 44 ++++++++++++++++++++++ src/lib/utils/__tests__/misc-utils.test.ts | 6 ++- src/lib/utils/preload-utils.ts | 2 +- 6 files changed, 85 insertions(+), 11 deletions(-) create mode 100644 scripts/verify-electron-native-modules.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7ad5873a..5e0a769c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -212,6 +212,9 @@ jobs: ls -la build/electron/preload.cjs || 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 timeout-minutes: 30 run: | @@ -274,6 +277,9 @@ jobs: ls -la build/electron/preload.cjs || 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 timeout-minutes: 30 run: | @@ -336,6 +342,9 @@ jobs: ls -la build/electron/preload.cjs || 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 timeout-minutes: 30 run: | @@ -399,6 +408,10 @@ jobs: ls -la build/electron/preload.cjs || 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 shell: bash timeout-minutes: 30 diff --git a/docs/agent-playbooks/known-surprises.md b/docs/agent-playbooks/known-surprises.md index 1646c8e3..84d9794d 100644 --- a/docs/agent-playbooks/known-surprises.md +++ b/docs/agent-playbooks/known-surprises.md @@ -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. - **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 + +### 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 diff --git a/package.json b/package.json index 6523dc41..e055da65 100644 --- a/package.json +++ b/package.json @@ -62,15 +62,18 @@ "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: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:build": "yarn build && yarn build:preload && electron-forge make", - "electron:build:linux": "yarn build && yarn build:preload && electron-forge make --platform=linux", - "electron:build:mac": "yarn build && yarn build:preload && electron-forge make --platform=darwin", - "electron:build:windows": "yarn build && yarn build:preload && electron-forge make --platform=win32", - "electron:build:linux:x64": "yarn build && yarn build:preload && electron-forge make --platform=linux --arch=x64", - "electron:build:linux:arm64": "yarn build && yarn build:preload && electron-forge make --platform=linux --arch=arm64", - "electron:build:mac:x64": "yarn build && yarn build:preload && electron-forge make --platform=darwin --arch=x64", - "electron:build:mac:arm64": "yarn build && yarn build:preload && electron-forge make --platform=darwin --arch=arm64", + "electron:rebuild-native": "electron-rebuild -f -w better-sqlite3", + "electron:verify-native": "node scripts/verify-electron-native-modules.js", + "electron:prepare-package": "yarn electron:rebuild-native && yarn electron:verify-native", + "electron:package": "yarn build && yarn build:preload && yarn electron:prepare-package && electron-forge package", + "electron:build": "yarn build && yarn build:preload && yarn electron:prepare-package && electron-forge make", + "electron:build:linux": "yarn build && yarn build:preload && yarn electron:prepare-package && electron-forge make --platform=linux", + "electron:build:mac": "yarn build && yarn build:preload && yarn electron:prepare-package && electron-forge make --platform=darwin", + "electron:build:windows": "yarn build && yarn build:preload && yarn electron:prepare-package && electron-forge make --platform=win32", + "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: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'", diff --git a/scripts/verify-electron-native-modules.js b/scripts/verify-electron-native-modules.js new file mode 100644 index 00000000..6001c69b --- /dev/null +++ b/scripts/verify-electron-native-modules.js @@ -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'}`); +} diff --git a/src/lib/utils/__tests__/misc-utils.test.ts b/src/lib/utils/__tests__/misc-utils.test.ts index 2df9ed21..59c8adbe 100644 --- a/src/lib/utils/__tests__/misc-utils.test.ts +++ b/src/lib/utils/__tests__/misc-utils.test.ts @@ -116,7 +116,11 @@ describe('misc utils', () => { 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('schedules reply modal preload with requestIdleCallback when available', () => { diff --git a/src/lib/utils/preload-utils.ts b/src/lib/utils/preload-utils.ts index 1773c668..479c3df7 100644 --- a/src/lib/utils/preload-utils.ts +++ b/src/lib/utils/preload-utils.ts @@ -7,7 +7,7 @@ import { THEME_BUTTON_IMAGES, THEME_BACKGROUND_IMAGES } from '../../generated/as const preloadImages = (imagePaths: readonly string[]): void => { imagePaths.forEach((path) => { const img = new Image(); - img.src = `/${path}`; + img.src = `${import.meta.env.BASE_URL}${path}`; }); }; From cf326a2e98bd36750c9fed2f2bccf2f7b64dda89 Mon Sep 17 00:00:00 2001 From: Tommaso Casaburi Date: Tue, 17 Mar 2026 16:43:40 +0800 Subject: [PATCH 2/2] test(preload-utils): cover non-root base urls --- src/lib/utils/__tests__/misc-utils.test.ts | 7 ++++++- src/lib/utils/preload-utils.ts | 4 +++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/lib/utils/__tests__/misc-utils.test.ts b/src/lib/utils/__tests__/misc-utils.test.ts index 59c8adbe..7b2c0018 100644 --- a/src/lib/utils/__tests__/misc-utils.test.ts +++ b/src/lib/utils/__tests__/misc-utils.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { copyToClipboard } from '../clipboard-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 { getQuotedCidsFromContent, mergeQuotedCids } from '../reply-quote-utils'; import { formatUserIDForDisplay, truncateWithEllipsisInMiddle } from '../string-utils'; @@ -123,6 +123,11 @@ describe('misc utils', () => { ]); }); + 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', () => { const requestIdleCallback = vi.fn(); diff --git a/src/lib/utils/preload-utils.ts b/src/lib/utils/preload-utils.ts index 479c3df7..fc749b21 100644 --- a/src/lib/utils/preload-utils.ts +++ b/src/lib/utils/preload-utils.ts @@ -1,5 +1,7 @@ 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. * 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 => { imagePaths.forEach((path) => { const img = new Image(); - img.src = `${import.meta.env.BASE_URL}${path}`; + img.src = resolveAssetUrl(path); }); };