import { spawn, type ChildProcess } from 'node:child_process'; import { existsSync } from 'node:fs'; import { createRequire } from 'node:module'; import { dirname, join } from 'node:path'; import { app } from 'electron'; const require = createRequire(import.meta.url); const OUTPUT_LIMIT = 64 * 1024; export class PublishRuntimeError extends Error { constructor(readonly code: 'LOCAL_BUILD_RUNTIME_UNAVAILABLE' | 'LOCAL_BUILD_FAILED' | 'LOCAL_BUILD_TIMEOUT') { super(code); this.name = 'PublishRuntimeError'; } } export type PublishRuntime = { npmCli: string; nodeVersion: string; npmVersion: string; }; function childEnvironment(): NodeJS.ProcessEnv { const allowed = ['ALLUSERSPROFILE', 'APPDATA', 'COMMONPROGRAMFILES', 'COMMONPROGRAMFILES(X86)', 'COMMONPROGRAMW6432', 'COMSPEC', 'HOME', 'HOMEDRIVE', 'HOMEPATH', 'LOCALAPPDATA', 'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'NODE_EXTRA_CA_CERTS', 'http_proxy', 'https_proxy', 'no_proxy', 'NUMBER_OF_PROCESSORS', 'OS', 'PATH', 'PATHEXT', 'PROGRAMDATA', 'PROGRAMFILES', 'PROGRAMFILES(X86)', 'PROGRAMW6432', 'SYSTEMDRIVE', 'SYSTEMROOT', 'TEMP', 'TMP', 'USERPROFILE', 'WINDIR', 'XDG_CACHE_HOME', 'XDG_CONFIG_HOME']; const env: NodeJS.ProcessEnv = { ELECTRON_RUN_AS_NODE: '1', CI: '1', npm_config_update_notifier: 'false' }; for (const key of allowed) { const value = process.env[key]; if (value !== undefined) env[key] = value; } return env; } async function terminateTree(child: ChildProcess): Promise { if (!child.pid || child.exitCode !== null) return; if (process.platform === 'win32') { const taskkill = join(process.env.SYSTEMROOT ?? 'C:\\Windows', 'System32', 'taskkill.exe'); await new Promise((resolve) => { const killer = spawn(taskkill, ['/pid', String(child.pid), '/t', '/f'], { shell: false, windowsHide: true }); killer.once('close', () => resolve()); killer.once('error', () => resolve()); }); } else { try { process.kill(-child.pid, 'SIGKILL'); } catch { child.kill('SIGKILL'); } } } export async function runElectronNode(input: { args: string[]; cwd?: string; deadlineMs: number; }): Promise<{ stdout: string; stderr: string }> { return await new Promise((resolve, reject) => { const child = spawn(process.execPath, input.args, { cwd: input.cwd, env: childEnvironment(), shell: false, windowsHide: true, detached: process.platform !== 'win32', stdio: ['ignore', 'pipe', 'pipe'], }); let stdout = ''; let stderr = ''; const append = (current: string, chunk: Buffer) => (current + chunk.toString('utf8')).slice(-OUTPUT_LIMIT); child.stdout?.on('data', (chunk: Buffer) => { stdout = append(stdout, chunk); }); child.stderr?.on('data', (chunk: Buffer) => { stderr = append(stderr, chunk); }); let timedOut = false; const timer = setTimeout(() => { timedOut = true; void terminateTree(child); }, input.deadlineMs); child.once('error', () => { clearTimeout(timer); reject(new PublishRuntimeError('LOCAL_BUILD_RUNTIME_UNAVAILABLE')); }); child.once('close', (code) => { clearTimeout(timer); if (timedOut) reject(new PublishRuntimeError('LOCAL_BUILD_TIMEOUT')); else if (code !== 0) reject(new PublishRuntimeError('LOCAL_BUILD_FAILED')); else resolve({ stdout, stderr }); }); }); } export async function resolvePublishRuntime(): Promise { try { const packagedNpmPackage = join(process.resourcesPath, 'publish-runtime', 'package.json'); const npmPackage = app.isPackaged && existsSync(packagedNpmPackage) ? packagedNpmPackage : require.resolve('npm/package.json'); const npmCli = join(dirname(npmPackage), 'bin', 'npm-cli.js'); const npmResult = await runElectronNode({ args: [npmCli, '--version'], deadlineMs: 10_000 }); const nodeResult = await runElectronNode({ args: ['--version'], deadlineMs: 10_000 }); return { npmCli, nodeVersion: nodeResult.stdout.trim().replace(/^v/, ''), npmVersion: npmResult.stdout.trim(), }; } catch (error) { if (error instanceof PublishRuntimeError) throw error; throw new PublishRuntimeError('LOCAL_BUILD_RUNTIME_UNAVAILABLE'); } }