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

build: compile 4 webpackRendererConfigs instead of N #3101

Merged
merged 22 commits into from
Aug 30, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/plugin/webpack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"main": "dist/WebpackPlugin.js",
"typings": "dist/WebpackPlugin.d.ts",
"scripts": {
"test": "xvfb-maybe mocha --config ../../../.mocharc.js test/**/*_spec.ts"
"test": "xvfb-maybe mocha --config ../../../.mocharc.js test/**/*_spec.ts test/*_spec.ts"
MarshallOfSound marked this conversation as resolved.
Show resolved Hide resolved
},
"devDependencies": {
"@malept/cross-spawn-promise": "^2.0.0",
Expand Down
264 changes: 163 additions & 101 deletions packages/plugin/webpack/src/WebpackConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { merge as webpackMerge } from 'webpack-merge';
import { WebpackPluginConfig, WebpackPluginEntryPoint, WebpackPluginEntryPointLocalWindow, WebpackPluginEntryPointPreloadOnly } from './Config';
import AssetRelocatorPatch from './util/AssetRelocatorPatch';
import processConfig from './util/processConfig';
import { isLocalWindow, isNoWindow, isPreloadOnly } from './util/rendererTypeUtils';
import { isLocalOrNoWindowEntries, isLocalWindow, isNoWindow, isPreloadOnly, isPreloadOnlyEntries } from './util/rendererTypeUtils';

type EntryType = string | string[] | Record<string, string | string[]>;
type WebpackMode = 'production' | 'development';
Expand All @@ -20,6 +20,35 @@ export type ConfigurationFactory = (
args: Record<string, unknown>
) => Configuration | Promise<Configuration>;

enum RendererTarget {
Web,
ElectronRenderer,
ElectronPreload,
SandboxedPreload,
}

enum WebpackTarget {
Web = 'web',
ElectronPreload = 'electron-preload',
ElectronRenderer = 'electron-renderer',
}

function isNotNull<T>(item: T | null): item is T {
return item !== null;
}

function rendererTargetToWebpackTarget(target: RendererTarget): WebpackTarget {
switch (target) {
case RendererTarget.Web:
case RendererTarget.SandboxedPreload:
return WebpackTarget.Web;
case RendererTarget.ElectronPreload:
return WebpackTarget.ElectronPreload;
case RendererTarget.ElectronRenderer:
return WebpackTarget.ElectronRenderer;
}
}

export default class WebpackConfigGenerator {
private isProd: boolean;

Expand Down Expand Up @@ -69,10 +98,6 @@ export default class WebpackConfigGenerator {
return this.isProd ? 'source-map' : 'eval-source-map';
}

rendererTarget(entryPoint: WebpackPluginEntryPoint): string {
return entryPoint.nodeIntegration ?? this.pluginConfig.renderer.nodeIntegration ? 'electron-renderer' : 'web';
}

rendererEntryPoint(entryPoint: WebpackPluginEntryPoint, inRendererDir: boolean, basename: string): string {
if (this.isProd) {
return `\`file://$\{require('path').resolve(__dirname, '..', '${inRendererDir ? 'renderer' : '.'}', '${entryPoint.name}', '${basename}')}\``;
Expand Down Expand Up @@ -163,114 +188,151 @@ export default class WebpackConfigGenerator {
);
}

async getPreloadConfigForEntryPoint(entryPoint: WebpackPluginEntryPointLocalWindow | WebpackPluginEntryPointPreloadOnly): Promise<Configuration> {
if (!entryPoint.preload) {
return {};
async getRendererConfig(entryPoints: WebpackPluginEntryPoint[]): Promise<Configuration[]> {
const entryPointsForTarget = {
web: [] as (WebpackPluginEntryPointLocalWindow | WebpackPluginEntryPoint)[],
electronRenderer: [] as (WebpackPluginEntryPointLocalWindow | WebpackPluginEntryPoint)[],
electronPreload: [] as WebpackPluginEntryPointPreloadOnly[],
sandboxedPreload: [] as WebpackPluginEntryPointPreloadOnly[],
};

for (const entry of entryPoints) {
const target = entry.nodeIntegration ?? this.pluginConfig.renderer.nodeIntegration ? 'electronRenderer' : 'web';
const preloadTarget = entry.nodeIntegration ?? this.pluginConfig.renderer.nodeIntegration ? 'electronPreload' : 'sandboxedPreload';

if (isPreloadOnly(entry)) {
entryPointsForTarget[preloadTarget].push(entry);
} else {
entryPointsForTarget[target].push(entry);
if (isLocalWindow(entry) && entry.preload) {
entryPointsForTarget[preloadTarget].push({ ...entry, preload: entry.preload });
}
}
}

const rendererConfig = await this.resolveConfig(entryPoint.preload.config || this.pluginConfig.renderer.config);
georgexu99 marked this conversation as resolved.
Show resolved Hide resolved
const prefixedEntries = entryPoint.prefixedEntries || [];
const rendererConfigs = await Promise.all(
[
await this.buildRendererConfigs(entryPointsForTarget.web, RendererTarget.Web),
await this.buildRendererConfigs(entryPointsForTarget.electronRenderer, RendererTarget.ElectronRenderer),
await this.buildRendererConfigs(entryPointsForTarget.electronPreload, RendererTarget.ElectronPreload),
await this.buildRendererConfigs(entryPointsForTarget.sandboxedPreload, RendererTarget.SandboxedPreload),
].reduce((configs, allConfigs) => allConfigs.concat(configs))
);

return webpackMerge(
{
devtool: this.rendererSourceMapOption,
mode: this.mode,
entry: prefixedEntries.concat([entryPoint.preload.js]),
output: {
path: path.resolve(this.webpackDir, 'renderer', entryPoint.name),
filename: 'preload.js',
},
node: {
__dirname: false,
__filename: false,
},
return rendererConfigs.filter(isNotNull);
}

buildRendererBaseConfig(target: RendererTarget): webpack.Configuration {
return {
target: rendererTargetToWebpackTarget(target),
devtool: this.rendererSourceMapOption,
mode: this.mode,
output: {
path: path.resolve(this.webpackDir, 'renderer'),
filename: '[name]/index.js',
globalObject: 'self',
...(this.isProd ? {} : { publicPath: '/' }),
},
rendererConfig || {},
{ target: 'electron-preload' }
);
node: {
__dirname: false,
__filename: false,
},
plugins: [new AssetRelocatorPatch(this.isProd, target === RendererTarget.ElectronRenderer || target === RendererTarget.ElectronPreload)],
};
}

async getRendererConfig(entryPoints: WebpackPluginEntryPoint[]): Promise<Configuration[]> {
async buildRendererConfigForWebOrRendererTarget(
entryPoints: WebpackPluginEntryPoint[],
target: RendererTarget.Web | RendererTarget.ElectronRenderer
): Promise<Configuration | null> {
if (!isLocalOrNoWindowEntries(entryPoints)) {
throw new Error('Invalid renderer entry point detected.');
}

const entry: webpack.Entry = {};
const baseConfig: webpack.Configuration = this.buildRendererBaseConfig(target);
const rendererConfig = await this.resolveConfig(this.pluginConfig.renderer.config);

return entryPoints.map((entryPoint) => {
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 AssetRelocatorPatch(this.isProd, !!this.pluginConfig.renderer.nodeIntegration)],
};
const output = {
path: path.resolve(this.webpackDir, 'renderer'),
filename: '[name]/index.js',
globalObject: 'self',
...(this.isProd ? {} : { publicPath: '/' }),
};
const plugins: webpack.WebpackPluginInstance[] = [];

for (const entryPoint of entryPoints) {
entry[entryPoint.name] = (entryPoint.prefixedEntries || []).concat([entryPoint.js]);

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,
],
},
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: '/' }),
},
},
rendererConfig || {}
plugins.push(
new HtmlWebpackPlugin({
title: entryPoint.name,
template: entryPoint.html,
filename: `${entryPoint.name}/index.html`,
chunks: [entryPoint.name].concat(entryPoint.additionalChunks || []),
}) as WebpackPluginInstance
);
} 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: '/' }),
},
},
rendererConfig || {}
);
} else {
}
}
return webpackMerge(baseConfig, rendererConfig || {}, { entry, output, plugins });
}

