feat: main process and preload scripts support hot reloading (#7)
This commit is contained in:
parent
006418ab14
commit
1bd15486ea
|
@ -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
52
src/electron.ts
Normal 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
|
||||||
|
}
|
|
@ -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...`))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
34
src/utils.ts
34
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<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 {
|
||||||
|
|
Loading…
Reference in a new issue