Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(plugin-webpack): support standalone preload entry points #2950

Merged
merged 11 commits into from
Oct 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
@@ -1,18 +1,7 @@
import { Configuration as RawWebpackConfiguration } from 'webpack';
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 @@ -24,34 +13,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 @@ -61,12 +77,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 @@ -80,14 +100,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 @@ -18,6 +18,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 @@ -297,11 +298,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 @@ -312,12 +313,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 @@ -331,12 +337,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 @@ -395,7 +397,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