import { spawn } from 'node:child_process'; import path from 'node:path'; import { watch } from 'chokidar'; import electronPath from 'electron'; import { build as esbuildBuild } from 'esbuild'; import { createServer } from 'vite'; import fileUrlToDirname from '../config/fileUrlToDirname.js'; /** @type {string} */ const thisDir = fileUrlToDirname(import.meta.url); /** @type {string} */ const sharedModule = path.join(thisDir, '../packages/shared/dist/index.mjs'); /** @type {string} */ const serviceSharedModule = path.join( thisDir, '../packages/service-shared/dist/index.mjs', ); /** @type {RegExp[]} */ const stderrIgnorePatterns = [ // warning about devtools extension // https://github.com/cawa-93/vite-electron-builder/issues/492 // https://github.com/MarshallOfSound/electron-devtools-installer/issues/143 /ExtensionLoadWarning/, // GPU sandbox error with the mesa GLSL cache // https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=918433 /InitializeSandbox\(\) called with multiple threads in process gpu-process/, ]; /** * @param {string} packageName * @param {string[]} [extraPaths] * @param {() => void} [callback] * @returns {Promise} */ async function setupEsbuildWatcher(packageName, extraPaths, callback) { /** @type {{ default: import('esbuild').BuildOptions }} */ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Read untyped config file. const { default: config } = await import( `../packages/${packageName}/esbuild.config.js` ); config.logLevel = 'info'; config.incremental = true; const incrementalBuild = await esbuildBuild(config); const paths = [ path.join(thisDir, `../packages/${packageName}/src`), ...(extraPaths || []), ]; const watcher = watch(paths, { ignored: /(^|[/\\])\.|__(tests|mocks)__|\.(spec|test)\.[jt]sx?$/, ignoreInitial: true, persistent: true, }); if (callback) { callback(); } /** * @returns {Promise} */ async function rebuild() { try { await incrementalBuild.rebuild?.(); } catch (error) { if (typeof error === 'object' && error !== null && 'errors' in error) { const { errors } = /** @type {{ errors: unknown }} */ (error); if (Array.isArray(errors)) { const errCount = errors.length; console.error( '\uD83D\uDD25', errCount, errCount > 1 ? 'errors' : 'error', 'while rebuilding package', packageName, ); return; } } console.error( '\uD83D\uDD25', 'error while rebuilding package', packageName, error, ); return; } if (callback) { console.log(`\u26A1 Reloading package ${packageName}`); callback(); } } watcher.on('change', () => { rebuild().catch((error) => { console.error('Unexpected error while rebuilding', error); }); }); } /** * @param {string} packageName * @returns {Promise} */ async function setupDevServer(packageName) { const viteDevServer = await createServer({ build: { watch: { skipWrite: true, clearScreen: false, }, }, configFile: path.join(thisDir, `../packages/${packageName}/vite.config.js`), }); await viteDevServer.listen(); return viteDevServer; } /** * @param {(event: import('vite').HMRPayload) => void} sendEvent * @returns {Promise} */ function setupPreloadPackageWatcher(sendEvent) { return setupEsbuildWatcher('preload', [sharedModule], () => { sendEvent({ type: 'full-reload', }); }); } /** * @param {string} packageName * @param {(event: import('vite').HMRPayload) => void} sendEvent * @returns {Promise} */ function setupServicePackageWatcher(packageName, sendEvent) { return setupEsbuildWatcher(packageName, [serviceSharedModule], () => { sendEvent({ type: 'custom', event: 'sophie:reload-services', }); }); } /** * @param {import('vite').ViteDevServer} viteDevServer * @returns {Promise} */ function setupMainPackageWatcher(viteDevServer) { // Write a value to an environment variable to pass it to the main process. const { config: { server: { port, https, host }, }, } = viteDevServer; const protocol = `http${https ? 's' : ''}:`; const hostOrDefault = typeof host === 'string' ? host : 'localhost'; const portOrDefault = port || 3000; process.env.VITE_DEV_SERVER_URL = `${protocol}//${hostOrDefault}:${portOrDefault}/`; /** @type {import('child_process').ChildProcessByStdio | undefined} */ let spawnProcess; return setupEsbuildWatcher( 'main', [serviceSharedModule, sharedModule], () => { if (spawnProcess !== undefined) { spawnProcess.kill('SIGINT'); spawnProcess = undefined; } spawnProcess = spawn(String(electronPath), ['.'], { stdio: ['inherit', 'inherit', 'pipe'], }); spawnProcess.stderr.on('data', (/** @type {Buffer} */ data) => { const stderrString = data.toString('utf8').trimEnd(); if (!stderrIgnorePatterns.some((r) => r.test(stderrString))) { console.error(stderrString); } }); }, ); } /** * @returns {Promise} */ async function setupDevEnvironment() { process.env.MODE = 'development'; process.env.NODE_ENV = 'development'; /** @type {import('vite').ViteDevServer | undefined} */ let viteDevServer; /** * @param {import('vite').HMRPayload} event * @returns {void} */ function sendEvent(event) { if (viteDevServer !== undefined) { viteDevServer.ws.send(event); } } /** * @returns {Promise} */ async function startDevServer() { viteDevServer = await setupDevServer('renderer'); } /** * @returns {Promise} */ async function watchRendererPackages() { await setupEsbuildWatcher('shared'); await Promise.all([ setupPreloadPackageWatcher(sendEvent), startDevServer(), ]); } /** * @returns {Promise} */ async function watchServicePackages() { await setupEsbuildWatcher('service-shared'); await Promise.all([ setupServicePackageWatcher('service-inject', sendEvent), setupServicePackageWatcher('service-preload', sendEvent), ]); } await Promise.all([watchRendererPackages(), watchServicePackages()]); if (viteDevServer === undefined) { console.error('Failed to create vite dev server'); return; } console.log('\uD83C\uDF80 Sophie is starting up'); await setupMainPackageWatcher(viteDevServer); } setupDevEnvironment().catch((error) => { console.error(error); process.exit(1); });