From 98c65089d361f366db546eb5a2c22399648b088f Mon Sep 17 00:00:00 2001 From: Manuele Conti Date: Sat, 23 May 2026 13:33:06 +0200 Subject: [PATCH 1/2] fix(editor): guard missing preload bridge handlers --- .../webpack/webpack.config.renderer.dev.ts | 2 + .../webpack/webpack.config.renderer.prod.ts | 9 +- .../__tests__/accelerator-adapter.test.ts | 31 ++++ .../editor/__tests__/device-adapter.test.ts | 8 + .../editor/__tests__/library-adapter.test.ts | 20 +++ .../editor/__tests__/project-adapter.test.ts | 8 + .../__tests__/simulator-adapter.test.ts | 15 ++ .../editor/__tests__/system-adapter.test.ts | 18 +++ .../editor/__tests__/theme-adapter.test.ts | 16 ++ .../editor/__tests__/window-adapter.test.ts | 20 +++ .../adapters/editor/accelerator-adapter.ts | 141 +++++------------- .../adapters/editor/device-adapter.ts | 4 +- .../adapters/editor/library-adapter.ts | 20 +++ .../adapters/editor/project-adapter.ts | 2 +- .../adapters/editor/simulator-adapter.ts | 11 +- .../adapters/editor/stlib-source-adapter.ts | 5 + .../adapters/editor/system-adapter.ts | 22 ++- .../adapters/editor/theme-adapter.ts | 10 +- .../adapters/editor/window-adapter.ts | 30 ++-- 19 files changed, 263 insertions(+), 129 deletions(-) diff --git a/configs/webpack/webpack.config.renderer.dev.ts b/configs/webpack/webpack.config.renderer.dev.ts index bb45bff1f..ac83e69f1 100644 --- a/configs/webpack/webpack.config.renderer.dev.ts +++ b/configs/webpack/webpack.config.renderer.dev.ts @@ -148,6 +148,8 @@ const configuration: webpack.Configuration = { extensions: ['.ts', '.js'], alias: { '@src': srcPath, + react: resolve(webpackPaths.rootPath, 'node_modules/react'), + 'react-dom': resolve(webpackPaths.rootPath, 'node_modules/react-dom'), }, }, diff --git a/configs/webpack/webpack.config.renderer.prod.ts b/configs/webpack/webpack.config.renderer.prod.ts index f04a1f69e..4c4373f2f 100644 --- a/configs/webpack/webpack.config.renderer.prod.ts +++ b/configs/webpack/webpack.config.renderer.prod.ts @@ -7,7 +7,7 @@ import CssMinimizerPlugin from 'css-minimizer-webpack-plugin' import HtmlWebpackPlugin from 'html-webpack-plugin' import MiniCssExtractPlugin from 'mini-css-extract-plugin' import MonacoEditorWebpackPlugin from 'monaco-editor-webpack-plugin' -import { join } from 'path' +import { join, resolve } from 'path' import tailwindcss from 'tailwindcss' import TerserPlugin from 'terser-webpack-plugin' import webpack from 'webpack' @@ -117,6 +117,13 @@ const configuration: webpack.Configuration = { ], }, + resolve: { + alias: { + react: resolve(webpackPaths.rootPath, 'node_modules/react'), + 'react-dom': resolve(webpackPaths.rootPath, 'node_modules/react-dom'), + }, + }, + optimization: { minimize: true, minimizer: [new TerserPlugin(), new CssMinimizerPlugin()], diff --git a/src/middleware/adapters/editor/__tests__/accelerator-adapter.test.ts b/src/middleware/adapters/editor/__tests__/accelerator-adapter.test.ts index 9e81e4440..3cb15becb 100644 --- a/src/middleware/adapters/editor/__tests__/accelerator-adapter.test.ts +++ b/src/middleware/adapters/editor/__tests__/accelerator-adapter.test.ts @@ -128,3 +128,34 @@ describe('onOpenRecent', () => { expect(cb).not.toHaveBeenCalled() }) }) + +describe('missing bridge listeners', () => { + it('returns no-op unsubscribers when the preload bridge is unavailable', () => { + window.bridge = undefined as unknown as typeof window.bridge + adapter = createEditorAcceleratorAdapter() + + const methods: Array = [ + 'onCreateProject', + 'onOpenProject', + 'onOpenRecent', + 'onSaveProject', + 'onSaveFile', + 'onCloseProject', + 'onExportProject', + 'onCloseTab', + 'onDeleteFile', + 'onFindInProject', + 'onUndo', + 'onRedo', + 'onSwitchPerspective', + 'onAbout', + 'onQuitApp', + ] + + for (const method of methods) { + const unsub = (adapter[method] as (cb: () => void) => () => void)(jest.fn()) + expect(typeof unsub).toBe('function') + expect(unsub).not.toThrow() + } + }) +}) diff --git a/src/middleware/adapters/editor/__tests__/device-adapter.test.ts b/src/middleware/adapters/editor/__tests__/device-adapter.test.ts index a2a79eb59..de4686925 100644 --- a/src/middleware/adapters/editor/__tests__/device-adapter.test.ts +++ b/src/middleware/adapters/editor/__tests__/device-adapter.test.ts @@ -73,4 +73,12 @@ describe('createEditorDeviceAdapter', () => { await adapter.getPreviewImage('motor-shield.png', '/path/to/pkg') expect(window.bridge.getPreviewImage).toHaveBeenCalledWith('motor-shield.png', '/path/to/pkg') }) + + it('returns empty communication ports when the preload bridge is unavailable', async () => { + window.bridge = undefined as unknown as typeof window.bridge + adapter = createEditorDeviceAdapter() + + await expect(adapter.getCommunicationPorts()).resolves.toEqual([]) + await expect(adapter.refreshCommunicationPorts()).resolves.toEqual([]) + }) }) diff --git a/src/middleware/adapters/editor/__tests__/library-adapter.test.ts b/src/middleware/adapters/editor/__tests__/library-adapter.test.ts index 227ceb09d..c9060d163 100644 --- a/src/middleware/adapters/editor/__tests__/library-adapter.test.ts +++ b/src/middleware/adapters/editor/__tests__/library-adapter.test.ts @@ -22,6 +22,12 @@ describe('loadAll', () => { expect(window.bridge.loadAllLibraries).toHaveBeenCalledTimes(1) expect(result).toEqual([{ manifest: { name: 'IEC' } }]) }) + + it('returns an empty list when bridge is unavailable', async () => { + window.bridge = undefined as unknown as typeof window.bridge + + await expect(createEditorLibraryAdapter().loadAll()).resolves.toEqual([]) + }) }) describe('listInstalled', () => { @@ -31,6 +37,12 @@ describe('listInstalled', () => { expect(window.bridge.listInstalledLibraries).toHaveBeenCalledTimes(1) expect(result).toEqual([{ name: 'IEC', bundled: true }]) }) + + it('returns an empty list when bridge is unavailable', async () => { + window.bridge = undefined as unknown as typeof window.bridge + + await expect(createEditorLibraryAdapter().listInstalled()).resolves.toEqual([]) + }) }) describe('installFromFile', () => { @@ -81,4 +93,12 @@ describe('onLibrariesChanged', () => { expect(window.bridge.onLibrariesChanged).toHaveBeenCalledWith(callback) expect(returned).toBe(unsub) }) + + it('returns a no-op unsubscribe when bridge is unavailable', () => { + window.bridge = undefined as unknown as typeof window.bridge + + const returned = createEditorLibraryAdapter().onLibrariesChanged(jest.fn()) + + expect(() => returned()).not.toThrow() + }) }) diff --git a/src/middleware/adapters/editor/__tests__/project-adapter.test.ts b/src/middleware/adapters/editor/__tests__/project-adapter.test.ts index 76e5637ea..3f7cbd2f1 100644 --- a/src/middleware/adapters/editor/__tests__/project-adapter.test.ts +++ b/src/middleware/adapters/editor/__tests__/project-adapter.test.ts @@ -470,6 +470,14 @@ describe('createEditorProjectAdapter', () => { expect(window.bridge.retrieveRecent).toHaveBeenCalledTimes(1) expect(result).toEqual(mockRecentProjects) }) + + it('returns an empty list when the preload bridge is unavailable', async () => { + window.bridge = undefined as unknown as typeof window.bridge + + const result = await adapter.getRecentProjects() + + expect(result).toEqual([]) + }) }) describe('readFileContent', () => { diff --git a/src/middleware/adapters/editor/__tests__/simulator-adapter.test.ts b/src/middleware/adapters/editor/__tests__/simulator-adapter.test.ts index 4b653fe2e..afe5fb8f1 100644 --- a/src/middleware/adapters/editor/__tests__/simulator-adapter.test.ts +++ b/src/middleware/adapters/editor/__tests__/simulator-adapter.test.ts @@ -154,6 +154,21 @@ describe('onStopped', () => { expect(window.bridge.onSimulatorStopped).toHaveBeenCalled() }) + it('does not throw when simulator stopped event bridge is unavailable', () => { + window.bridge = { + ...window.bridge, + onSimulatorStopped: undefined, + } as unknown as typeof window.bridge + + expect(() => createEditorSimulatorAdapter()).not.toThrow() + }) + + it('does not throw when the bridge is unavailable during adapter creation', () => { + window.bridge = undefined as unknown as typeof window.bridge + + expect(() => createEditorSimulatorAdapter()).not.toThrow() + }) + it('fires callback when main process signals stopped', async () => { const cb = jest.fn() adapter.onStopped(cb) diff --git a/src/middleware/adapters/editor/__tests__/system-adapter.test.ts b/src/middleware/adapters/editor/__tests__/system-adapter.test.ts index d24aa126b..1b61e444c 100644 --- a/src/middleware/adapters/editor/__tests__/system-adapter.test.ts +++ b/src/middleware/adapters/editor/__tests__/system-adapter.test.ts @@ -69,3 +69,21 @@ describe('log', () => { expect(window.bridge.log).toHaveBeenCalledWith('error', 'error message') }) }) + +describe('missing preload bridge', () => { + it('uses browser-safe fallbacks', async () => { + window.bridge = undefined as unknown as typeof window.bridge + ;(window.matchMedia as jest.Mock | undefined) = jest.fn().mockReturnValue({ matches: false }) + adapter = createEditorSystemAdapter() + + await expect(adapter.getSystemInfo()).resolves.toEqual({ + OS: '', + architecture: '', + prefersDarkMode: false, + isWindowMaximized: false, + }) + await expect(adapter.openExternalLink('https://example.com')).resolves.toEqual({ success: false }) + + expect(() => adapter.log('info', 'ignored')).not.toThrow() + }) +}) diff --git a/src/middleware/adapters/editor/__tests__/theme-adapter.test.ts b/src/middleware/adapters/editor/__tests__/theme-adapter.test.ts index 9ae35fd9d..cae4af696 100644 --- a/src/middleware/adapters/editor/__tests__/theme-adapter.test.ts +++ b/src/middleware/adapters/editor/__tests__/theme-adapter.test.ts @@ -100,3 +100,19 @@ describe('onThemeChanged', () => { expect(cb).not.toHaveBeenCalled() }) }) + +describe('missing preload bridge', () => { + it('keeps local theme state and returns a no-op unsubscribe', () => { + window.bridge = undefined as unknown as typeof window.bridge + adapter = createEditorThemeAdapter() + + expect(() => adapter.setTheme('light')).not.toThrow() + expect(adapter.getCurrentTheme()).toBe('light') + expect(() => adapter.toggleTheme()).not.toThrow() + expect(adapter.getCurrentTheme()).toBe('dark') + + const unsub = adapter.onThemeChanged(jest.fn()) + expect(typeof unsub).toBe('function') + expect(unsub).not.toThrow() + }) +}) diff --git a/src/middleware/adapters/editor/__tests__/window-adapter.test.ts b/src/middleware/adapters/editor/__tests__/window-adapter.test.ts index eb14c2fb4..643b1f375 100644 --- a/src/middleware/adapters/editor/__tests__/window-adapter.test.ts +++ b/src/middleware/adapters/editor/__tests__/window-adapter.test.ts @@ -160,3 +160,23 @@ describe('onMaximizedChanged', () => { unsub() }) }) + +describe('missing preload bridge', () => { + it('turns window operations and listeners into no-ops', () => { + window.bridge = undefined as unknown as typeof window.bridge + adapter = createEditorWindowAdapter() + + expect(() => adapter.minimize()).not.toThrow() + expect(() => adapter.maximize()).not.toThrow() + expect(() => adapter.close()).not.toThrow() + expect(() => adapter.hide()).not.toThrow() + expect(() => adapter.reload()).not.toThrow() + expect(() => adapter.quit()).not.toThrow() + expect(() => adapter.rebuildMenu()).not.toThrow() + + expect(adapter.onCloseRequested(jest.fn())).not.toThrow() + expect(adapter.onDarwinAppQuitting!(jest.fn())).not.toThrow() + expect(adapter.enableAutoCloseHandshake!()).not.toThrow() + expect(adapter.onMaximizedChanged!(jest.fn())).not.toThrow() + }) +}) diff --git a/src/middleware/adapters/editor/accelerator-adapter.ts b/src/middleware/adapters/editor/accelerator-adapter.ts index 02afe1ad0..9df1d2754 100644 --- a/src/middleware/adapters/editor/accelerator-adapter.ts +++ b/src/middleware/adapters/editor/accelerator-adapter.ts @@ -29,156 +29,87 @@ import type { AcceleratorPort } from '../../shared/ports/accelerator-port' import type { Unsubscribe } from '../../shared/ports/types' +type BridgeListener = (callback: (...args: unknown[]) => void) => void + +const subscribeBridgeEvent = ( + listener: BridgeListener | undefined, + callback: (...args: unknown[]) => void, + mapArgs: (...args: unknown[]) => unknown[] = () => [], +): Unsubscribe => { + if (typeof listener !== 'function') { + return () => {} + } + + let active = true + listener((...args: unknown[]) => { + if (active) callback(...mapArgs(...args)) + }) + + return () => { + active = false + } +} + export function createEditorAcceleratorAdapter(): AcceleratorPort { return { onCreateProject(callback: () => void): Unsubscribe { - let active = true - window.bridge.createProjectAccelerator(() => { - if (active) callback() - }) - return () => { - active = false - } + return subscribeBridgeEvent(window.bridge?.createProjectAccelerator, callback) }, onOpenProject(callback: () => void): Unsubscribe { - let active = true - window.bridge.handleOpenProjectRequest(() => { - if (active) callback() - }) - return () => { - active = false - } + return subscribeBridgeEvent(window.bridge?.handleOpenProjectRequest, callback) }, onOpenRecent(callback: (projectData?: unknown) => void): Unsubscribe { - let active = true - window.bridge.openRecentAccelerator((_event: unknown, response: unknown) => { - if (active) callback(response) - }) - return () => { - active = false - } + return subscribeBridgeEvent(window.bridge?.openRecentAccelerator, callback, (_event, response) => [response]) }, onSaveProject(callback: () => void): Unsubscribe { - let active = true - window.bridge.saveProjectAccelerator(() => { - if (active) callback() - }) - return () => { - active = false - } + return subscribeBridgeEvent(window.bridge?.saveProjectAccelerator, callback) }, onSaveFile(callback: () => void): Unsubscribe { - let active = true - window.bridge.saveFileAccelerator(() => { - if (active) callback() - }) - return () => { - active = false - } + return subscribeBridgeEvent(window.bridge?.saveFileAccelerator, callback) }, onCloseProject(callback: () => void): Unsubscribe { - let active = true - window.bridge.closeProjectAccelerator(() => { - if (active) callback() - }) - return () => { - active = false - } + return subscribeBridgeEvent(window.bridge?.closeProjectAccelerator, callback) }, onExportProject(callback: () => void): Unsubscribe { - let active = true - window.bridge.exportProjectRequest(() => { - if (active) callback() - }) - return () => { - active = false - } + return subscribeBridgeEvent(window.bridge?.exportProjectRequest, callback) }, onCloseTab(callback: () => void): Unsubscribe { - let active = true - window.bridge.closeTabAccelerator(() => { - if (active) callback() - }) - return () => { - active = false - } + return subscribeBridgeEvent(window.bridge?.closeTabAccelerator, callback) }, onDeleteFile(callback: () => void): Unsubscribe { - let active = true - window.bridge.deleteFileAccelerator(() => { - if (active) callback() - }) - return () => { - active = false - } + return subscribeBridgeEvent(window.bridge?.deleteFileAccelerator, callback) }, onFindInProject(callback: () => void): Unsubscribe { - let active = true - window.bridge.findInProjectAccelerator(() => { - if (active) callback() - }) - return () => { - active = false - } + return subscribeBridgeEvent(window.bridge?.findInProjectAccelerator, callback) }, onUndo(callback: () => void): Unsubscribe { - let active = true - window.bridge.handleUndoRequest(() => { - if (active) callback() - }) - return () => { - active = false - } + return subscribeBridgeEvent(window.bridge?.handleUndoRequest, callback) }, onRedo(callback: () => void): Unsubscribe { - let active = true - window.bridge.handleRedoRequest(() => { - if (active) callback() - }) - return () => { - active = false - } + return subscribeBridgeEvent(window.bridge?.handleRedoRequest, callback) }, onSwitchPerspective(callback: () => void): Unsubscribe { - let active = true - window.bridge.switchPerspective(() => { - if (active) callback() - }) - return () => { - active = false - } + return subscribeBridgeEvent(window.bridge?.switchPerspective, callback) }, onAbout(callback: () => void): Unsubscribe { - let active = true - window.bridge.aboutModalAccelerator(() => { - if (active) callback() - }) - return () => { - active = false - } + return subscribeBridgeEvent(window.bridge?.aboutModalAccelerator, callback) }, onQuitApp(callback: () => void): Unsubscribe { - let active = true - window.bridge.quitAppRequest(() => { - if (active) callback() - }) - return () => { - active = false - } + return subscribeBridgeEvent(window.bridge?.quitAppRequest, callback) }, } } diff --git a/src/middleware/adapters/editor/device-adapter.ts b/src/middleware/adapters/editor/device-adapter.ts index 9e59afadf..e2a533199 100644 --- a/src/middleware/adapters/editor/device-adapter.ts +++ b/src/middleware/adapters/editor/device-adapter.ts @@ -24,7 +24,7 @@ export function createEditorDeviceAdapter(): DevicePort { }, getCommunicationPorts(): Promise { - return window.bridge.getAvailableCommunicationPorts() + return window.bridge?.getAvailableCommunicationPorts?.() ?? Promise.resolve([]) }, refreshBoards(): Promise> { @@ -32,7 +32,7 @@ export function createEditorDeviceAdapter(): DevicePort { }, refreshCommunicationPorts(): Promise { - return window.bridge.refreshCommunicationPorts() + return window.bridge?.refreshCommunicationPorts?.() ?? Promise.resolve([]) }, getPreviewImage(imageName: string, packagePath?: string): Promise { diff --git a/src/middleware/adapters/editor/library-adapter.ts b/src/middleware/adapters/editor/library-adapter.ts index dd83d4811..1a36cfd13 100644 --- a/src/middleware/adapters/editor/library-adapter.ts +++ b/src/middleware/adapters/editor/library-adapter.ts @@ -28,25 +28,45 @@ export function createEditorLibraryAdapter(): LibraryPort { // boundary — the structural shape lines up by construction // (the main process JSON.parses .stlib files we ship and // .stlib files compileStlib produces, both share the contract). + if (typeof window.bridge?.loadAllLibraries !== 'function') { + return [] + } + const archives = await window.bridge.loadAllLibraries() return archives as StlibArchiveDTO[] }, listInstalled(): Promise { + if (typeof window.bridge?.listInstalledLibraries !== 'function') { + return Promise.resolve([]) + } + return window.bridge.listInstalledLibraries() }, installFromFile(): Promise { + if (typeof window.bridge?.installLibraryFromFile !== 'function') { + return Promise.resolve({ success: false, error: 'Library installation is not available in this context' }) + } + return window.bridge.installLibraryFromFile() }, async uninstall(name: string): Promise { + if (typeof window.bridge?.uninstallLibrary !== 'function') { + return { success: false, error: 'Library uninstall is not available in this context' } + } + const result = await window.bridge.uninstallLibrary(name) if (result.success) return { success: true } as Result return { success: false, error: result.error ?? 'Uninstall failed' } }, onLibrariesChanged(callback: () => void): Unsubscribe { + if (typeof window.bridge?.onLibrariesChanged !== 'function') { + return () => undefined + } + return window.bridge.onLibrariesChanged(callback) }, } diff --git a/src/middleware/adapters/editor/project-adapter.ts b/src/middleware/adapters/editor/project-adapter.ts index 198987592..6f60797a0 100644 --- a/src/middleware/adapters/editor/project-adapter.ts +++ b/src/middleware/adapters/editor/project-adapter.ts @@ -288,7 +288,7 @@ export function createEditorProjectAdapter(): ProjectPort { }, async getRecentProjects(): Promise { - return window.bridge.retrieveRecent() + return window.bridge?.retrieveRecent?.() ?? [] }, async readFileContent(filePath: string): Promise<{ success: boolean; content?: string; error?: string }> { diff --git a/src/middleware/adapters/editor/simulator-adapter.ts b/src/middleware/adapters/editor/simulator-adapter.ts index 72aebf308..6b0826157 100644 --- a/src/middleware/adapters/editor/simulator-adapter.ts +++ b/src/middleware/adapters/editor/simulator-adapter.ts @@ -23,10 +23,13 @@ export function createEditorSimulatorAdapter( const stopCallbacks: Array<() => void> = [] // Subscribe to main process simulator stop events once on creation - const unsubscribeFromMain = window.bridge.onSimulatorStopped(() => { - running = false - for (const cb of stopCallbacks) cb() - }) + const unsubscribeFromMain = + typeof window.bridge?.onSimulatorStopped === 'function' + ? window.bridge.onSimulatorStopped(() => { + running = false + for (const cb of stopCallbacks) cb() + }) + : undefined // Keep the unsubscribe reference to allow cleanup if needed void unsubscribeFromMain diff --git a/src/middleware/adapters/editor/stlib-source-adapter.ts b/src/middleware/adapters/editor/stlib-source-adapter.ts index 6b450dbfd..255b49249 100644 --- a/src/middleware/adapters/editor/stlib-source-adapter.ts +++ b/src/middleware/adapters/editor/stlib-source-adapter.ts @@ -43,6 +43,11 @@ export function createEditorStlibSourceAdapter(): StlibSourcePort { async function ensureCache(): Promise> { if (archives) return archives + if (typeof window.bridge?.loadAllLibraries !== 'function') { + archives = new Map() + return archives + } + // Returns StlibArchiveDTO[] — the structural shape matches // strucpp's `StlibArchive` (just a JSON object). Each entry // carries a full `manifest` block and the chunks/dependencies. diff --git a/src/middleware/adapters/editor/system-adapter.ts b/src/middleware/adapters/editor/system-adapter.ts index 03188e578..48a033fa4 100644 --- a/src/middleware/adapters/editor/system-adapter.ts +++ b/src/middleware/adapters/editor/system-adapter.ts @@ -19,23 +19,35 @@ import type { SystemInfo } from '../../shared/ports/types' export function createEditorSystemAdapter(): SystemPort { return { getSystemInfo(): Promise { - return window.bridge.getSystemInfo() + return ( + window.bridge?.getSystemInfo?.() ?? + Promise.resolve({ + OS: '', + architecture: '', + prefersDarkMode: window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false, + isWindowMaximized: false, + }) + ) }, getStoreValue(key: string): Promise { - return window.bridge.getStoreValue(key) + return window.bridge?.getStoreValue?.(key) ?? Promise.resolve(localStorage.getItem(key)) }, setStoreValue(key: string, value: string): void { - window.bridge.setStoreValue(key, value) + if (typeof window.bridge?.setStoreValue === 'function') { + window.bridge.setStoreValue(key, value) + return + } + localStorage.setItem(key, value) }, openExternalLink(url: string): Promise<{ success: boolean }> { - return window.bridge.openExternalLinkAccelerator(url) + return window.bridge?.openExternalLinkAccelerator?.(url) ?? Promise.resolve({ success: false }) }, log(level: 'info' | 'error', message: string): void { - window.bridge.log(level, message) + window.bridge?.log?.(level, message) }, } } diff --git a/src/middleware/adapters/editor/theme-adapter.ts b/src/middleware/adapters/editor/theme-adapter.ts index a7e9dc5e0..efd690036 100644 --- a/src/middleware/adapters/editor/theme-adapter.ts +++ b/src/middleware/adapters/editor/theme-adapter.ts @@ -25,12 +25,12 @@ export function createEditorThemeAdapter(): ThemePort { setTheme(theme: ThemeVariant): void { currentTheme = theme - window.bridge.winHandleUpdateTheme() + window.bridge?.winHandleUpdateTheme?.() }, toggleTheme(): void { currentTheme = currentTheme === 'dark' ? 'light' : 'dark' - window.bridge.winHandleUpdateTheme() + window.bridge?.winHandleUpdateTheme?.() }, onThemeChanged(callback: (theme: ThemeVariant) => void): Unsubscribe { @@ -42,6 +42,12 @@ export function createEditorThemeAdapter(): ThemePort { callback(currentTheme) } + if (typeof window.bridge?.handleUpdateTheme !== 'function') { + return () => { + active = false + } + } + window.bridge.handleUpdateTheme(handler) return () => { diff --git a/src/middleware/adapters/editor/window-adapter.ts b/src/middleware/adapters/editor/window-adapter.ts index c8fb72b86..a051bfcbb 100644 --- a/src/middleware/adapters/editor/window-adapter.ts +++ b/src/middleware/adapters/editor/window-adapter.ts @@ -25,34 +25,38 @@ import type { WindowPort } from '../../shared/ports/window-port' export function createEditorWindowAdapter(): WindowPort { return { minimize(): void { - window.bridge.minimizeWindow() + window.bridge?.minimizeWindow?.() }, maximize(): void { - window.bridge.maximizeWindow() + window.bridge?.maximizeWindow?.() }, close(): void { - window.bridge.handleCloseOrHideWindow() + window.bridge?.handleCloseOrHideWindow?.() }, hide(): void { - window.bridge.hideWindow() + window.bridge?.hideWindow?.() }, reload(): void { - window.bridge.reloadWindow() + window.bridge?.reloadWindow?.() }, quit(): void { - window.bridge.handleQuitApp() + window.bridge?.handleQuitApp?.() }, rebuildMenu(): void { - window.bridge.rebuildMenu() + window.bridge?.rebuildMenu?.() }, onCloseRequested(callback: () => void): Unsubscribe { + if (typeof window.bridge?.windowIsClosing !== 'function') { + return () => {} + } + let active = true window.bridge.windowIsClosing(() => { if (active) callback() @@ -63,6 +67,10 @@ export function createEditorWindowAdapter(): WindowPort { }, onDarwinAppQuitting(callback: () => void): Unsubscribe { + if (typeof window.bridge?.darwinAppIsClosing !== 'function') { + return () => {} + } + let active = true window.bridge.darwinAppIsClosing(() => { if (active) callback() @@ -73,13 +81,17 @@ export function createEditorWindowAdapter(): WindowPort { }, enableAutoCloseHandshake(): Unsubscribe { - window.bridge.handleCloseOrHideWindowAccelerator() + window.bridge?.handleCloseOrHideWindowAccelerator?.() return () => { - window.bridge.removeHandleCloseOrHideWindowAccelerator() + window.bridge?.removeHandleCloseOrHideWindowAccelerator?.() } }, onMaximizedChanged(callback: (isMaximized: boolean) => void): Unsubscribe { + if (typeof window.bridge?.isMaximizedWindow !== 'function') { + return () => {} + } + let maximized = false const handler = () => { From 6f608bfeafdac9f2dd2ac0acd315e793b7743f8d Mon Sep 17 00:00:00 2001 From: Manuele Conti Date: Wed, 27 May 2026 23:50:16 +0200 Subject: [PATCH 2/2] fix: surface board resolution build errors --- .../editor/compiler/compiler-module.ts | 18 ++++++++++++--- .../assets/icons/project/fbd/Block.tsx | 8 +++---- .../assets/icons/project/fbd/VariableIn.tsx | 10 +------- .../icons/project/fbd/VariableInOut.tsx | 12 ++-------- .../assets/icons/project/fbd/VariableOut.tsx | 2 +- .../branches/branch-status-bar.tsx | 12 +++++----- .../workspace-activity-bar/default.tsx | 6 ++++- src/frontend/screens/workspace-screen.tsx | 2 +- src/main/modules/ipc/main.ts | 9 +++++++- .../editor/__tests__/compiler-adapter.test.ts | 23 +++++++++++++++++++ .../adapters/editor/compiler-adapter.ts | 7 +++++- 11 files changed, 72 insertions(+), 37 deletions(-) diff --git a/src/backend/editor/compiler/compiler-module.ts b/src/backend/editor/compiler/compiler-module.ts index b29d6a34d..4c5e77469 100644 --- a/src/backend/editor/compiler/compiler-module.ts +++ b/src/backend/editor/compiler/compiler-module.ts @@ -2035,9 +2035,21 @@ class CompilerModule { boolean | undefined, ] - const boardRuntime = await this.#getBoardRuntime(boardTarget) // Get the board runtime from the hals.json file - - const halsContent = await CompilerModule.readJSONFile(this.halsFilePath) + let boardRuntime: string + let halsContent: HalsFile + try { + _mainProcessPort.postMessage({ logLevel: 'info', message: `Resolving board target: ${boardTarget}` }) + boardRuntime = await this.#getBoardRuntime(boardTarget) // Get the board runtime from the hals.json file + halsContent = await CompilerModule.readJSONFile(this.halsFilePath) + } catch (error) { + _mainProcessPort.postMessage({ + logLevel: 'error', + message: `Error resolving board target "${boardTarget}": ${getErrorMessage(error)}\nStopping compilation process.`, + }) + _mainProcessPort.postMessage({ closePort: true }) + _mainProcessPort.close() + return + } const normalizedProjectPath = projectPath.replace('project.json', '') diff --git a/src/frontend/assets/icons/project/fbd/Block.tsx b/src/frontend/assets/icons/project/fbd/Block.tsx index 472cdfb52..867aa6729 100644 --- a/src/frontend/assets/icons/project/fbd/Block.tsx +++ b/src/frontend/assets/icons/project/fbd/Block.tsx @@ -38,17 +38,17 @@ export default function BlockIcon(props: IBlockIconProps) { - - + + diff --git a/src/frontend/assets/icons/project/fbd/VariableIn.tsx b/src/frontend/assets/icons/project/fbd/VariableIn.tsx index d97ce93bd..c5b9550bd 100644 --- a/src/frontend/assets/icons/project/fbd/VariableIn.tsx +++ b/src/frontend/assets/icons/project/fbd/VariableIn.tsx @@ -36,15 +36,7 @@ export default function VariableInIcon(props: IBlockIconProps) { stroke='#B4D0FE' strokeWidth='1.22727' /> - + ) } diff --git a/src/frontend/assets/icons/project/fbd/VariableInOut.tsx b/src/frontend/assets/icons/project/fbd/VariableInOut.tsx index 70be2cf05..bcd2e1cae 100644 --- a/src/frontend/assets/icons/project/fbd/VariableInOut.tsx +++ b/src/frontend/assets/icons/project/fbd/VariableInOut.tsx @@ -36,16 +36,8 @@ export default function VariableInOutIcon(props: IBlockIconProps) { stroke='#B4D0FE' strokeWidth='1.22727' /> - - + + ) } diff --git a/src/frontend/assets/icons/project/fbd/VariableOut.tsx b/src/frontend/assets/icons/project/fbd/VariableOut.tsx index 559f90ce0..b7fdeed12 100644 --- a/src/frontend/assets/icons/project/fbd/VariableOut.tsx +++ b/src/frontend/assets/icons/project/fbd/VariableOut.tsx @@ -36,7 +36,7 @@ export default function VariableOutIcon(props: IBlockIconProps) { stroke='#B4D0FE' strokeWidth='1.22727' /> - + ) } diff --git a/src/frontend/components/_features/[workspace]/branches/branch-status-bar.tsx b/src/frontend/components/_features/[workspace]/branches/branch-status-bar.tsx index e604c5983..4b91520c9 100644 --- a/src/frontend/components/_features/[workspace]/branches/branch-status-bar.tsx +++ b/src/frontend/components/_features/[workspace]/branches/branch-status-bar.tsx @@ -9,7 +9,7 @@ import { UnsavedChangesWarningModal } from './unsaved-changes-warning-modal' type BranchStatusBarProps = { projectId: string - onBranchSwitch?: (branchName: string) => void + onBranchSwitch?: (branchName: string) => void | Promise } export function BranchStatusBar({ projectId, onBranchSwitch }: BranchStatusBarProps) { @@ -30,7 +30,7 @@ export function BranchStatusBar({ projectId, onBranchSwitch }: BranchStatusBarPr try { await versionControl.switchBranch(projectId, branch.name) setActiveBranch(branch.name) - onBranchSwitch?.(branch.name) + void onBranchSwitch?.(branch.name) } catch (error) { console.error('Failed to switch branch:', error) } @@ -56,7 +56,7 @@ export function BranchStatusBar({ projectId, onBranchSwitch }: BranchStatusBarPr // If we can't check, proceed with switch } - doSwitch(branch) + void doSwitch(branch) }, [activeBranchName, projectId, versionControl, doSwitch], ) @@ -103,7 +103,7 @@ export function BranchStatusBar({ projectId, onBranchSwitch }: BranchStatusBarPr .then(({ branches }) => { const defaultBranch = branches.find((b) => b.isDefault) if (defaultBranch) { - doSwitch(defaultBranch) + void doSwitch(defaultBranch) } }) .catch(() => {}) @@ -133,7 +133,7 @@ export function BranchStatusBar({ projectId, onBranchSwitch }: BranchStatusBarPr currentBranchName={activeBranchName} anchorRef={branchButtonRef} onClose={() => setShowSwitcher(false)} - onSelect={handleSelect} + onSelect={(branch) => void handleSelect(branch)} onDelete={handleDelete} onMerge={handleMerge} /> @@ -152,7 +152,7 @@ export function BranchStatusBar({ projectId, onBranchSwitch }: BranchStatusBarPr void handleDiscardAndSwitch()} onCancel={handleCancelSwitch} /> diff --git a/src/frontend/components/_organisms/workspace-activity-bar/default.tsx b/src/frontend/components/_organisms/workspace-activity-bar/default.tsx index 9a47f82b9..444fe74db 100644 --- a/src/frontend/components/_organisms/workspace-activity-bar/default.tsx +++ b/src/frontend/components/_organisms/workspace-activity-bar/default.tsx @@ -154,6 +154,7 @@ export const DefaultWorkspaceActivityBar = ({ zoom }: DefaultWorkspaceActivityBa const freshProjectData = useOpenPLCStore.getState().project.data try { + let streamedError = false const result = await compiler.compileProgram( { projectData: freshProjectData, @@ -171,6 +172,9 @@ export const DefaultWorkspaceActivityBar = ({ zoom }: DefaultWorkspaceActivityBa .getState() .deviceActions.setPlcRuntimeStatus(event.plcStatus as NonNullable) } + if (event.level === 'error' || event.stage === 'error') { + streamedError = true + } logCompilerEvent(event, addLog) if (event.firmwarePath && isSimulatorBoard) { void simulator.loadFirmware(event.firmwarePath).then((loadResult) => { @@ -194,7 +198,7 @@ export const DefaultWorkspaceActivityBar = ({ zoom }: DefaultWorkspaceActivityBa }, ) - if (!result.success) { + if (!result.success && !streamedError) { addLog({ id: crypto.randomUUID(), level: 'error', message: result.error ?? 'Compilation failed' }) } } catch (err: unknown) { diff --git a/src/frontend/screens/workspace-screen.tsx b/src/frontend/screens/workspace-screen.tsx index 1c1676b83..ea583a494 100644 --- a/src/frontend/screens/workspace-screen.tsx +++ b/src/frontend/screens/workspace-screen.tsx @@ -469,7 +469,7 @@ const WorkspaceScreen = () => { diff --git a/src/main/modules/ipc/main.ts b/src/main/modules/ipc/main.ts index fd1a7d14e..ac4150536 100644 --- a/src/main/modules/ipc/main.ts +++ b/src/main/modules/ipc/main.ts @@ -1009,7 +1009,14 @@ class MainProcessBridge implements MainIpcModule { handleRunCompileProgram = (event: IpcMainEvent, args: Array) => { const mainProcessPort = event.ports[0] - void this.compilerModule.compileProgram(args, mainProcessPort, this) + void this.compilerModule.compileProgram(args, mainProcessPort, this).catch((error) => { + mainProcessPort.postMessage({ + logLevel: 'error', + message: `${getErrorMessage(error)}\nStopping compilation process.`, + }) + mainProcessPort.postMessage({ closePort: true }) + mainProcessPort.close() + }) } handleRunDebugCompilation = (event: IpcMainEvent, args: Array) => { diff --git a/src/middleware/adapters/editor/__tests__/compiler-adapter.test.ts b/src/middleware/adapters/editor/__tests__/compiler-adapter.test.ts index ac394c135..e5d3d0ab9 100644 --- a/src/middleware/adapters/editor/__tests__/compiler-adapter.test.ts +++ b/src/middleware/adapters/editor/__tests__/compiler-adapter.test.ts @@ -261,6 +261,29 @@ describe('createEditorCompilerAdapter', () => { expect(result).toEqual({ success: false, error: 'Compilation failed: missing file' }) expect(progressEvents.some((e) => e.stage === 'error')).toBe(true) + expect(progressEvents.some((e) => e.stage === 'done' && e.message === 'Compilation complete')).toBe(false) + }) + + it('ignores duplicate closePort events after an error', async () => { + const progressEvents: CompileProgressEvent[] = [] + const promise = adapter.compileProgram( + { + projectData: mockProjectData, + boardTarget: 'Arduino Mega', + projectPath: '/path', + }, + (event) => progressEvents.push(event), + ) + + await flushMicrotasks() + compileCallback!({ message: 'Board not found', logLevel: 'error' }) + compileCallback!({ closePort: true }) + compileCallback!({ closePort: true }) + + const result = await promise + + expect(result).toEqual({ success: false, error: 'Board not found' }) + expect(progressEvents).toEqual([{ stage: 'error', message: 'Board not found', level: 'error' }]) }) it('captures simulatorFirmwarePath as hexPath', async () => { diff --git a/src/middleware/adapters/editor/compiler-adapter.ts b/src/middleware/adapters/editor/compiler-adapter.ts index fa98f0da7..809c83306 100644 --- a/src/middleware/adapters/editor/compiler-adapter.ts +++ b/src/middleware/adapters/editor/compiler-adapter.ts @@ -204,6 +204,7 @@ export function createEditorCompilerAdapter(): CompilerPort { let hasError = false let lastError = '' let hexPath: string | undefined + let settled = false window.bridge.runCompileProgram( [ @@ -225,7 +226,11 @@ export function createEditorCompilerAdapter(): CompilerPort { } if (data.closePort) { - onProgress({ stage: 'done', message: 'Compilation complete' }) + if (settled) return + settled = true + if (!hasError) { + onProgress({ stage: 'done', message: 'Compilation complete' }) + } resolve( hasError ? { success: false, error: lastError }