diff --git a/scripts/electron-launcher.js b/scripts/electron-launcher.js index b1f3384..7190beb 100644 --- a/scripts/electron-launcher.js +++ b/scripts/electron-launcher.js @@ -35,6 +35,61 @@ function platformBinaryCandidates(platform) { } } +function electronBinaryCandidates({ packageRoot, distDir, platform }) { + 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)); + } + + return candidatePaths; +} + +function runNpmRebuild({ + packageRoot, + platform = process.platform, + arch = process.arch, + npmExecPath = process.env.npm_execpath || null, + npmNodeExecPath = process.env.npm_node_execpath || process.execPath, +}) { + if (!npmExecPath) { + return false; + } + + const result = spawnSync( + npmNodeExecPath, + [npmExecPath, 'rebuild', 'electron', '--force', '--foreground-scripts'], + { + cwd: packageRoot, + env: { + ...process.env, + npm_config_platform: platform, + npm_config_arch: arch, + }, + stdio: 'inherit', + } + ); + + if (result.error) { + throw result.error; + } + + if (result.signal) { + throw new Error(`Electron repair was interrupted by ${result.signal}`); + } + + if (result.status !== 0) { + throw new Error(`Electron rebuild failed with exit code ${result.status ?? 1}`); + } + + return true; +} + function repairElectronInstall({ packageRoot, platform = process.platform, @@ -81,6 +136,7 @@ function buildMissingElectronError({ packageRoot, distDir, candidatePaths }) { 'Try reinstalling dependencies from the repo root:', '', ' npm install', + ' npm rebuild electron --force --foreground-scripts', '', 'If that does not help, delete node_modules/electron and install again.', '', @@ -103,32 +159,27 @@ function resolveElectronBinary({ } 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 candidatePaths = electronBinaryCandidates({ packageRoot, distDir, platform }); const resolved = candidatePaths.find((candidate) => fs.existsSync(candidate)); if (!resolved) { - if (packageRoot && repairElectronInstall({ packageRoot, platform, arch })) { - const repairedHint = readElectronPathHint(packageRoot); - const repairedCandidates = []; - if (repairedHint) { - repairedCandidates.push(path.join(distDir, repairedHint)); - } - for (const relativePath of platformBinaryCandidates(platform)) { - repairedCandidates.push(path.join(distDir, relativePath)); + if (packageRoot) { + if (runNpmRebuild({ packageRoot, platform, arch })) { + const rebuilt = electronBinaryCandidates({ packageRoot, distDir, platform }).find((candidate) => + fs.existsSync(candidate) + ); + if (rebuilt) { + return rebuilt; + } } - const repaired = repairedCandidates.find((candidate) => fs.existsSync(candidate)); - if (repaired) { - return repaired; + if (repairElectronInstall({ packageRoot, platform, arch })) { + const repaired = electronBinaryCandidates({ packageRoot, distDir, platform }).find((candidate) => + fs.existsSync(candidate) + ); + if (repaired) { + return repaired; + } } } @@ -140,8 +191,10 @@ function resolveElectronBinary({ module.exports = { buildMissingElectronError, + electronBinaryCandidates, readElectronPathHint, repairElectronInstall, + runNpmRebuild, resolveElectronBinary, resolveElectronPackageRoot, platformBinaryCandidates, diff --git a/tests/unit/electron-launcher.test.js b/tests/unit/electron-launcher.test.js index 6cd0d5a..fc8d4bb 100644 --- a/tests/unit/electron-launcher.test.js +++ b/tests/unit/electron-launcher.test.js @@ -65,6 +65,40 @@ test('repairs a broken Electron install before resolving the binary', (t) => { ); }); +test('rebuilds Electron through npm when the binary is missing', (t) => { + const root = makeTmpDir('electron-rebuild'); + t.after(() => rmrf(root)); + + fs.mkdirSync(path.join(root, 'dist'), { recursive: true }); + const fakeNpmCli = path.join(root, 'fake-npm-cli.js'); + fs.writeFileSync( + fakeNpmCli, + [ + "const fs = require('node:fs');", + "const path = require('node:path');", + "fs.mkdirSync(path.join(__dirname, 'dist'), { recursive: true });", + "fs.writeFileSync(path.join(__dirname, 'dist', 'electron.exe'), 'binary');", + "fs.writeFileSync(path.join(__dirname, 'path.txt'), 'electron.exe');", + ].join('\n') + ); + + const originalNpmExecPath = process.env.npm_execpath; + const originalNpmNodeExecPath = process.env.npm_node_execpath; + process.env.npm_execpath = fakeNpmCli; + process.env.npm_node_execpath = process.execPath; + t.after(() => { + if (originalNpmExecPath === undefined) delete process.env.npm_execpath; + else process.env.npm_execpath = originalNpmExecPath; + if (originalNpmNodeExecPath === undefined) delete process.env.npm_node_execpath; + else process.env.npm_node_execpath = originalNpmNodeExecPath; + }); + + 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));