Skip to content

Commit

Permalink
feat(plugin-webpack): support standalone preload entry points (#2950)
Browse files Browse the repository at this point in the history
  • Loading branch information
erickzhao authored Oct 24, 2022
1 parent 07d54a4 commit 93b31c7
Show file tree
Hide file tree
Showing 7 changed files with 242 additions and 125 deletions.
Binary file not shown.
80 changes: 50 additions & 30 deletions packages/plugin/webpack/src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,7 @@ import { Configuration as RawWebpackConfiguration } from 'webpack';
import WebpackDevServer from 'webpack-dev-server';
import { ConfigurationFactory as WebpackConfigurationFactory } from './WebpackConfig';

export interface WebpackPluginEntryPoint {
/**
* Relative or absolute path to the HTML template file for this entry point
*
* If this is a window, you MUST provide this. Only leave it unset for things
* like WebWorker scripts.
*/
html?: string;
/**
* Relative or absolute path to the main JS file for this entry point
*/
js: string;
export interface WebpackPluginEntryPointBase {
/**
* Human friendly name of your entry point
*/
Expand All @@ -25,34 +14,61 @@ export interface WebpackPluginEntryPoint {
*/
prefixedEntries?: string[];
/**
* Additional chunks to include in the outputted HTML file, use this if you
* set up some custom chunking. E.g. CommonChunksPlugin
* Additional chunks to include in the outputted HTML file. Use this if you
* set up some custom chunking (e.g. using SplitChunksPlugin).
*/
additionalChunks?: string[];
/**
* Information about the preload script for this entry point, if you don't use
* preload scripts you don't need to set this.
*/
preload?: WebpackPreloadEntryPoint;
/**
* Override the Webpack config for this renderer based on whether `nodeIntegration` for
* the `BrowserWindow` is enabled. Namely, for Webpack's `target` option:
* Override the webpack config for this renderer based on whether `nodeIntegration` for
* the `BrowserWindow` is enabled. For webpack's `target` option:
*
* * When `nodeIntegration` is true, the `target` is `electron-renderer`.
* * When `nodeIntegration` is false, the `target` is `web`.
*
* Unfortunately, we cannot derive the value from the main process code as it can be a
* dynamically generated value at runtime, and Webpack processes at build-time.
* Unfortunately, we cannot derive the value from the main process code as it can be
* dynamically generated at run-time, and webpack processes at build-time.
*
* Defaults to `false` (as it is disabled by default in Electron \>= 5) or the value set
* for all entries.
*/
nodeIntegration?: boolean;
}

export interface WebpackPluginEntryPointLocalWindow extends WebpackPluginEntryPointBase {
/**
* Relative or absolute path to the HTML template file for this entry point.
*/
html: string;
/**
* Relative or absolute path to the main JS file for this entry point.
*/
js: string;
/**
* Information about the preload script for this entry point. If you don't use
* preload scripts, you don't need to set this.
*/
preload?: WebpackPreloadEntryPoint;
}

export interface WebpackPluginEntryPointPreloadOnly extends WebpackPluginEntryPointBase {
/**
* Information about the preload script for this entry point.
*/
preload: WebpackPreloadEntryPoint;
}

export interface WebpackPluginEntryPointNoWindow extends WebpackPluginEntryPointBase {
/**
* Relative or absolute path to the main JS file for this entry point.
*/
js: string;
}

export type WebpackPluginEntryPoint = WebpackPluginEntryPointLocalWindow | WebpackPluginEntryPointNoWindow | WebpackPluginEntryPointPreloadOnly;

export interface WebpackPreloadEntryPoint {
/**
* Relative or absolute path to the preload JS file
* Relative or absolute path to the preload JS file.
*/
js: string;
/**
Expand All @@ -62,12 +78,16 @@ export interface WebpackPreloadEntryPoint {
*/
prefixedEntries?: string[];
/**
* The optional webpack config for your preload process, defaults to the
* renderer webpack config if blank
* The optional webpack config for your preload process.
* Defaults to the renderer webpack config if blank.
*/
config?: WebpackConfiguration | string;
}

export interface StandaloneWebpackPreloadEntryPoint extends WebpackPreloadEntryPoint {
name: string;
}

export interface WebpackPluginRendererConfig {
/**
* The webpack config for your renderer process
Expand All @@ -81,14 +101,14 @@ export interface WebpackPluginRendererConfig {
*/
jsonStats?: boolean;
/**
* Adjusts the Webpack config for all renderer entries based on whether `nodeIntegration`
* for the `BrowserWindow` is enabled. Namely, for Webpack's `target` option:
* Override the webpack config for this renderer based on whether `nodeIntegration` for
* the `BrowserWindow` is enabled. For webpack's `target` option:
*
* * When `nodeIntegration` is true, the `target` is `electron-renderer`.
* * When `nodeIntegration` is false, the `target` is `web`.
*
* Unfortunately, we cannot derive the value from the main process code as it can be a
* dynamically generated value at runtime, and Webpack processes at build-time.
* Unfortunately, we cannot derive the value from the main process code as it can be
* dynamically generated at run-time, and webpack processes at build-time.
*
* Defaults to `false` (as it is disabled by default in Electron \>= 5).
*/
Expand Down
135 changes: 91 additions & 44 deletions packages/plugin/webpack/src/WebpackConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import HtmlWebpackPlugin from 'html-webpack-plugin';
import path from 'path';
import webpack, { Configuration, WebpackPluginInstance } from 'webpack';
import { merge as webpackMerge } from 'webpack-merge';
import { WebpackPluginConfig, WebpackPluginEntryPoint, WebpackPreloadEntryPoint } from './Config';
import { WebpackPluginConfig, WebpackPluginEntryPoint, WebpackPluginEntryPointLocalWindow, WebpackPluginEntryPointPreloadOnly } from './Config';
import AssetRelocatorPatch from './util/AssetRelocatorPatch';
import processConfig from './util/processConfig';
import { isLocalWindow, isNoWindow, isPreloadOnly } from './util/rendererTypeUtils';

type EntryType = string | string[] | Record<string, string | string[]>;
type WebpackMode = 'production' | 'development';
Expand Down Expand Up @@ -87,15 +88,16 @@ export default class WebpackConfigGenerator {
}

getPreloadDefine(entryPoint: WebpackPluginEntryPoint): string {
if (entryPoint.preload) {
if (!isNoWindow(entryPoint)) {
if (this.isProd) {
return `require('path').resolve(__dirname, '../renderer', '${entryPoint.name}', 'preload.js')`;
}
return `'${path.resolve(this.webpackDir, 'renderer', entryPoint.name, 'preload.js').replace(/\\/g, '\\\\')}'`;
} else {
// If this entry-point has no configured preload script just map this constant to `undefined`
// so that any code using it still works. This makes quick-start / docs simpler.
return 'undefined';
}
// If this entry-point has no configured preload script just map this constant to `undefined`
// so that any code using it still works. This makes quick-start / docs simpler.
return 'undefined';
}

getDefines(inRendererDir = true): Record<string, string> {
Expand All @@ -105,7 +107,7 @@ export default class WebpackConfigGenerator {
}
for (const entryPoint of this.pluginConfig.renderer.entryPoints) {
const entryKey = this.toEnvironmentVariable(entryPoint);
if (entryPoint.html) {
if (isLocalWindow(entryPoint)) {
defines[entryKey] = this.rendererEntryPoint(entryPoint, inRendererDir, 'index.html');
} else {
defines[entryKey] = this.rendererEntryPoint(entryPoint, inRendererDir, 'index.js');
Expand All @@ -116,6 +118,7 @@ export default class WebpackConfigGenerator {
defines[preloadDefineKey] = this.getPreloadDefine(entryPoint);
defines[`process.env.${preloadDefineKey}`] = defines[preloadDefineKey];
}

return defines;
}

Expand Down Expand Up @@ -158,17 +161,21 @@ export default class WebpackConfigGenerator {
);
}

async getPreloadRendererConfig(parentPoint: WebpackPluginEntryPoint, entryPoint: WebpackPreloadEntryPoint): Promise<Configuration> {
const rendererConfig = await this.resolveConfig(entryPoint.config || this.pluginConfig.renderer.config);
async getPreloadConfigForEntryPoint(entryPoint: WebpackPluginEntryPointLocalWindow | WebpackPluginEntryPointPreloadOnly): Promise<Configuration> {
if (!entryPoint.preload) {
return {};
}

const rendererConfig = await this.resolveConfig(entryPoint.preload.config || this.pluginConfig.renderer.config);
const prefixedEntries = entryPoint.prefixedEntries || [];

return webpackMerge(
{
devtool: this.rendererSourceMapOption,
mode: this.mode,
entry: prefixedEntries.concat([entryPoint.js]),
entry: prefixedEntries.concat([entryPoint.preload.js]),
output: {
path: path.resolve(this.webpackDir, 'renderer', parentPoint.name),
path: path.resolve(this.webpackDir, 'renderer', entryPoint.name),
filename: 'preload.js',
},
node: {
Expand All @@ -186,43 +193,83 @@ export default class WebpackConfigGenerator {
const defines = this.getDefines(false);

return entryPoints.map((entryPoint) => {
const config = webpackMerge(
{
entry: {
[entryPoint.name]: (entryPoint.prefixedEntries || []).concat([entryPoint.js]),
const baseConfig: webpack.Configuration = {
target: this.rendererTarget(entryPoint),
devtool: this.rendererSourceMapOption,
mode: this.mode,
output: {
path: path.resolve(this.webpackDir, 'renderer'),
filename: '[name]/index.js',
globalObject: 'self',
...(this.isProd ? {} : { publicPath: '/' }),
},
node: {
__dirname: false,
__filename: false,
},
plugins: [new webpack.DefinePlugin(defines), new AssetRelocatorPatch(this.isProd, !!this.pluginConfig.renderer.nodeIntegration)],
};

if (isLocalWindow(entryPoint)) {
return webpackMerge(
baseConfig,
{
entry: {
[entryPoint.name]: (entryPoint.prefixedEntries || []).concat([entryPoint.js]),
},
output: {
path: path.resolve(this.webpackDir, 'renderer'),
filename: '[name]/index.js',
globalObject: 'self',
...(this.isProd ? {} : { publicPath: '/' }),
},
plugins: [
new HtmlWebpackPlugin({
title: entryPoint.name,
template: entryPoint.html,
filename: `${entryPoint.name}/index.html`,
chunks: [entryPoint.name].concat(entryPoint.additionalChunks || []),
}) as WebpackPluginInstance,
],
},
target: this.rendererTarget(entryPoint),
devtool: this.rendererSourceMapOption,
mode: this.mode,
output: {
path: path.resolve(this.webpackDir, 'renderer'),
filename: '[name]/index.js',
globalObject: 'self',
...(this.isProd ? {} : { publicPath: '/' }),
rendererConfig || {}
);
} else if (isNoWindow(entryPoint)) {
return webpackMerge(
baseConfig,
{
entry: {
[entryPoint.name]: (entryPoint.prefixedEntries || []).concat([entryPoint.js]),
},
output: {
path: path.resolve(this.webpackDir, 'renderer'),
filename: '[name]/index.js',
globalObject: 'self',
...(this.isProd ? {} : { publicPath: '/' }),
},
},
node: {
__dirname: false,
__filename: false,
rendererConfig || {}
);
} else if (isPreloadOnly(entryPoint)) {
return webpackMerge(
baseConfig,
{
target: 'electron-preload',
entry: {
[entryPoint.name]: (entryPoint.prefixedEntries || []).concat([entryPoint.preload.js]),
},
output: {
path: path.resolve(this.webpackDir, 'renderer'),
filename: 'preload.js',
globalObject: 'self',
...(this.isProd ? {} : { publicPath: '/' }),
},
},
plugins: [
...(entryPoint.html
? [
new HtmlWebpackPlugin({
title: entryPoint.name,
template: entryPoint.html,
filename: `${entryPoint.name}/index.html`,
chunks: [entryPoint.name].concat(entryPoint.additionalChunks || []),
}) as WebpackPluginInstance,
]
: []),
new webpack.DefinePlugin(defines),
new AssetRelocatorPatch(this.isProd, !!this.pluginConfig.renderer.nodeIntegration),
],
},
rendererConfig || {}
);

return config;
rendererConfig || {}
);
} else {
throw new Error('Invalid renderer entry point detected.');
}
});
}
}
26 changes: 14 additions & 12 deletions packages/plugin/webpack/src/WebpackPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { WebpackPluginConfig } from './Config';
import ElectronForgeLoggingPlugin from './util/ElectronForgeLogging';
import once from './util/once';
import WebpackConfigGenerator from './WebpackConfig';
import { isLocalWindow, isPreloadOnly } from './util/rendererTypeUtils';

const d = debug('electron-forge:plugin:webpack');
const DEFAULT_PORT = 3000;
Expand Down Expand Up @@ -292,11 +293,11 @@ the generated files). Instead, it is ${JSON.stringify(pj.main)}`);
});

for (const entryPoint of this.config.renderer.entryPoints) {
if (entryPoint.preload) {
await asyncOra(`Compiling Renderer Preload: ${entryPoint.name}`, async () => {
if ((isLocalWindow(entryPoint) && !!entryPoint.preload) || isPreloadOnly(entryPoint)) {
await asyncOra(`Compiling Renderer Preload: ${chalk.cyan(entryPoint.name)}`, async () => {
const stats = await this.runWebpack(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
[await this.configGenerator.getPreloadRendererConfig(entryPoint, entryPoint.preload!)]
[await this.configGenerator.getPreloadConfigForEntryPoint(entryPoint)]
);

if (stats?.hasErrors()) {
Expand All @@ -307,12 +308,17 @@ the generated files). Instead, it is ${JSON.stringify(pj.main)}`);
}
};

launchDevServers = async (logger: Logger): Promise<void> => {
await asyncOra('Launch Dev Servers', async () => {
launchRendererDevServers = async (logger: Logger): Promise<void> => {
await asyncOra('Launching Dev Servers for Renderer Process Code', async () => {
const tab = logger.createTab('Renderers');
const pluginLogs = new ElectronForgeLoggingPlugin(tab);

const config = await this.configGenerator.getRendererConfig(this.config.renderer.entryPoints);

if (config.length === 0) {
return;
}

for (const entryConfig of config) {
if (!entryConfig.plugins) entryConfig.plugins = [];
entryConfig.plugins.push(pluginLogs);
Expand All @@ -326,12 +332,8 @@ the generated files). Instead, it is ${JSON.stringify(pj.main)}`);

await asyncOra('Compiling Preload Scripts', async () => {
for (const entryPoint of this.config.renderer.entryPoints) {
if (entryPoint.preload) {
const config = await this.configGenerator.getPreloadRendererConfig(
entryPoint,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
entryPoint.preload!
);
if ((isLocalWindow(entryPoint) && !!entryPoint.preload) || isPreloadOnly(entryPoint)) {
const config = await this.configGenerator.getPreloadConfigForEntryPoint(entryPoint);
await new Promise((resolve, reject) => {
const tab = logger.createTab(`${entryPoint.name} - Preload`);
const [onceResolve, onceReject] = once(resolve, reject);
Expand Down Expand Up @@ -390,7 +392,7 @@ the generated files). Instead, it is ${JSON.stringify(pj.main)}`);
const logger = new Logger(this.loggerPort);
this.loggers.push(logger);
await this.compileMain(true, logger);
await this.launchDevServers(logger);
await this.launchRendererDevServers(logger);
await logger.start();
return false;
}
Expand Down
Loading

0 comments on commit 93b31c7

Please sign in to comment.