feat: main process and preload scripts support hot reloading (#7)

This commit is contained in:
alex8088 2022-09-08 23:49:30 +08:00
parent 006418ab14
commit 1bd15486ea
5 changed files with 137 additions and 69 deletions

View file

@ -21,6 +21,8 @@ interface GlobalCLIOptions {
mode?: string mode?: string
ignoreConfigWarning?: boolean ignoreConfigWarning?: boolean
sourcemap?: boolean sourcemap?: boolean
w?: boolean
watch?: boolean
outDir?: string outDir?: string
} }
@ -34,7 +36,8 @@ function createInlineConfig(root: string, options: GlobalCLIOptions): InlineConf
ignoreConfigWarning: options.ignoreConfigWarning, ignoreConfigWarning: options.ignoreConfigWarning,
build: { build: {
sourcemap: options.sourcemap, 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') .command('[root]', 'start dev server and electron app')
.alias('serve') .alias('serve')
.alias('dev') .alias('dev')
.option('-w, --watch', `[boolean] rebuilds when main process or preload script modules have changed on disk`)
.action(async (root: string, options: GlobalCLIOptions) => { .action(async (root: string, options: GlobalCLIOptions) => {
const { createServer } = await import('./server') const { createServer } = await import('./server')
const inlineConfig = createInlineConfig(root, options) const inlineConfig = createInlineConfig(root, options)

52
src/electron.ts Normal file
View file

@ -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
}

View file

@ -1,8 +1,7 @@
import { spawn } from 'child_process'
import colors from 'picocolors' import colors from 'picocolors'
import { createLogger } from 'vite' import { createLogger } from 'vite'
import { InlineConfig } from './config' import type { InlineConfig } from './config'
import { ensureElectronEntryFile, getElectronPath } from './utils' import { startElectron } from './electron'
import { build } from './build' import { build } from './build'
export async function preview(inlineConfig: InlineConfig = {}): Promise<void> { export async function preview(inlineConfig: InlineConfig = {}): Promise<void> {
@ -10,18 +9,7 @@ export async function preview(inlineConfig: InlineConfig = {}): Promise<void> {
const logger = createLogger(inlineConfig.logLevel) const logger = createLogger(inlineConfig.logLevel)
ensureElectronEntryFile(inlineConfig.root) startElectron(inlineConfig.root, logger)
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)
logger.info(colors.green(`\nstart electron app...`)) logger.info(colors.green(`\nstart electron app...`))
} }

View file

@ -1,17 +1,42 @@
import { spawn } from 'child_process' import type { ChildProcessWithoutNullStreams } from 'child_process'
import { createServer as ViteCreateServer, build as viteBuild, createLogger } from 'vite' import {
type UserConfig as ViteConfig,
type ViteDevServer,
createServer as ViteCreateServer,
build as viteBuild,
createLogger,
mergeConfig
} from 'vite'
import colors from 'picocolors' import colors from 'picocolors'
import { InlineConfig, resolveConfig } from './config' import { type InlineConfig, resolveConfig } from './config'
import { ensureElectronEntryFile, getElectronPath, resolveHostname } from './utils' import { resolveHostname } from './utils'
import { startElectron } from './electron'
export async function createServer(inlineConfig: InlineConfig = {}): Promise<void> { export async function createServer(inlineConfig: InlineConfig = {}): Promise<void> {
const config = await resolveConfig(inlineConfig, 'serve', 'development') const config = await resolveConfig(inlineConfig, 'serve', 'development')
if (config.config) { if (config.config) {
const logger = createLogger(inlineConfig.logLevel) const logger = createLogger(inlineConfig.logLevel)
let server: ViteDevServer | undefined
let ps: ChildProcessWithoutNullStreams | undefined
const mainViteConfig = config.config?.main const mainViteConfig = config.config?.main
if (mainViteConfig) { 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`)) logger.info(colors.green(`\nbuild the electron main process successfully`))
} }
@ -19,7 +44,18 @@ export async function createServer(inlineConfig: InlineConfig = {}): Promise<voi
const preloadViteConfig = config.config?.preload const preloadViteConfig = config.config?.preload
if (preloadViteConfig) { if (preloadViteConfig) {
logger.info(colors.gray(`\n-----\n`)) logger.info(colors.gray(`\n-----\n`))
await viteBuild(preloadViteConfig)
const watchHook = (): void => {
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`)) logger.info(colors.green(`\nbuild the electron preload files successfully`))
} }
@ -28,7 +64,7 @@ export async function createServer(inlineConfig: InlineConfig = {}): Promise<voi
if (rendererViteConfig) { if (rendererViteConfig) {
logger.info(colors.gray(`\n-----\n`)) logger.info(colors.gray(`\n-----\n`))
const server = await ViteCreateServer(rendererViteConfig) server = await ViteCreateServer(rendererViteConfig)
if (!server.httpServer) { if (!server.httpServer) {
throw new Error('HTTP server not available') throw new Error('HTTP server not available')
@ -52,19 +88,41 @@ export async function createServer(inlineConfig: InlineConfig = {}): Promise<voi
server.printUrls() server.printUrls()
} }
ensureElectronEntryFile(inlineConfig.root) ps = startElectron(inlineConfig.root, logger)
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)
logger.info(colors.green(`\nstart electron app...`)) logger.info(colors.green(`\nstart electron app...`))
} }
} }
type UserConfig = ViteConfig & { configFile?: string | false }
async function doBuild(config: UserConfig, watchHook: () => void): Promise<void> {
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()
}
})
})
}

View file

@ -1,43 +1,9 @@
import * as path from 'path'
import * as fs from 'fs'
export function isObject(value: unknown): value is Record<string, unknown> { export function isObject(value: unknown): value is Record<string, unknown> {
return Object.prototype.toString.call(value) === '[object Object]' return Object.prototype.toString.call(value) === '[object Object]'
} }
export const dynamicImport = new Function('file', 'return import(file)') 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 const wildcardHosts = new Set(['0.0.0.0', '::', '0000:0000:0000:0000:0000:0000:0000:0000'])
export function resolveHostname(optionsHost: string | boolean | undefined): string { export function resolveHostname(optionsHost: string | boolean | undefined): string {