From 8a725e80fa654ea28ca63bd32f2abcd4cc08d2e3 Mon Sep 17 00:00:00 2001 From: meng-work Date: Fri, 6 Mar 2026 14:43:08 +0800 Subject: [PATCH 1/2] feat(desktop): add Windows build and runtime support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Cross-platform postinstall: replace bash-only postinstall.sh with Node.js postinstall.mjs that works on Windows, macOS, and Linux - Fix ELECTRON_RUN_AS_NODE: clear the env var before require("electron") via rollup banner so the app works when launched from VS Code terminal - Disable GPU hardware acceleration on Windows (in addition to Linux) to prevent black/blank screens from GPU driver incompatibilities - Strip crossorigin attributes from HTML on Windows — Electron's ASAR file:// handler doesn't support CORS, causing silent load failures - Fix junction removal on Windows: use rmSync({ recursive: true }) since Windows junctions are directory-like, not file symlinks - Disable npmRebuild on Windows in electron-builder — native modules are handled by install:deps/copy:native-modules, and node-gyp fails without Visual Studio Build Tools - Add NSIS installer shortcuts (desktop + start menu) - Forward renderer console warnings/errors to main process on Windows - Fix defineEnv to use || instead of ?? so empty strings from unresolved CI secrets fall back to defaults Tested: built and launched Superset.exe on Windows 11 Pro x64. Co-Authored-By: Claude Opus 4.6 --- apps/desktop/electron-builder.ts | 9 +++- apps/desktop/electron.vite.config.ts | 7 +++ apps/desktop/scripts/copy-native-modules.ts | 9 +++- .../lib/electron-app/factories/app/setup.ts | 4 +- apps/desktop/src/main/windows/main.ts | 16 +++++++ apps/desktop/vite/helpers.ts | 20 +++++++- package.json | 2 +- scripts/postinstall.mjs | 48 +++++++++++++++++++ 8 files changed, 108 insertions(+), 7 deletions(-) create mode 100644 scripts/postinstall.mjs diff --git a/apps/desktop/electron-builder.ts b/apps/desktop/electron-builder.ts index 64858b8d7ae..550566e2572 100644 --- a/apps/desktop/electron-builder.ts +++ b/apps/desktop/electron-builder.ts @@ -135,8 +135,10 @@ const config: Configuration = { "!**/.DS_Store", ], - // Rebuild native modules for Electron's Node.js version - npmRebuild: true, + // Rebuild native modules for Electron's Node.js version. + // Disabled on Windows — native modules are already handled by install:deps + // and copy:native-modules, and node-gyp may fail without Visual Studio. + npmRebuild: process.platform !== "win32", // macOS mac: { @@ -204,6 +206,9 @@ const config: Configuration = { nsis: { oneClick: false, allowToChangeInstallationDirectory: true, + createDesktopShortcut: true, + createStartMenuShortcut: true, + shortcutName: productName, }, }; diff --git a/apps/desktop/electron.vite.config.ts b/apps/desktop/electron.vite.config.ts index 5c2fcd7054b..cfe84ba174b 100644 --- a/apps/desktop/electron.vite.config.ts +++ b/apps/desktop/electron.vite.config.ts @@ -15,6 +15,7 @@ import { defineEnv, devPath, htmlEnvTransformPlugin, + stripCrossOriginPlugin, } from "./vite/helpers"; // override: true ensures .env values take precedence over inherited env vars @@ -104,6 +105,11 @@ export default defineConfig({ }, output: { dir: resolve(devPath, "main"), + // VS Code and other Electron hosts set ELECTRON_RUN_AS_NODE=1 which + // prevents Electron from entering browser mode. Clear it before any + // require("electron") call — must be the very first statement. + banner: + 'delete process.env.ELECTRON_RUN_AS_NODE;', }, external: [ "electron", @@ -235,6 +241,7 @@ export default defineConfig({ }), reactPlugin(), htmlEnvTransformPlugin(), + stripCrossOriginPlugin(), ], worker: { diff --git a/apps/desktop/scripts/copy-native-modules.ts b/apps/desktop/scripts/copy-native-modules.ts index bf8ba11e2fe..b5fd6b27d28 100644 --- a/apps/desktop/scripts/copy-native-modules.ts +++ b/apps/desktop/scripts/copy-native-modules.ts @@ -99,8 +99,13 @@ function copyModuleIfSymlink( console.log(` ${moduleName}: symlink -> replacing with real files`); console.log(` Real path: ${realPath}`); - // Remove the symlink - rmSync(modulePath); + // Windows uses junctions (directory-like) instead of symlinks; + // rmSync needs { recursive: true } to remove them. + if (process.platform === "win32") { + rmSync(modulePath, { recursive: true, force: true }); + } else { + rmSync(modulePath); + } // Copy the actual files cpSync(realPath, modulePath, { recursive: true }); diff --git a/apps/desktop/src/lib/electron-app/factories/app/setup.ts b/apps/desktop/src/lib/electron-app/factories/app/setup.ts index d2590b7a697..ebb5640e565 100644 --- a/apps/desktop/src/lib/electron-app/factories/app/setup.ts +++ b/apps/desktop/src/lib/electron-app/factories/app/setup.ts @@ -56,7 +56,9 @@ export async function makeAppSetup( return window; } -PLATFORM.IS_LINUX && app.disableHardwareAcceleration(); +// Disable GPU hardware acceleration on Linux and Windows to prevent black/blank +// screens caused by GPU driver incompatibilities with Chromium's compositor. +(PLATFORM.IS_LINUX || PLATFORM.IS_WINDOWS) && app.disableHardwareAcceleration(); // macOS Sequoia+: occluded window throttling can corrupt GPU compositor layers if (PLATFORM.IS_MAC) { diff --git a/apps/desktop/src/main/windows/main.ts b/apps/desktop/src/main/windows/main.ts index 9cfb1e3fa3c..fd7d20dce6d 100644 --- a/apps/desktop/src/main/windows/main.ts +++ b/apps/desktop/src/main/windows/main.ts @@ -220,6 +220,22 @@ export async function MainWindow() { }); } + // Forward renderer warning/error messages to main process stdout for Windows debugging. + if (PLATFORM.IS_WINDOWS) { + window.webContents.on( + "console-message", + (_event, level, message, line, sourceId) => { + if (level < 2) return; + const levelStr = + ["verbose", "info", "warning", "error"][level] ?? "unknown"; + const source = sourceId ? ` (${sourceId}:${line})` : ""; + const formatted = `[renderer:${levelStr}] ${message}${source}`; + if (level === 3) console.error(formatted); + else console.warn(formatted); + }, + ); + } + // Persist window bounds on move/resize so state survives app.exit(0) // (which skips the close handler — e.g. electron-vite SIGTERM during dev). // Gated by `initialized` so the initial maximize() doesn't immediately diff --git a/apps/desktop/vite/helpers.ts b/apps/desktop/vite/helpers.ts index 6180cd40fed..4b289df3f37 100644 --- a/apps/desktop/vite/helpers.ts +++ b/apps/desktop/vite/helpers.ts @@ -20,7 +20,7 @@ export function defineEnv( value: string | undefined, fallback?: string, ): string { - return JSON.stringify(value ?? fallback); + return JSON.stringify(value || fallback); } const RESOURCES_TO_COPY = [ @@ -62,6 +62,24 @@ export function copyResourcesPlugin(): Plugin { }; } +/** + * Strips the `crossorigin` attribute that Vite adds to script/link tags. + * Electron's ASAR file:// handler doesn't support CORS on Windows, + * so crossorigin causes scripts/styles to silently fail to load (black screen). + */ +export function stripCrossOriginPlugin(): Plugin { + return { + name: "strip-crossorigin", + transformIndexHtml: { + order: "post", + handler(html) { + if (process.platform !== "win32") return html; + return html.replace(/ crossorigin(?:="[^"]*")?/g, ""); + }, + }, + }; +} + /** * Injects environment variables into index.html CSP. */ diff --git a/package.json b/package.json index 2d37dd6c9e1..81e2da93a3f 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "format:check": "biome format .", "typecheck": "turbo typecheck", "ui-add": "turbo run ui-add", - "postinstall": "./scripts/postinstall.sh", + "postinstall": "node scripts/postinstall.mjs", "clean": "git clean -xdf node_modules", "clean:workspaces": "turbo clean", "release:desktop": "./apps/desktop/create-release.sh", diff --git a/scripts/postinstall.mjs b/scripts/postinstall.mjs new file mode 100644 index 00000000000..9a8e2b7a898 --- /dev/null +++ b/scripts/postinstall.mjs @@ -0,0 +1,48 @@ +/** + * Cross-platform postinstall script. + * + * Replaces the bash-only postinstall.sh so that `bun install` works on + * Windows, macOS and Linux without special flags. + * + * Steps: + * 1. Guard against infinite recursion (electron-builder install-app-deps + * can trigger nested bun installs which would re-run this script). + * 2. Run sherif for workspace validation. + * 3. Install native dependencies for the desktop app. + */ + +import { execSync } from "node:child_process"; + +// Prevent infinite recursion during postinstall +if (process.env.SUPERSET_POSTINSTALL_RUNNING) { + process.exit(0); +} +process.env.SUPERSET_POSTINSTALL_RUNNING = "1"; + +const env = { ...process.env, SUPERSET_POSTINSTALL_RUNNING: "1" }; + +/** Run a command, inheriting stdio so output is visible. */ +function run(cmd) { + execSync(cmd, { stdio: "inherit", env }); +} + +/** Run a command but don't fail if it errors (for optional native deps on Windows). */ +function tryRun(cmd, label) { + try { + execSync(cmd, { stdio: "inherit", env }); + } catch { + console.warn(`[postinstall] ${label} failed (non-fatal on Windows) — continuing`); + } +} + +// Run sherif for workspace validation +run("sherif"); + +// Install native dependencies for desktop app. +// On Windows, native module compilation may fail if Visual Studio Build Tools +// are not installed. This is non-fatal — prebuilt binaries will be used when available. +if (process.platform === "win32") { + tryRun("bun run --filter=@superset/desktop install:deps", "install:deps"); +} else { + run("bun run --filter=@superset/desktop install:deps"); +} From 6bd2fcafcff3fbd7e9936dbf3fdf083c9f232c8a Mon Sep 17 00:00:00 2001 From: meng-work Date: Fri, 6 Mar 2026 17:57:22 +0800 Subject: [PATCH 2/2] fix(desktop): fix Windows runtime issues with custom protocol and feature flags - Register superset-app:// custom protocol to serve renderer files, fixing dynamic import failures (code-split chunks) on file:// protocol in Electron on Windows - Add CORS header bypass for API requests from the custom protocol origin - Load renderer via superset-app://app/index.html#/ on Windows in production - Default PostHog feature flag to false when not configured, preventing CollectionsProvider from blocking render indefinitely Co-Authored-By: Claude Opus 4.6 --- apps/desktop/src/lib/window-loader.ts | 8 ++- apps/desktop/src/main/index.ts | 51 +++++++++++++++++++ .../CollectionsProvider.tsx | 8 +-- 3 files changed, 62 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/lib/window-loader.ts b/apps/desktop/src/lib/window-loader.ts index d31c07dde09..612d63067c0 100644 --- a/apps/desktop/src/lib/window-loader.ts +++ b/apps/desktop/src/lib/window-loader.ts @@ -24,8 +24,14 @@ export function registerRoute(props: { const url = `http://localhost:${env.DESKTOP_VITE_PORT}/#/`; console.log("[window-loader] Loading development URL:", url); props.browserWindow.loadURL(url); + } else if (process.platform === "win32") { + // Production (Windows): use custom protocol for proper dynamic import support. + // file:// protocol breaks ES module dynamic imports (code-split chunks) on Windows. + const url = "superset-app://app/index.html#/"; + console.log("[window-loader] Loading custom protocol URL:", url); + props.browserWindow.loadURL(url); } else { - // Production: load from file with hash routing + // Production (macOS/Linux): load from file with hash routing // TanStack Router uses hash-based routing, so we always start at #/ console.log("[window-loader] Loading file:", props.htmlFile); props.browserWindow.loadFile(props.htmlFile, { hash: "/" }); diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index dc7c56ed990..79db8604f3b 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -247,6 +247,15 @@ protocol.registerSchemesAsPrivileged([ supportFetchAPI: true, }, }, + { + scheme: "superset-app", + privileges: { + standard: true, + secure: true, + supportFetchAPI: true, + corsEnabled: true, + }, + }, ]); const gotTheLock = app.requestSingleInstanceLock(); @@ -283,6 +292,48 @@ if (!gotTheLock) { .fromPartition("persist:superset") .protocol.handle("superset-icon", iconProtocolHandler); + // Register custom protocol for serving renderer files. + // Dynamic imports (code-split chunks) fail on file:// protocol in Electron on Windows. + // The superset-app:// protocol serves files from the renderer dist directory with + // proper CORS and module support, enabling lazy-loaded route components. + const rendererDir = path.join(__dirname, "../renderer"); + const appProtocolHandler = (request: Request) => { + let urlPath = new URL(request.url).pathname; + // Remove leading slash on Windows + if (urlPath.startsWith("/")) urlPath = urlPath.slice(1); + const filePath = path.join(rendererDir, urlPath); + return net.fetch(pathToFileURL(filePath).toString()); + }; + protocol.handle("superset-app", appProtocolHandler); + session + .fromPartition("persist:superset") + .protocol.handle("superset-app", appProtocolHandler); + + // On Windows, the custom superset-app:// protocol origin is not recognized by + // the API server's CORS policy. Bypass CORS for API requests by modifying headers. + if (PLATFORM.IS_WINDOWS) { + const appSession = session.fromPartition("persist:superset"); + appSession.webRequest.onBeforeSendHeaders( + { urls: ["https://api.superset.sh/*", "https://*.posthog.com/*", "https://*.sentry.io/*", "https://app.outlit.ai/*"] }, + (details, callback) => { + // Replace custom protocol origin with the expected web origin + if (details.requestHeaders.Origin === "superset-app://app") { + delete details.requestHeaders.Origin; + } + callback({ requestHeaders: details.requestHeaders }); + }, + ); + appSession.webRequest.onHeadersReceived( + { urls: ["https://api.superset.sh/*"] }, + (details, callback) => { + const headers = details.responseHeaders ?? {}; + headers["access-control-allow-origin"] = ["superset-app://app"]; + headers["access-control-allow-credentials"] = ["true"]; + callback({ responseHeaders: headers }); + }, + ); + } + ensureProjectIconsDir(); setWorkspaceDockIcon(); initSentry(); diff --git a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/CollectionsProvider.tsx b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/CollectionsProvider.tsx index 1474f308d5f..eb8ac0ecb84 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/CollectionsProvider.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/CollectionsProvider.tsx @@ -64,12 +64,12 @@ export function CollectionsProvider({ children }: { children: ReactNode }) { preloadActiveOrganizationCollections(activeOrganizationId); }, [activeOrganizationId]); - if (useElectricCloud === undefined) { - return null; - } + // When PostHog is not configured (no key), feature flags stay undefined forever. + // Default to false (use proxy) so the app doesn't block rendering. + const isElectricCloud = useElectricCloud ?? false; setElectricUrl( - useElectricCloud + isElectricCloud ? env.NEXT_PUBLIC_ELECTRIC_URL : env.NEXT_PUBLIC_ELECTRIC_PROXY_URL, );