Skip to content

Commit

Permalink
feat(template): add vite template
Browse files Browse the repository at this point in the history
  • Loading branch information
caoxiemeihao committed Dec 18, 2022
1 parent e0c8495 commit 6f68476
Show file tree
Hide file tree
Showing 20 changed files with 681 additions and 0 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"source-map-support": "^0.5.13",
"sudo-prompt": "^9.1.1",
"username": "^5.1.0",
"vite": "^4.0.1",
"webpack": "^5.69.1",
"webpack-dev-server": "^4.0.0",
"webpack-merge": "^5.7.3",
Expand Down
35 changes: 35 additions & 0 deletions packages/plugin/vite/README.md
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.
},
},
}
]
}
```
39 changes: 39 additions & 0 deletions packages/plugin/vite/package.json
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"
}
}
22 changes: 22 additions & 0 deletions packages/plugin/vite/src/Config.ts
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>;
}
122 changes: 122 additions & 0 deletions packages/plugin/vite/src/ViteConfig.ts
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 ?? {}
);
}
}
166 changes: 166 additions & 0 deletions packages/plugin/vite/src/VitePlugin.ts
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 };
Loading

0 comments on commit 6f68476

Please sign in to comment.