-
-
Notifications
You must be signed in to change notification settings - Fork 512
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
e0c8495
commit 6f68476
Showing
20 changed files
with
681 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
## plugin-vite | ||
|
||
This plugin makes it easy to set up standard vite tooling to compile both your main process code and your renderer process code, with built-in support for Hot Module Replacement (HMR) in the renderer process and support for multiple renderers. | ||
|
||
``` | ||
// forge.config.js | ||
module.exports = { | ||
plugins: [ | ||
{ | ||
name: '@electron-forge/plugin-vite', | ||
config: { | ||
// `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc. | ||
// If you are familiar with Vite configuration, it will look really familiar :) | ||
build: [ | ||
{ | ||
// `entry` is just an alias for `build.lib.entry` in the `config` counterpart file. | ||
entry: 'src/main.js', | ||
config: 'vite.main.config.mjs', | ||
}, | ||
{ | ||
entry: 'src/preload.js', | ||
config: 'vite.preload.config.mjs', | ||
}, | ||
], | ||
// Vite command options, see https://vitejs.dev/guide/cli.html | ||
CLIOptions: { | ||
// The Renderer process is configured just like a normal Vite project. | ||
// This will be more in line with what Vite users are used to. | ||
}, | ||
}, | ||
} | ||
] | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
{ | ||
"name": "@electron-forge/plugin-vite", | ||
"version": "0.0.0", | ||
"description": "Vite plugin for Electron Forge, lets you use Vite directly in your tooling", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/electron/forge", | ||
"directory": "packages/plugin/vite" | ||
}, | ||
"author": "caoxiemeihao", | ||
"license": "MIT", | ||
"main": "dist/VitePlugin.js", | ||
"typings": "dist/VitePlugin.d.ts", | ||
"scripts": { | ||
"test": "xvfb-maybe mocha --config ../../../.mocharc.js test/**/*_spec.ts" | ||
}, | ||
"devDependencies": { | ||
"@malept/cross-spawn-promise": "^2.0.0", | ||
"@types/node": "^18.0.3", | ||
"chai": "^4.3.3", | ||
"electron-packager": "^17.1.1", | ||
"fs-extra": "^10.0.0", | ||
"mocha": "^9.0.1", | ||
"which": "^2.0.2", | ||
"xvfb-maybe": "^0.2.1" | ||
}, | ||
"engines": { | ||
"node": "^14.18.0 || >=16.0.0" | ||
}, | ||
"dependencies": { | ||
"@electron-forge/core-utils": "6.0.1", | ||
"@electron-forge/plugin-base": "6.0.1", | ||
"@electron-forge/shared-types": "6.0.1", | ||
"@electron-forge/web-multi-logger": "6.0.1", | ||
"chalk": "^4.0.0", | ||
"debug": "^4.3.1", | ||
"vite": "^4.0.1" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
export interface VitePluginBuildConfig { | ||
/** | ||
* Alias of `build.lib.entry` in `config`. | ||
*/ | ||
entry?: import('vite').LibraryOptions['entry']; | ||
/** | ||
* Vite config file path. | ||
*/ | ||
config?: string; | ||
} | ||
|
||
export interface VitePluginConfig { | ||
/** | ||
* Build anything such as Main process, Preload scripts and Worker process, etc. | ||
*/ | ||
build: VitePluginBuildConfig[]; | ||
/** | ||
* Vite's CLI Options, for serve and build. | ||
* @see https://vitejs.dev/guide/cli.html | ||
*/ | ||
CLIOptions?: Record<string, any>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
import path from 'node:path'; | ||
|
||
import debug from 'debug'; | ||
import { ConfigEnv, InlineConfig, loadConfigFromFile, mergeConfig, UserConfig } from 'vite'; | ||
|
||
import { VitePluginConfig } from './Config'; | ||
import { externalBuiltins } from './util/plugins'; | ||
|
||
const d = debug('electron-forge:plugin:vite:viteconfig'); | ||
|
||
/** | ||
* If LoadResult is null, it means that Vite allows run without config file. | ||
*/ | ||
export type LoadResult = Awaited<ReturnType<typeof loadConfigFromFile>>; | ||
|
||
export default class ViteConfigGenerator { | ||
static resolveInlineConfig(CLIOptions: Record<string, any> = {}): InlineConfig { | ||
// TODO: enhance | ||
// https://github.com/vitejs/vite/blob/v4.0.1/packages/vite/src/node/cli.ts#L123-L132 | ||
const { root, base, mode, config: configFile, logLevel, clearScreen, force, ...server } = CLIOptions; | ||
return { | ||
root, | ||
base, | ||
mode, | ||
configFile, | ||
logLevel, | ||
clearScreen, | ||
optimizeDeps: { force }, | ||
server, | ||
}; | ||
} | ||
|
||
private isProd: boolean; | ||
|
||
private pluginConfig: VitePluginConfig; | ||
|
||
private projectDir!: string; | ||
|
||
private baseDir!: string; | ||
|
||
// Renderer config | ||
loadResult: LoadResult; | ||
|
||
// Vite's command config. | ||
inlineConfig: InlineConfig; | ||
|
||
constructor(pluginConfig: VitePluginConfig, projectDir: string, isProd: boolean, loadResult: LoadResult) { | ||
this.pluginConfig = pluginConfig; | ||
this.projectDir = projectDir; | ||
this.baseDir = path.join(projectDir, '.vite'); | ||
this.isProd = isProd; | ||
this.loadResult = loadResult; | ||
this.inlineConfig = ViteConfigGenerator.resolveInlineConfig(this.pluginConfig.CLIOptions); | ||
|
||
d('Config mode:', this.mode); | ||
} | ||
|
||
async resolveConfig(config: string, configEnv: Partial<ConfigEnv> = {}) { | ||
configEnv.command ??= 'build'; // should always build. | ||
configEnv.mode ??= this.mode; | ||
return loadConfigFromFile(configEnv as ConfigEnv, config); | ||
} | ||
|
||
get mode(): string { | ||
return this.inlineConfig.mode ?? (this.isProd ? 'production' : 'development'); | ||
} | ||
|
||
getDefines(): Record<string, string> { | ||
const port = this.loadResult?.config.server?.port ?? 5173; | ||
return { VITE_DEV_SERVER_URL: this.isProd ? (undefined as any) : `'http://localhost:${port}'` }; | ||
} | ||
|
||
getBuildConfig(watch = false): Promise<UserConfig>[] { | ||
if (!Array.isArray(this.pluginConfig.build)) { | ||
throw new Error('"config.build" must be an Array'); | ||
} | ||
|
||
return this.pluginConfig.build | ||
.filter(({ entry, config }) => entry || config) | ||
.map(async ({ entry, config }) => { | ||
const defaultConfig: UserConfig = { | ||
// Ensure that each build config loads the .env file correctly. | ||
mode: this.mode, | ||
build: { | ||
lib: entry | ||
? { | ||
entry, | ||
// Electron can only support cjs. | ||
formats: ['cjs'], | ||
fileName: () => '[name].js', | ||
} | ||
: undefined, | ||
// Prevent multiple builds from interfering with each other. | ||
emptyOutDir: false, | ||
// 🚧 Multiple builds may conflict. | ||
outDir: path.join(this.baseDir, 'build'), | ||
watch: watch ? {} : undefined, | ||
}, | ||
define: this.getDefines(), | ||
plugins: [externalBuiltins()], | ||
}; | ||
if (config) { | ||
const loadResult = await this.resolveConfig(config); | ||
return mergeConfig(defaultConfig, loadResult?.config ?? {}); | ||
} | ||
return defaultConfig; | ||
}); | ||
} | ||
|
||
getRendererConfig(): UserConfig { | ||
return mergeConfig( | ||
<UserConfig>{ | ||
// Make sure that Electron can be loaded into the local file using `loadFile` after packaging. | ||
base: './', | ||
build: { | ||
outDir: path.join(this.baseDir, 'renderer'), | ||
}, | ||
}, | ||
this.loadResult?.config ?? {} | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
import fs from 'node:fs'; | ||
import http from 'node:http'; | ||
import path from 'node:path'; | ||
|
||
import { namedHookWithTaskFn, PluginBase } from '@electron-forge/plugin-base'; | ||
import { ForgeMultiHookMap, StartResult } from '@electron-forge/shared-types'; | ||
import debug from 'debug'; | ||
// eslint-disable-next-line node/no-extraneous-import | ||
import { RollupWatcher } from 'rollup'; | ||
import { loadConfigFromFile, default as vite } from 'vite'; | ||
|
||
import { VitePluginConfig } from './Config'; | ||
import ViteConfigGenerator from './ViteConfig'; | ||
|
||
const d = debug('electron-forge:plugin:vite'); | ||
|
||
export default class VitePlugin extends PluginBase<VitePluginConfig> { | ||
name = 'vite'; | ||
|
||
private isProd = false; | ||
|
||
// The root of the Electron app | ||
private projectDir!: string; | ||
|
||
// Where the Vite output is generated. Usually `${projectDir}/.vite` | ||
private baseDir!: string; | ||
|
||
private _configGenerator!: Promise<ViteConfigGenerator>; | ||
|
||
private watchers: RollupWatcher[] = []; | ||
|
||
private servers: http.Server[] = []; | ||
|
||
init = (dir: string): void => { | ||
this.setDirectories(dir); | ||
|
||
d('hooking process events'); | ||
process.on('exit', (_code) => this.exitHandler({ cleanup: true })); | ||
process.on('SIGINT' as NodeJS.Signals, (_signal) => this.exitHandler({ exit: true })); | ||
}; | ||
|
||
setDirectories = (dir: string): void => { | ||
this.projectDir = dir; | ||
this.baseDir = path.join(dir, '.vite'); | ||
}; | ||
|
||
get configGenerator(): Promise<ViteConfigGenerator> { | ||
if (!this._configGenerator) { | ||
// TODO: alias(mode: m, config: c) | ||
const { mode = 'development', config: configFile } = this.config.CLIOptions ?? {}; | ||
const command = this.isProd ? 'build' : 'serve'; | ||
this._configGenerator = loadConfigFromFile({ command, mode }, configFile).then( | ||
(result) => new ViteConfigGenerator(this.config, this.projectDir, this.isProd, result) | ||
); | ||
} | ||
|
||
return this._configGenerator; | ||
} | ||
|
||
getHooks = (): ForgeMultiHookMap => { | ||
return { | ||
prePackage: [ | ||
namedHookWithTaskFn<'prePackage'>(async () => { | ||
this.isProd = true; | ||
fs.rmSync(this.baseDir, { recursive: true, force: true }); | ||
|
||
await this.build(); | ||
await this.buildRenderer(); | ||
}, 'Building vite bundles'), | ||
], | ||
}; | ||
}; | ||
|
||
private alreadyStarted = false; | ||
startLogic = async (): Promise<StartResult> => { | ||
if (this.alreadyStarted) return false; | ||
this.alreadyStarted = true; | ||
|
||
fs.rmSync(this.baseDir, { recursive: true, force: true }); | ||
|
||
return { | ||
tasks: [ | ||
{ | ||
title: 'Compiling main process code', | ||
task: async () => { | ||
await this.build(true); | ||
}, | ||
options: { | ||
showTimer: true, | ||
}, | ||
}, | ||
{ | ||
title: 'Launching dev servers for renderer process code', | ||
task: async () => { | ||
await this.launchRendererDevServers(); | ||
}, | ||
options: { | ||
persistentOutput: true, | ||
showTimer: true, | ||
}, | ||
}, | ||
], | ||
result: false, | ||
}; | ||
}; | ||
|
||
// Main process, Preload scripts and Worker process, etc. | ||
build = async (watch = false): Promise<void> => { | ||
const configs = (await this.configGenerator).getBuildConfig(watch); | ||
for (const userConfig of configs) { | ||
const buildResult = await vite.build({ | ||
// Avoid recursive builds caused by users configuring @electron-forge/plugin-vite in Vite config file. | ||
configFile: false, | ||
...(await userConfig), | ||
}); | ||
|
||
if (Object.keys(buildResult).includes('close')) { | ||
this.watchers.push(buildResult as RollupWatcher); | ||
} | ||
} | ||
}; | ||
|
||
// Renderer process | ||
buildRenderer = async (): Promise<void> => { | ||
await vite.build({ | ||
configFile: false, | ||
...(await this.configGenerator).getRendererConfig(), | ||
}); | ||
}; | ||
|
||
launchRendererDevServers = async (): Promise<void> => { | ||
const viteDevServer = await vite.createServer({ | ||
configFile: false, | ||
...(await this.configGenerator).getRendererConfig(), | ||
}); | ||
|
||
await viteDevServer.listen(); | ||
viteDevServer.printUrls(); | ||
|
||
if (viteDevServer.httpServer) { | ||
this.servers.push(viteDevServer.httpServer); | ||
} | ||
}; | ||
|
||
exitHandler = (options: { cleanup?: boolean; exit?: boolean }, err?: Error): void => { | ||
d('handling process exit with:', options); | ||
if (options.cleanup) { | ||
for (const watcher of this.watchers) { | ||
d('cleaning vite watcher'); | ||
watcher.close(); | ||
} | ||
this.watchers = []; | ||
for (const server of this.servers) { | ||
d('cleaning http server'); | ||
server.close(); | ||
} | ||
this.servers = []; | ||
} | ||
if (err) console.error(err.stack); | ||
// Why: This is literally what the option says to do. | ||
// eslint-disable-next-line no-process-exit | ||
if (options.exit) process.exit(); | ||
}; | ||
} | ||
|
||
export { VitePlugin }; |
Oops, something went wrong.