async buildRendererConfigForPreloadOrSandboxedPreloadTarget(
entryPoints: WebpackPluginEntryPointPreloadOnly[],
target: RendererTarget.ElectronPreload | RendererTarget.SandboxedPreload
): Promise<Configuration | null> {
if (entryPoints.length === 0) {
return null;
}
georgexu99 marked this conversation as resolved.
Show resolved Hide resolved

const externals = ['electron', 'electron/renderer', 'electron/common', 'events', 'timers', 'url'];

const entry: webpack.Entry = {};
const baseConfig: webpack.Configuration = this.buildRendererBaseConfig(target);
const rendererConfig = await this.resolveConfig(entryPoints[0].preload?.config || this.pluginConfig.renderer.config);

for (const entryPoint of entryPoints) {
entry[entryPoint.name] = (entryPoint.prefixedEntries || []).concat([entryPoint.preload.js]);
}
const config: Configuration = {
target: rendererTargetToWebpackTarget(target),
entry,
output: {
path: path.resolve(this.webpackDir, 'renderer'),
filename: '[name]/preload.js',
globalObject: 'self',
...(this.isProd ? {} : { publicPath: '/' }),
},
plugins: target === RendererTarget.ElectronPreload ? [] : [new webpack.ExternalsPlugin('commonjs2', externals)],
};
return webpackMerge(baseConfig, rendererConfig || {}, config);
}

