Skip to content

Commit

Permalink
feat: add support for sequential groups of webpack builds (#3435)
Browse files Browse the repository at this point in the history
* feat: add support for sequential groups of webpack builds

* fix: use webpack multi-compiler dependency specifications to use a single dev server
  • Loading branch information
MarshallOfSound authored Nov 30, 2023
1 parent f3cd9c6 commit cf760c9
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 69 deletions.
8 changes: 7 additions & 1 deletion packages/plugin/webpack/src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,14 @@ export interface WebpackPluginConfig {
jsonStats?: boolean;
/**
* Electron Forge webpack configuration for your renderer process
*
* If this property is configured as an array each group of entryPoints is built sequentially
* such that later indexed renderer configurations can depend on the output of previous ones.
*
* If you want to build multiple targets in parallel please specify multiple entryPoints in a
* single renderer configuration. Most usecases should not set this to an array.
*/
renderer: WebpackPluginRendererConfig;
renderer: WebpackPluginRendererConfig | WebpackPluginRendererConfig[];
/**
* The TCP port for the dev servers. Defaults to 3000.
*/
Expand Down
81 changes: 50 additions & 31 deletions packages/plugin/webpack/src/WebpackConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@ import HtmlWebpackPlugin from 'html-webpack-plugin';
import webpack, { Configuration, WebpackPluginInstance } from 'webpack';
import { merge as webpackMerge } from 'webpack-merge';

import { WebpackPluginConfig, WebpackPluginEntryPoint, WebpackPluginEntryPointLocalWindow, WebpackPluginEntryPointPreloadOnly } from './Config';
import {
WebpackPluginConfig,
WebpackPluginEntryPoint,
WebpackPluginEntryPointLocalWindow,
WebpackPluginEntryPointPreloadOnly,
WebpackPluginRendererConfig,
} from './Config';
import AssetRelocatorPatch from './util/AssetRelocatorPatch';
import processConfig from './util/processConfig';
import { isLocalOrNoWindowEntries, isLocalWindow, isNoWindow, isPreloadOnly, isPreloadOnlyEntries } from './util/rendererTypeUtils';
Expand Down Expand Up @@ -104,9 +110,9 @@ export default class WebpackConfigGenerator {
return this.isProd ? 'source-map' : 'eval-source-map';
}

rendererEntryPoint(entryPoint: WebpackPluginEntryPoint, inRendererDir: boolean, basename: string): string {
rendererEntryPoint(entryPoint: WebpackPluginEntryPoint, basename: string): string {
if (this.isProd) {
return `\`file://$\{require('path').resolve(__dirname, '..', '${inRendererDir ? 'renderer' : '.'}', '${entryPoint.name}', '${basename}')}\``;
return `\`file://$\{require('path').resolve(__dirname, '..', 'renderer', '${entryPoint.name}', '${basename}')}\``;
}
const baseUrl = `http://localhost:${this.port}/${entryPoint.name}`;
if (basename !== 'index.html') {
Expand All @@ -133,23 +139,30 @@ export default class WebpackConfigGenerator {
}
}

getDefines(inRendererDir = true): Record<string, string> {
private get allPluginRendererOptions() {
return Array.isArray(this.pluginConfig.renderer) ? this.pluginConfig.renderer : [this.pluginConfig.renderer];
}

getDefines(): Record<string, string> {
const defines: Record<string, string> = {};
if (!this.pluginConfig.renderer.entryPoints || !Array.isArray(this.pluginConfig.renderer.entryPoints)) {
throw new Error('Required config option "renderer.entryPoints" has not been defined');
}
for (const entryPoint of this.pluginConfig.renderer.entryPoints) {
const entryKey = this.toEnvironmentVariable(entryPoint);
if (isLocalWindow(entryPoint)) {
defines[entryKey] = this.rendererEntryPoint(entryPoint, inRendererDir, 'index.html');
} else {
defines[entryKey] = this.rendererEntryPoint(entryPoint, inRendererDir, 'index.js');

for (const pluginRendererOptions of this.allPluginRendererOptions) {
if (!pluginRendererOptions.entryPoints || !Array.isArray(pluginRendererOptions.entryPoints)) {
throw new Error('Required config option "renderer.entryPoints" has not been defined');
}
defines[`process.env.${entryKey}`] = defines[entryKey];
for (const entryPoint of pluginRendererOptions.entryPoints) {
const entryKey = this.toEnvironmentVariable(entryPoint);
if (isLocalWindow(entryPoint)) {
defines[entryKey] = this.rendererEntryPoint(entryPoint, 'index.html');
} else {
defines[entryKey] = this.rendererEntryPoint(entryPoint, 'index.js');
}
defines[`process.env.${entryKey}`] = defines[entryKey];

const preloadDefineKey = this.toEnvironmentVariable(entryPoint, true);
defines[preloadDefineKey] = this.getPreloadDefine(entryPoint);
defines[`process.env.${preloadDefineKey}`] = defines[preloadDefineKey];
const preloadDefineKey = this.toEnvironmentVariable(entryPoint, true);
defines[preloadDefineKey] = this.getPreloadDefine(entryPoint);
defines[`process.env.${preloadDefineKey}`] = defines[preloadDefineKey];
}
}

return defines;
Expand Down Expand Up @@ -194,17 +207,17 @@ export default class WebpackConfigGenerator {
);
}

async getRendererConfig(entryPoints: WebpackPluginEntryPoint[]): Promise<Configuration[]> {
async getRendererConfig(rendererOptions: WebpackPluginRendererConfig): 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';
for (const entry of rendererOptions.entryPoints) {
const target = entry.nodeIntegration ?? rendererOptions.nodeIntegration ? 'electronRenderer' : 'web';
const preloadTarget = entry.nodeIntegration ?? rendererOptions.nodeIntegration ? 'electronPreload' : 'sandboxedPreload';

if (isPreloadOnly(entry)) {
entryPointsForTarget[preloadTarget].push(entry);
Expand All @@ -218,10 +231,10 @@ export default class WebpackConfigGenerator {

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),
await this.buildRendererConfigs(rendererOptions, entryPointsForTarget.web, RendererTarget.Web),
await this.buildRendererConfigs(rendererOptions, entryPointsForTarget.electronRenderer, RendererTarget.ElectronRenderer),
await this.buildRendererConfigs(rendererOptions, entryPointsForTarget.electronPreload, RendererTarget.ElectronPreload),
await this.buildRendererConfigs(rendererOptions, entryPointsForTarget.sandboxedPreload, RendererTarget.SandboxedPreload),
].reduce((configs, allConfigs) => allConfigs.concat(configs))
);

Expand All @@ -248,6 +261,7 @@ export default class WebpackConfigGenerator {
}

async buildRendererConfigForWebOrRendererTarget(
rendererOptions: WebpackPluginRendererConfig,
entryPoints: WebpackPluginEntryPoint[],
target: RendererTarget.Web | RendererTarget.ElectronRenderer
): Promise<Configuration | null> {
Expand All @@ -257,7 +271,7 @@ export default class WebpackConfigGenerator {

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

const output = {
path: path.resolve(this.webpackDir, 'renderer'),
Expand Down Expand Up @@ -285,6 +299,7 @@ export default class WebpackConfigGenerator {
}

async buildRendererConfigForPreloadOrSandboxedPreloadTarget(
rendererOptions: WebpackPluginRendererConfig,
entryPoints: WebpackPluginEntryPointPreloadOnly[],
target: RendererTarget.ElectronPreload | RendererTarget.SandboxedPreload
): Promise<Configuration | null> {
Expand All @@ -296,7 +311,7 @@ export default class WebpackConfigGenerator {

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

for (const entryPoint of entryPoints) {
entry[entryPoint.name] = (entryPoint.prefixedEntries || []).concat([entryPoint.preload.js]);
Expand All @@ -315,13 +330,17 @@ export default class WebpackConfigGenerator {
return webpackMerge(baseConfig, rendererConfig || {}, config);
}

async buildRendererConfigs(entryPoints: WebpackPluginEntryPoint[], target: RendererTarget): Promise<Promise<webpack.Configuration | null>[]> {
async buildRendererConfigs(
rendererOptions: WebpackPluginRendererConfig,
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));
rendererConfigs.push(this.buildRendererConfigForWebOrRendererTarget(rendererOptions, entryPoints, target));
return rendererConfigs;
} else if (target === RendererTarget.ElectronPreload || target === RendererTarget.SandboxedPreload) {
if (!isPreloadOnlyEntries(entryPoints)) {
Expand All @@ -332,9 +351,9 @@ export default class WebpackConfigGenerator {
entryPointsWithoutPreloadConfig: WebpackPluginEntryPointPreloadOnly[] = [];
entryPoints.forEach((entryPoint) => (entryPoint.preload.config ? entryPointsWithPreloadConfig : entryPointsWithoutPreloadConfig).push(entryPoint));

rendererConfigs.push(this.buildRendererConfigForPreloadOrSandboxedPreloadTarget(entryPointsWithoutPreloadConfig, target));
rendererConfigs.push(this.buildRendererConfigForPreloadOrSandboxedPreloadTarget(rendererOptions, entryPointsWithoutPreloadConfig, target));
entryPointsWithPreloadConfig.forEach((entryPoint) => {
rendererConfigs.push(this.buildRendererConfigForPreloadOrSandboxedPreloadTarget([entryPoint], target));
rendererConfigs.push(this.buildRendererConfigForPreloadOrSandboxedPreloadTarget(rendererOptions, [entryPoint], target));
});
return rendererConfigs;
} else {
Expand Down
36 changes: 27 additions & 9 deletions packages/plugin/webpack/src/WebpackPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import webpack, { Configuration, Watching } from 'webpack';
import WebpackDevServer from 'webpack-dev-server';
import { merge } from 'webpack-merge';

import { WebpackPluginConfig } from './Config';
import { WebpackPluginConfig, WebpackPluginRendererConfig } from './Config';
import ElectronForgeLoggingPlugin from './util/ElectronForgeLogging';
import EntryPointPreloadPlugin from './util/EntryPointPreloadPlugin';
import once from './util/once';
Expand Down Expand Up @@ -111,12 +111,12 @@ export default class WebpackPlugin extends PluginBase<WebpackPluginConfig> {
await fs.writeJson(jsonStatsFilename, jsonStats, { spaces: 2 });
}

private runWebpack = async (options: Configuration[], isRenderer = false): Promise<webpack.MultiStats | undefined> =>
private runWebpack = async (options: Configuration[], rendererOptions: WebpackPluginRendererConfig | null): Promise<webpack.MultiStats | undefined> =>
new Promise((resolve, reject) => {
webpack(options).run(async (err, stats) => {
if (isRenderer && this.config.renderer.jsonStats) {
if (rendererOptions && rendererOptions.jsonStats) {
for (const [index, entryStats] of (stats?.stats ?? []).entries()) {
const name = this.config.renderer.entryPoints[index].name;
const name = rendererOptions.entryPoints[index].name;
await this.writeJSONStats('renderer', entryStats, options[index].stats as WebpackToJsonOptions, name);
}
}
Expand Down Expand Up @@ -263,7 +263,7 @@ Your packaged app may be larger than expected if you dont ignore everything othe
return true;
}

if (this.config.renderer.jsonStats && file.endsWith(path.join('.webpack', 'renderer', 'stats.json'))) {
if (this.allRendererOptions.some((r) => r.jsonStats) && file.endsWith(path.join('.webpack', 'renderer', 'stats.json'))) {
return true;
}

Expand All @@ -276,6 +276,10 @@ Your packaged app may be larger than expected if you dont ignore everything othe
return forgeConfig;
};

private get allRendererOptions() {
return Array.isArray(this.config.renderer) ? this.config.renderer : [this.config.renderer];
}

packageAfterCopy = async (_forgeConfig: ResolvedForgeConfig, buildPath: string): Promise<void> => {
const pj = await fs.readJson(path.resolve(this.projectDir, 'package.json'));

Expand Down Expand Up @@ -334,14 +338,28 @@ the generated files). Instead, it is ${JSON.stringify(pj.main)}`);
};

compileRenderers = async (watch = false): Promise<void> => {
const stats = await this.runWebpack(await this.configGenerator.getRendererConfig(this.config.renderer.entryPoints), true);
if (!watch && stats?.hasErrors()) {
throw new Error(`Compilation errors in the renderer: ${stats.toString()}`);
for (const rendererOptions of this.allRendererOptions) {
const stats = await this.runWebpack(await this.configGenerator.getRendererConfig(rendererOptions), rendererOptions);
if (!watch && stats?.hasErrors()) {
throw new Error(`Compilation errors in the renderer: ${stats.toString()}`);
}
}
};

launchRendererDevServers = async (logger: Logger): Promise<void> => {
const configs = await this.configGenerator.getRendererConfig(this.config.renderer.entryPoints);
const configs: Configuration[] = [];
const rollingDependencies: string[] = [];
for (const [i, rendererOptions] of this.allRendererOptions.entries()) {
const groupName = `group_${i}`;
configs.push(
...(await this.configGenerator.getRendererConfig(rendererOptions)).map((config) => ({
...config,
name: groupName,
dependencies: rollingDependencies,
}))
);
}

if (configs.length === 0) {
return;
}
Expand Down
22 changes: 15 additions & 7 deletions packages/plugin/webpack/test/AssetRelocatorPatch_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ async function yarnStart(): Promise<string> {
});
}

const safeFirstRendererConfig = (renderer: WebpackPluginConfig['renderer']) => {
if (Array.isArray(renderer)) return renderer[0];
return renderer;
};

describe('AssetRelocatorPatch', () => {
const rendererOut = path.join(appPath, '.webpack/renderer');
const mainOut = path.join(appPath, '.webpack/main');
Expand Down Expand Up @@ -145,7 +150,7 @@ describe('AssetRelocatorPatch', () => {
});

it('builds preload', async () => {
const preloadConfig = await generator.getRendererConfig(config.renderer.entryPoints);
const preloadConfig = await generator.getRendererConfig(safeFirstRendererConfig(config.renderer));
await asyncWebpack(preloadConfig[0]);

await expectOutputFileToHaveTheCorrectNativeModulePath({
Expand All @@ -157,7 +162,7 @@ describe('AssetRelocatorPatch', () => {
});

it('builds renderer', async () => {
const rendererConfig = await generator.getRendererConfig(config.renderer.entryPoints);
const rendererConfig = await generator.getRendererConfig(safeFirstRendererConfig(config.renderer));
for (const rendererEntryConfig of rendererConfig) {
await asyncWebpack(rendererEntryConfig);
}
Expand Down Expand Up @@ -197,8 +202,11 @@ describe('AssetRelocatorPatch', () => {
});

it('builds preload', async () => {
const entryPoint = config.renderer.entryPoints[0] as WebpackPluginEntryPointLocalWindow;
const preloadConfig = await generator.getRendererConfig([entryPoint]);
const entryPoint = safeFirstRendererConfig(config.renderer).entryPoints[0] as WebpackPluginEntryPointLocalWindow;
const preloadConfig = await generator.getRendererConfig({
...safeFirstRendererConfig(config.renderer),
entryPoints: [entryPoint],
});
await asyncWebpack(preloadConfig[0]);

await expectOutputFileToHaveTheCorrectNativeModulePath({
Expand All @@ -210,7 +218,7 @@ describe('AssetRelocatorPatch', () => {
});

it('builds renderer', async () => {
const rendererConfig = await generator.getRendererConfig(config.renderer.entryPoints);
const rendererConfig = await generator.getRendererConfig(safeFirstRendererConfig(config.renderer));
for (const rendererEntryConfig of rendererConfig) {
await asyncWebpack(rendererEntryConfig);
}
Expand All @@ -232,10 +240,10 @@ describe('AssetRelocatorPatch', () => {
});

it('builds renderer with nodeIntegration = false', async () => {
config.renderer.nodeIntegration = false;
safeFirstRendererConfig(config.renderer).nodeIntegration = false;
generator = new WebpackConfigGenerator(config, appPath, true, 3000);

const rendererConfig = await generator.getRendererConfig(config.renderer.entryPoints);
const rendererConfig = await generator.getRendererConfig(safeFirstRendererConfig(config.renderer));
for (const rendererEntryConfig of rendererConfig) {
await asyncWebpack(rendererEntryConfig);
}
Expand Down
Loading

0 comments on commit cf760c9

Please sign in to comment.