From 1bd15486ea06902f05d23383f5b4729707810e54 Mon Sep 17 00:00:00 2001 From: alex8088 <244096523@qq.com> Date: Thu, 8 Sep 2022 23:49:30 +0800 Subject: [PATCH] feat: main process and preload scripts support hot reloading (#7) --- src/cli.ts | 6 +++- src/electron.ts | 52 +++++++++++++++++++++++++++ src/preview.ts | 18 ++-------- src/server.ts | 96 +++++++++++++++++++++++++++++++++++++++---------- src/utils.ts | 34 ------------------ 5 files changed, 137 insertions(+), 69 deletions(-) create mode 100644 src/electron.ts diff --git a/src/cli.ts b/src/cli.ts index 75a2999..c1950e4 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -21,6 +21,8 @@ interface GlobalCLIOptions { mode?: string ignoreConfigWarning?: boolean sourcemap?: boolean + w?: boolean + watch?: boolean outDir?: string } @@ -34,7 +36,8 @@ function createInlineConfig(root: string, options: GlobalCLIOptions): InlineConf ignoreConfigWarning: options.ignoreConfigWarning, build: { sourcemap: options.sourcemap, - outDir: options.outDir + outDir: options.outDir, + ...(options.w || options.watch ? { watch: {} } : null) } } } @@ -55,6 +58,7 @@ cli .command('[root]', 'start dev server and electron app') .alias('serve') .alias('dev') + .option('-w, --watch', `[boolean] rebuilds when main process or preload script modules have changed on disk`) .action(async (root: string, options: GlobalCLIOptions) => { const { createServer } = await import('./server') const inlineConfig = createInlineConfig(root, options) diff --git a/src/electron.ts b/src/electron.ts new file mode 100644 index 0000000..7595198 --- /dev/null +++ b/src/electron.ts @@ -0,0 +1,52 @@ +import path from 'node:path' +import fs from 'node:fs' +import { type ChildProcessWithoutNullStreams, spawn } from 'child_process' +import { type Logger } from 'vite' + +const ensureElectronEntryFile = (root = process.cwd()): void => { + const pkg = path.join(root, 'package.json') + if (fs.existsSync(pkg)) { + const main = require(pkg).main + if (!main) { + throw new Error('not found an entry point to electorn app, please add main field for your package.json') + } else { + const entryPath = path.resolve(root, main) + if (!fs.existsSync(entryPath)) { + throw new Error(`not found the electorn app entry file: ${entryPath}`) + } + } + } else { + throw new Error('no package.json') + } +} + +const getElectronPath = (): string => { + const electronModulePath = path.resolve(process.cwd(), 'node_modules', 'electron') + const pathFile = path.join(electronModulePath, 'path.txt') + let executablePath + if (fs.existsSync(pathFile)) { + executablePath = fs.readFileSync(pathFile, 'utf-8') + } + if (executablePath) { + return path.join(electronModulePath, 'dist', executablePath) + } else { + throw new Error('Electron uninstall') + } +} + +export function startElectron(root: string | undefined, logger: Logger): ChildProcessWithoutNullStreams { + ensureElectronEntryFile(root) + + const electronPath = getElectronPath() + + const ps = spawn(electronPath, ['.']) + ps.stdout.on('data', chunk => { + chunk.toString().trim() && logger.info(chunk.toString()) + }) + ps.stderr.on('data', chunk => { + chunk.toString().trim() && logger.error(chunk.toString()) + }) + ps.on('close', process.exit) + + return ps +} diff --git a/src/preview.ts b/src/preview.ts index c034d47..07ec3c8 100644 --- a/src/preview.ts +++ b/src/preview.ts @@ -1,8 +1,7 @@ -import { spawn } from 'child_process' import colors from 'picocolors' import { createLogger } from 'vite' -import { InlineConfig } from './config' -import { ensureElectronEntryFile, getElectronPath } from './utils' +import type { InlineConfig } from './config' +import { startElectron } from './electron' import { build } from './build' export async function preview(inlineConfig: InlineConfig = {}): Promise { @@ -10,18 +9,7 @@ export async function preview(inlineConfig: InlineConfig = {}): Promise { const logger = createLogger(inlineConfig.logLevel) - ensureElectronEntryFile(inlineConfig.root) - - const electronPath = getElectronPath() - - const ps = spawn(electronPath, ['.']) - ps.stdout.on('data', chunk => { - chunk.toString().trim() && logger.info(chunk.toString()) - }) - ps.stderr.on('data', chunk => { - chunk.toString().trim() && logger.error(chunk.toString()) - }) - ps.on('close', process.exit) + startElectron(inlineConfig.root, logger) logger.info(colors.green(`\nstart electron app...`)) } diff --git a/src/server.ts b/src/server.ts index 7214636..47d78ef 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,17 +1,42 @@ -import { spawn } from 'child_process' -import { createServer as ViteCreateServer, build as viteBuild, createLogger } from 'vite' +import type { ChildProcessWithoutNullStreams } from 'child_process' +import { + type UserConfig as ViteConfig, + type ViteDevServer, + createServer as ViteCreateServer, + build as viteBuild, + createLogger, + mergeConfig +} from 'vite' import colors from 'picocolors' -import { InlineConfig, resolveConfig } from './config' -import { ensureElectronEntryFile, getElectronPath, resolveHostname } from './utils' +import { type InlineConfig, resolveConfig } from './config' +import { resolveHostname } from './utils' +import { startElectron } from './electron' export async function createServer(inlineConfig: InlineConfig = {}): Promise { const config = await resolveConfig(inlineConfig, 'serve', 'development') if (config.config) { const logger = createLogger(inlineConfig.logLevel) + let server: ViteDevServer | undefined + let ps: ChildProcessWithoutNullStreams | undefined + const mainViteConfig = config.config?.main if (mainViteConfig) { - await viteBuild(mainViteConfig) + const watchHook = (): void => { + logger.info(colors.green(`\nrebuild the electron main process successfully`)) + + if (ps) { + logger.info(colors.cyan(`\n waiting for electron to exit...`)) + + ps.removeAllListeners() + ps.kill() + ps = startElectron(inlineConfig.root, logger) + + logger.info(colors.green(`\nrestart electron app...`)) + } + } + + await doBuild(mainViteConfig, watchHook) logger.info(colors.green(`\nbuild the electron main process successfully`)) } @@ -19,7 +44,18 @@ export async function createServer(inlineConfig: InlineConfig = {}): Promise { + logger.info(colors.green(`\nrebuild the electron preload files successfully`)) + + if (server) { + logger.info(colors.cyan(`\n trigger renderer reload`)) + + server.ws.send({ type: 'full-reload' }) + } + } + + await doBuild(preloadViteConfig, watchHook) logger.info(colors.green(`\nbuild the electron preload files successfully`)) } @@ -28,7 +64,7 @@ export async function createServer(inlineConfig: InlineConfig = {}): Promise { - chunk.toString().trim() && logger.info(chunk.toString()) - }) - ps.stderr.on('data', chunk => { - chunk.toString().trim() && logger.error(chunk.toString()) - }) - ps.on('close', process.exit) + ps = startElectron(inlineConfig.root, logger) logger.info(colors.green(`\nstart electron app...`)) } } + +type UserConfig = ViteConfig & { configFile?: string | false } + +async function doBuild(config: UserConfig, watchHook: () => void): Promise { + return new Promise(resolve => { + if (config.build?.watch) { + let firstBundle = true + const closeBundle = (): void => { + if (firstBundle) { + firstBundle = false + resolve() + } else { + watchHook() + } + } + + config = mergeConfig(config, { + plugins: [ + { + name: 'vite:electron-watcher', + closeBundle + } + ] + }) + } + + viteBuild(config).then(() => { + if (!config.build?.watch) { + resolve() + } + }) + }) +} diff --git a/src/utils.ts b/src/utils.ts index 93a6ce1..0bab821 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,43 +1,9 @@ -import * as path from 'path' -import * as fs from 'fs' - export function isObject(value: unknown): value is Record { return Object.prototype.toString.call(value) === '[object Object]' } export const dynamicImport = new Function('file', 'return import(file)') -export function ensureElectronEntryFile(root = process.cwd()): void { - const pkg = path.join(root, 'package.json') - if (fs.existsSync(pkg)) { - const main = require(pkg).main - if (!main) { - throw new Error('not found an entry point to electorn app, please add main field for your package.json') - } else { - const entryPath = path.resolve(root, main) - if (!fs.existsSync(entryPath)) { - throw new Error(`not found the electorn app entry file: ${entryPath}`) - } - } - } else { - throw new Error('no package.json') - } -} - -export function getElectronPath(): string { - const electronModulePath = path.resolve(process.cwd(), 'node_modules', 'electron') - const pathFile = path.join(electronModulePath, 'path.txt') - let executablePath - if (fs.existsSync(pathFile)) { - executablePath = fs.readFileSync(pathFile, 'utf-8') - } - if (executablePath) { - return path.join(electronModulePath, 'dist', executablePath) - } else { - throw new Error('Electron uninstall') - } -} - export const wildcardHosts = new Set(['0.0.0.0', '::', '0000:0000:0000:0000:0000:0000:0000:0000']) export function resolveHostname(optionsHost: string | boolean | undefined): string {