diff --git a/scripts/electron-launcher.js b/scripts/electron-launcher.js new file mode 100644 index 0000000..e2bfe1c --- /dev/null +++ b/scripts/electron-launcher.js @@ -0,0 +1,94 @@ +'use strict'; + +const fs = require('node:fs'); +const path = require('node:path'); + +function resolveElectronPackageRoot() { + try { + return path.dirname(require.resolve('electron/package.json')); + } catch { + return null; + } +} + +function readElectronPathHint(packageRoot) { + const pathFile = path.join(packageRoot, 'path.txt'); + if (!fs.existsSync(pathFile)) return null; + + const hint = fs.readFileSync(pathFile, 'utf8').trim(); + return hint || null; +} + +function platformBinaryCandidates(platform) { + switch (platform) { + case 'win32': + return ['electron.exe']; + case 'darwin': + return ['Electron.app/Contents/MacOS/Electron']; + case 'linux': + case 'freebsd': + case 'openbsd': + return ['electron']; + default: + return ['electron']; + } +} + +function buildMissingElectronError({ packageRoot, distDir, candidatePaths }) { + const tried = candidatePaths.map((candidate) => ` - ${candidate}`).join('\n'); + return [ + 'Electron could not be started because the desktop runtime is missing.', + '', + `Looked under: ${packageRoot}`, + `Expected the binary in: ${distDir}`, + '', + 'Try reinstalling dependencies from the repo root:', + '', + ' npm install', + '', + 'If that does not help, delete node_modules/electron and install again.', + '', + 'Searched:', + tried, + ].join('\n'); +} + +function resolveElectronBinary({ + packageRoot = resolveElectronPackageRoot(), + platform = process.platform, + overrideDistPath = process.env.ELECTRON_OVERRIDE_DIST_PATH || null, +} = {}) { + if (!packageRoot && !overrideDistPath) { + throw new Error( + 'Electron could not be started because node_modules/electron is not installed.\n\n' + + 'Run `npm install` from the repo root, then try `npm start` again.' + ); + } + + const distDir = overrideDistPath || path.join(packageRoot, 'dist'); + const candidatePaths = []; + const pathHint = packageRoot ? readElectronPathHint(packageRoot) : null; + + if (pathHint) { + candidatePaths.push(path.join(distDir, pathHint)); + } + + for (const relativePath of platformBinaryCandidates(platform)) { + candidatePaths.push(path.join(distDir, relativePath)); + } + + const resolved = candidatePaths.find((candidate) => fs.existsSync(candidate)); + if (!resolved) { + throw new Error(buildMissingElectronError({ packageRoot, distDir, candidatePaths })); + } + + return resolved; +} + +module.exports = { + buildMissingElectronError, + readElectronPathHint, + resolveElectronBinary, + resolveElectronPackageRoot, + platformBinaryCandidates, +}; diff --git a/scripts/start-electron.js b/scripts/start-electron.js index 6dfd261..81a767c 100644 --- a/scripts/start-electron.js +++ b/scripts/start-electron.js @@ -3,7 +3,9 @@ const { spawn } = require('node:child_process'); -const electronPath = require('electron'); +const { resolveElectronBinary } = require('./electron-launcher'); + +const electronPath = resolveElectronBinary(); const env = { ...process.env }; delete env.ELECTRON_RUN_AS_NODE; diff --git a/tests/unit/electron-launcher.test.js b/tests/unit/electron-launcher.test.js new file mode 100644 index 0000000..2510556 --- /dev/null +++ b/tests/unit/electron-launcher.test.js @@ -0,0 +1,59 @@ +'use strict'; + +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); + +const { + buildMissingElectronError, + resolveElectronBinary, +} = require('../../scripts/electron-launcher'); +const { makeTmpDir, rmrf } = require('./helpers'); + +test('resolves the Electron binary from path.txt when present', (t) => { + const root = makeTmpDir('electron-path-hint'); + t.after(() => rmrf(root)); + + fs.mkdirSync(path.join(root, 'dist'), { recursive: true }); + fs.writeFileSync(path.join(root, 'path.txt'), 'electron.exe\n'); + fs.writeFileSync(path.join(root, 'dist', 'electron.exe'), 'binary'); + + assert.equal( + resolveElectronBinary({ packageRoot: root, platform: 'win32' }), + path.join(root, 'dist', 'electron.exe') + ); +}); + +test('falls back to the platform binary when path.txt is absent', (t) => { + const root = makeTmpDir('electron-platform-fallback'); + t.after(() => rmrf(root)); + + fs.mkdirSync(path.join(root, 'dist'), { recursive: true }); + fs.writeFileSync(path.join(root, 'dist', 'electron.exe'), 'binary'); + + assert.equal( + resolveElectronBinary({ packageRoot: root, platform: 'win32' }), + path.join(root, 'dist', 'electron.exe') + ); +}); + +test('reports a helpful error when the runtime is missing', (t) => { + const root = makeTmpDir('electron-missing'); + t.after(() => rmrf(root)); + + fs.mkdirSync(path.join(root, 'dist'), { recursive: true }); + + assert.throws( + () => resolveElectronBinary({ packageRoot: root, platform: 'win32' }), + /npm install/ + ); + + const message = buildMissingElectronError({ + packageRoot: root, + distDir: path.join(root, 'dist'), + candidatePaths: [path.join(root, 'dist', 'electron.exe')], + }); + assert.match(message, /Electron could not be started/); + assert.match(message, /Expected the binary in:/); +});