import { spawn } from 'child_process'; import { join } from '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 = join(thisDir, '../packages/shared/dist/index.mjs'); /** @type {string} */ const serviceSharedModule = 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] * @return {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 = [ join(thisDir, `../packages/${packageName}/src`), ...(extraPaths || []), ]; const watcher = watch(paths, { ignored: /(^|[/\\])\.|__(tests|mocks)__|\.(spec|test)\.[jt]sx?$/, ignoreInitial: true, persistent: true, }); if (callback) { callback(); } watcher.on('change', () => { incrementalBuild .rebuild?.() .then(() => { if (callback) { console.log(`\u26a1 Reloading package ${packageName}`); callback(); } }) .catch((err) => { if (typeof err === 'object' && 'errors' in err) { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- We just checked. const { errors } = err; 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, err, ); }); }); } /** * @param {string} packageName * @return {Promise} */ async function setupDevServer(packageName) { const viteDevServer = await createServer({ build: { watch: { skipWrite: true, clearScreen: false, }, }, configFile: join(thisDir, `../packages/${packageName}/vite.config.js`), }); await viteDevServer.listen(); return viteDevServer; } /** * @param {(event: import('vite').HMRPayload) => void} sendEvent * @return {Promise} */ function setupPreloadPackageWatcher(sendEvent) { return setupEsbuildWatcher('preload', [sharedModule], () => { sendEvent({ type: 'full-reload', }); }); } /** * @param {string} packageName * @param {(event: import('vite').HMRPayload) => void} sendEvent * @return {Promise} */ function setupServicePackageWatcher(packageName, sendEvent) { return setupEsbuildWatcher(packageName, [serviceSharedModule], () => { sendEvent({ type: 'custom', event: 'sophie:reload-services', }); }); } /** * @param {import('vite').ViteDevServer} viteDevServer * @return {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 | null} */ let spawnProcess = null; return setupEsbuildWatcher( 'main', [serviceSharedModule, sharedModule], () => { if (spawnProcess !== null) { spawnProcess.kill('SIGINT'); spawnProcess = null; } spawnProcess = spawn(String(electronPath), ['.'], { stdio: ['inherit', 'inherit', 'pipe'], }); spawnProcess.stderr.on('data', (/** @type {Buffer} */ data) => { const stderrString = data.toString('utf-8').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 | null} */ let viteDevServer = null; /** @type {(event: import('vite').HMRPayload) => void} */ const sendEvent = (event) => { if (viteDevServer !== null) { viteDevServer.ws.send(event); } }; const sharedWatcher = setupEsbuildWatcher('shared'); const serviceSharedWatcher = setupEsbuildWatcher('service-shared'); await Promise.all([ sharedWatcher.then(() => Promise.all([ setupPreloadPackageWatcher(sendEvent), setupDevServer('renderer').then((devServer) => { viteDevServer = devServer; }), ]), ), serviceSharedWatcher.then(() => Promise.all([ setupServicePackageWatcher('service-inject', sendEvent), setupServicePackageWatcher('service-preload', sendEvent), ]), ), ]); if (viteDevServer === null) { console.error('Failed to create vite dev server'); return; } console.log('\ud83c\udf80 Sophie is starting up'); await setupMainPackageWatcher(viteDevServer); } setupDevEnvironment().catch((err) => { console.error(err); process.exit(1); });