fix(electron): stage standalone bundle for desktop builds
Prepare a dedicated `.next/electron-standalone` bundle before running electron-builder so desktop packaging operates on a stable, Electron-specific server payload. This also adds a preflight that rejects standalone bundles whose top-level `node_modules` is a symlink, because electron-builder preserves `extraResources` symlinks and would otherwise ship an app that depends on the build machine at runtime.
This commit is contained in:
+13
-18
@@ -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"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
@@ -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) || "."}`
|
||||
);
|
||||
Reference in New Issue
Block a user