import { build as esbuildBuild } from 'esbuild'; import { spawn } from 'child_process'; import { watch } from 'chokidar'; import electronPath from 'electron'; import { join } from 'path'; import { createServer } from 'vite'; import { fileURLToDirname } from '../config/utils.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 }} */ 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) => { const errCount = err.errors.length; console.error( '\ud83d\udd25', errCount, errCount > 1 ? 'errors' : 'error', 'while rebuilding package', packageName, ); }); }); } /** * @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 protocol = `http${viteDevServer.config.server.https ? 's' : ''}:`; const host = viteDevServer.config.server.host || 'localhost'; const port = viteDevServer.config.server.port; const path = '/'; process.env.VITE_DEV_SERVER_URL = `${protocol}//${host}:${port}${path}`; /** @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', (data) => { const stderrString = data.toString('utf-8').trimRight(); 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'); return setupMainPackageWatcher(viteDevServer); } setupDevEnvironment().catch((err) => { console.error(err); process.exit(1); });