async buildRendererConfigs(entryPoints: WebpackPluginEntryPoint[], target: RendererTarget): Promise<Promise<webpack.Configuration | null>[]> {
if (entryPoints.length === 0) {
return [];
}
const rendererConfigs = [];
if (target === RendererTarget.Web || target === RendererTarget.ElectronRenderer) {
rendererConfigs.push(this.buildRendererConfigForWebOrRendererTarget(entryPoints, target));
return rendererConfigs;
} else if (target === RendererTarget.ElectronPreload || target === RendererTarget.SandboxedPreload) {
if (!isPreloadOnlyEntries(entryPoints)) {
throw new Error('Invalid renderer entry point detected.');
}
});

const entryPointsWithPreloadConfig: WebpackPluginEntryPointPreloadOnly[] = [],
entryPointsWithoutPreloadConfig: WebpackPluginEntryPointPreloadOnly[] = [];
entryPoints.forEach((entryPoint) => (entryPoint.preload.config ? entryPointsWithPreloadConfig : entryPointsWithoutPreloadConfig).push(entryPoint));

rendererConfigs.push(this.buildRendererConfigForPreloadOrSandboxedPreloadTarget(entryPointsWithoutPreloadConfig, target));
entryPointsWithPreloadConfig.forEach((entryPoint) => {
rendererConfigs.push(this.buildRendererConfigForPreloadOrSandboxedPreloadTarget([entryPoint], target));
});
return rendererConfigs;
} else {
throw new Error('Invalid renderer entry point detected.');
}
}
}
49 changes: 1 addition & 48 deletions packages/plugin/webpack/src/WebpackPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { merge } from 'webpack-merge';
import { WebpackPluginConfig } from './Config';
import ElectronForgeLoggingPlugin from './util/ElectronForgeLogging';
import once from './util/once';
import { isLocalWindow, isPreloadOnly } from './util/rendererTypeUtils';
import WebpackConfigGenerator from './WebpackConfig';

const d = debug('electron-forge:plugin:webpack');
Expand Down Expand Up @@ -281,34 +280,17 @@ the generated files). Instead, it is ${JSON.stringify(pj.main)}`);
if (!watch && stats?.hasErrors()) {
throw new Error(`Compilation errors in the renderer: ${stats.toString()}`);
}

for (const entryPoint of this.config.renderer.entryPoints) {
if ((isLocalWindow(entryPoint) && !!entryPoint.preload) || isPreloadOnly(entryPoint)) {
const stats = await this.runWebpack(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
[await this.configGenerator.getPreloadConfigForEntryPoint(entryPoint)]
);

if (stats?.hasErrors()) {
throw new Error(`Compilation errors in the preload (${entryPoint.name}): ${stats.toString()}`);
}
}
}
};

launchRendererDevServers = async (logger: Logger): Promise<void> => {
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);
entryConfig.plugins.push(new ElectronForgeLoggingPlugin(logger.createTab(`Renderer Target Bundle (${entryConfig.target})`)));

entryConfig.infrastructureLogging = {
level: 'none',
Expand All @@ -320,35 +302,6 @@ the generated files). Instead, it is ${JSON.stringify(pj.main)}`);
const webpackDevServer = new WebpackDevServer(this.devServerOptions(), compiler);
await webpackDevServer.start();
this.servers.push(webpackDevServer.server!);

for (const entryPoint of this.config.renderer.entryPoints) {
if ((isLocalWindow(entryPoint) && !!entryPoint.preload) || isPreloadOnly(entryPoint)) {
const config = await this.configGenerator.getPreloadConfigForEntryPoint(entryPoint);
config.infrastructureLogging = {
level: 'none',
};
config.stats = 'none';
await new Promise((resolve, reject) => {
const tab = logger.createTab(`${entryPoint.name} - Preload`);
const [onceResolve, onceReject] = once(resolve, reject);

this.watchers.push(
webpack(config).watch({}, (err, stats) => {
if (stats) {
tab.log(
stats.toString({
colors: true,
})
);
}

if (err) return onceReject(err);
return onceResolve(undefined);
})
);
});
}
}
};

devServerOptions(): WebpackDevServer.Configuration {
Expand Down
Loading