diff --git a/electron/package.json b/electron/package.json index 6a728a8b..5b3a4ddc 100644 --- a/electron/package.json +++ b/electron/package.json @@ -12,13 +12,14 @@ "scripts": { "start": "electron .", "dev": "electron . --no-sandbox", - "build": "electron-builder", - "build:win": "electron-builder --win", - "build:mac": "electron-builder --mac", - "build:mac-x64": "electron-builder --mac --x64", - "build:mac-arm64": "electron-builder --mac --arm64", - "build:linux": "electron-builder --linux", - "pack": "electron-builder --dir" + "prepare:bundle": "node ../scripts/prepare-electron-standalone.mjs", + "build": "npm run prepare:bundle && electron-builder", + "build:win": "npm run prepare:bundle && electron-builder --win", + "build:mac": "npm run prepare:bundle && electron-builder --mac", + "build:mac-x64": "npm run prepare:bundle && electron-builder --mac --x64", + "build:mac-arm64": "npm run prepare:bundle && electron-builder --mac --arm64", + "build:linux": "npm run prepare:bundle && electron-builder --linux", + "pack": "npm run prepare:bundle && electron-builder --dir" }, "dependencies": { "electron-updater": "^6.8.3" @@ -47,24 +48,18 @@ ], "extraResources": [ { - "from": "../.next/standalone", + "from": "../.next/electron-standalone", "to": "app", "filter": [ "**/*" ] }, { - "from": "../.next/static", - "to": "app/.next/static", + "from": "assets", + "to": "assets", "filter": [ - "**/*" - ] - }, - { - "from": "../public", - "to": "app/public", - "filter": [ - "**/*" + "icon.png", + "tray-icon.png" ] } ], diff --git a/scripts/prepare-electron-standalone.mjs b/scripts/prepare-electron-standalone.mjs new file mode 100644 index 00000000..4d26be01 --- /dev/null +++ b/scripts/prepare-electron-standalone.mjs @@ -0,0 +1,136 @@ +#!/usr/bin/env node + +import { + cpSync, + existsSync, + lstatSync, + mkdirSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { dirname, join, relative } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const ROOT = join(__dirname, ".."); + +const STANDALONE_DIR = join(ROOT, ".next", "standalone"); +const ELECTRON_STANDALONE_DIR = join(ROOT, ".next", "electron-standalone"); +const STATIC_SRC = join(ROOT, ".next", "static"); +const STATIC_DEST = join(ELECTRON_STANDALONE_DIR, ".next", "static"); +const PUBLIC_SRC = join(ROOT, "public"); +const PUBLIC_DEST = join(ELECTRON_STANDALONE_DIR, "public"); + +function resolveStandaloneBundleDir() { + const directServer = join(STANDALONE_DIR, "server.js"); + if (existsSync(directServer)) { + return STANDALONE_DIR; + } + + const nestedCandidates = [ + join(STANDALONE_DIR, "projects", "OmniRoute"), + join(STANDALONE_DIR, basename(ROOT)), + ]; + + for (const candidate of nestedCandidates) { + if (existsSync(join(candidate, "server.js"))) { + return candidate; + } + } + + throw new Error( + `Standalone server bundle not found in ${STANDALONE_DIR}. Run \`npm run build\` first.` + ); +} + +function basename(filePath) { + return filePath.split(/[\\/]/).filter(Boolean).at(-1) || filePath; +} + +function sanitizeBuildPaths(bundleDir) { + const buildRoot = ROOT.replace(/\\/g, "/"); + const bundleRoot = bundleDir.replace(/\\/g, "/"); + const replacements = [buildRoot, bundleRoot]; + const targets = [ + join(ELECTRON_STANDALONE_DIR, "server.js"), + join(ELECTRON_STANDALONE_DIR, ".next", "required-server-files.json"), + ]; + + for (const filePath of targets) { + if (!existsSync(filePath)) continue; + + let content = readFileSync(filePath, "utf8"); + let updated = content; + + for (const original of replacements) { + const escaped = original.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + updated = updated.replace(new RegExp(escaped, "g"), "."); + } + + if (updated !== content) { + writeFileSync(filePath, updated, "utf8"); + } + } +} + +function ensurePackage(pkgPath, sourcePath) { + if (existsSync(pkgPath) || !existsSync(sourcePath)) return; + mkdirSync(dirname(pkgPath), { recursive: true }); + cpSync(sourcePath, pkgPath, { recursive: true, dereference: true }); +} + +function assertBundleIsPackagable(bundleDir) { + const nodeModulesPath = join(bundleDir, "node_modules"); + if (!existsSync(nodeModulesPath)) return; + + if (lstatSync(nodeModulesPath).isSymbolicLink()) { + throw new Error( + [ + "Next standalone emitted app/node_modules as a symlink.", + "electron-builder preserves extraResources symlinks, which would make the packaged app", + "depend on the original build machine path at runtime.", + "", + `Offending path: ${nodeModulesPath}`, + "Use a real node_modules directory in the build worktree before packaging Electron.", + ].join("\n") + ); + } +} + +const bundleDir = resolveStandaloneBundleDir(); +assertBundleIsPackagable(bundleDir); + +rmSync(ELECTRON_STANDALONE_DIR, { recursive: true, force: true }); +mkdirSync(ELECTRON_STANDALONE_DIR, { recursive: true }); + +cpSync(bundleDir, ELECTRON_STANDALONE_DIR, { + recursive: true, + dereference: true, +}); + +sanitizeBuildPaths(bundleDir); + +if (existsSync(STATIC_SRC)) { + mkdirSync(dirname(STATIC_DEST), { recursive: true }); + cpSync(STATIC_SRC, STATIC_DEST, { recursive: true, dereference: true }); +} + +if (existsSync(PUBLIC_SRC)) { + cpSync(PUBLIC_SRC, PUBLIC_DEST, { recursive: true, dereference: true }); +} + +ensurePackage( + join(ELECTRON_STANDALONE_DIR, "node_modules", "@swc", "helpers"), + join(ROOT, "node_modules", "@swc", "helpers") +); + +ensurePackage( + join(ELECTRON_STANDALONE_DIR, "node_modules", "better-sqlite3"), + join(ROOT, "node_modules", "better-sqlite3") +); + +console.log( + `[electron] prepared standalone bundle: ${relative(ROOT, ELECTRON_STANDALONE_DIR) || "."}` +);