SurfSense/surfsense_desktop/scripts/build-electron.mjs

136 lines
4.4 KiB
JavaScript

import { build } from 'esbuild';
import fs from 'fs';
import path from 'path';
import dotenv from 'dotenv';
const desktopEnv = dotenv.config().parsed || {};
const STANDALONE_ROOT = path.join(
'..', 'surfsense_web', '.next', 'standalone', 'surfsense_web'
);
/**
* electron-builder cannot follow symlinks when packaging into ASAR.
* Recursively walk the standalone output and replace every symlink
* with a real copy (or remove it if the target doesn't exist).
*/
function resolveAllSymlinks(dir) {
if (!fs.existsSync(dir)) return;
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isSymbolicLink()) {
const target = fs.readlinkSync(full);
const resolved = path.resolve(dir, target);
if (fs.existsSync(resolved)) {
fs.rmSync(full, { recursive: true, force: true });
fs.cpSync(resolved, full, { recursive: true });
console.log(`Resolved symlink: ${full}`);
} else {
fs.rmSync(full, { force: true });
console.log(`Removed broken symlink: ${full}`);
}
} else if (entry.isDirectory()) {
resolveAllSymlinks(full);
}
}
}
/**
* pnpm's .pnpm/ virtual store uses symlinks for sibling dependency resolution.
* After resolveAllSymlinks converts everything to real copies, packages can no
* longer find their dependencies through the pnpm structure. We flatten the
* tree into a standard npm-like layout: every package from .pnpm/*/node_modules/
* gets hoisted to the top-level node_modules/. This lets Node.js standard
* module resolution find all dependencies (e.g. next → styled-jsx).
*/
function flattenPnpmStore(nodeModulesDir) {
const pnpmDir = path.join(nodeModulesDir, '.pnpm');
if (!fs.existsSync(pnpmDir)) return;
console.log('Flattening pnpm store to top-level node_modules...');
let hoisted = 0;
for (const storePkg of fs.readdirSync(pnpmDir, { withFileTypes: true })) {
if (!storePkg.isDirectory() || storePkg.name === 'node_modules') continue;
const innerNM = path.join(pnpmDir, storePkg.name, 'node_modules');
if (!fs.existsSync(innerNM)) continue;
for (const dep of fs.readdirSync(innerNM, { withFileTypes: true })) {
const depName = dep.name;
// Handle scoped packages (@org/pkg)
if (depName.startsWith('@') && dep.isDirectory()) {
const scopeDir = path.join(innerNM, depName);
for (const scopedPkg of fs.readdirSync(scopeDir, { withFileTypes: true })) {
const fullName = `${depName}/${scopedPkg.name}`;
const src = path.join(scopeDir, scopedPkg.name);
const dest = path.join(nodeModulesDir, depName, scopedPkg.name);
if (!fs.existsSync(dest)) {
fs.mkdirSync(path.join(nodeModulesDir, depName), { recursive: true });
fs.cpSync(src, dest, { recursive: true });
hoisted++;
}
}
} else if (dep.isDirectory() || dep.isFile()) {
const dest = path.join(nodeModulesDir, depName);
if (!fs.existsSync(dest)) {
fs.cpSync(path.join(innerNM, depName), dest, { recursive: true });
hoisted++;
}
}
}
}
// Remove the .pnpm directory — no longer needed
fs.rmSync(pnpmDir, { recursive: true, force: true });
console.log(`Hoisted ${hoisted} packages, removed .pnpm/`);
}
function resolveStandaloneSymlinks() {
console.log('Resolving symlinks in standalone output...');
resolveAllSymlinks(STANDALONE_ROOT);
flattenPnpmStore(path.join(STANDALONE_ROOT, 'node_modules'));
}
async function buildElectron() {
if (fs.existsSync('dist')) {
fs.rmSync('dist', { recursive: true });
console.log('Cleaned dist/');
}
fs.mkdirSync('dist', { recursive: true });
const shared = {
bundle: true,
platform: 'node',
target: 'node18',
external: ['electron'],
sourcemap: true,
minify: false,
define: {
'process.env.HOSTED_FRONTEND_URL': JSON.stringify(
process.env.HOSTED_FRONTEND_URL || desktopEnv.HOSTED_FRONTEND_URL || 'https://surfsense.net'
),
},
};
await build({
...shared,
entryPoints: ['src/main.ts'],
outfile: 'dist/main.js',
});
await build({
...shared,
entryPoints: ['src/preload.ts'],
outfile: 'dist/preload.js',
});
console.log('Electron build complete');
resolveStandaloneSymlinks();
}
buildElectron().catch((err) => {
console.error(err);
process.exit(1);
});