diff --git a/packages/plugin/webpack/src/Config.ts b/packages/plugin/webpack/src/Config.ts index cf0646d006..635001fed7 100644 --- a/packages/plugin/webpack/src/Config.ts +++ b/packages/plugin/webpack/src/Config.ts @@ -1,4 +1,5 @@ -import { Configuration as WebpackConfiguration } from 'webpack'; +import { Configuration as RawWebpackConfiguration } from 'webpack'; +import { ConfigurationFactory as WebpackConfigurationFactory } from './WebpackConfig'; export interface WebpackPluginEntryPoint { /** @@ -139,3 +140,5 @@ export interface WebpackPluginConfig { devServer?: Record; // TODO: use webpack-dev-server.Configuration when @types/webpack-dev-server upgrades to v4 } + +export type WebpackConfiguration = RawWebpackConfiguration | WebpackConfigurationFactory; diff --git a/packages/plugin/webpack/src/WebpackConfig.ts b/packages/plugin/webpack/src/WebpackConfig.ts index 479a7bbda8..9343e2bcb1 100644 --- a/packages/plugin/webpack/src/WebpackConfig.ts +++ b/packages/plugin/webpack/src/WebpackConfig.ts @@ -5,12 +5,18 @@ import webpack, { Configuration, WebpackPluginInstance } from 'webpack'; import { merge as webpackMerge } from 'webpack-merge'; import { WebpackPluginConfig, WebpackPluginEntryPoint, WebpackPreloadEntryPoint } from './Config'; import AssetRelocatorPatch from './util/AssetRelocatorPatch'; +import processConfig from './util/processConfig'; type EntryType = string | string[] | Record; type WebpackMode = 'production' | 'development'; const d = debug('electron-forge:plugin:webpack:webpackconfig'); +export type ConfigurationFactory = ( + env: string | Record | unknown, + args: Record +) => Configuration | Promise; + export default class WebpackConfigGenerator { private isProd: boolean; @@ -32,15 +38,26 @@ export default class WebpackConfigGenerator { d('Config mode:', this.mode); } - resolveConfig(config: Configuration | string): Configuration { - if (typeof config === 'string') { - // eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-dynamic-require, global-require - return require(path.resolve(this.projectDir, config)) as Configuration; - } + async resolveConfig(config: Configuration | ConfigurationFactory | string): Promise { + const rawConfig = + typeof config === 'string' + ? // eslint-disable-next-line import/no-dynamic-require, global-require, @typescript-eslint/no-var-requires + (require(path.resolve(this.projectDir, config)) as Configuration | ConfigurationFactory) + : config; - return config; + return processConfig(this.preprocessConfig, rawConfig); } + // Users can override this method in a subclass to provide custom logic or + // configuration parameters. + preprocessConfig = async (config: ConfigurationFactory): Promise => + config( + {}, + { + mode: this.mode, + } + ); + get mode(): WebpackMode { return this.isProd ? 'production' : 'development'; } @@ -102,8 +119,8 @@ export default class WebpackConfigGenerator { return defines; } - getMainConfig(): Configuration { - const mainConfig = this.resolveConfig(this.pluginConfig.mainConfig); + async getMainConfig(): Promise { + const mainConfig = await this.resolveConfig(this.pluginConfig.mainConfig); if (!mainConfig.entry) { throw new Error('Required option "mainConfig.entry" has not been defined'); @@ -142,7 +159,7 @@ export default class WebpackConfigGenerator { } async getPreloadRendererConfig(parentPoint: WebpackPluginEntryPoint, entryPoint: WebpackPreloadEntryPoint): Promise { - const rendererConfig = this.resolveConfig(entryPoint.config || this.pluginConfig.renderer.config); + const rendererConfig = await this.resolveConfig(entryPoint.config || this.pluginConfig.renderer.config); const prefixedEntries = entryPoint.prefixedEntries || []; return webpackMerge( @@ -165,7 +182,7 @@ export default class WebpackConfigGenerator { } async getRendererConfig(entryPoints: WebpackPluginEntryPoint[]): Promise { - const rendererConfig = this.resolveConfig(this.pluginConfig.renderer.config); + const rendererConfig = await this.resolveConfig(this.pluginConfig.renderer.config); const entry: webpack.Entry = {}; for (const entryPoint of entryPoints) { const prefixedEntries = entryPoint.prefixedEntries || []; diff --git a/packages/plugin/webpack/src/WebpackPlugin.ts b/packages/plugin/webpack/src/WebpackPlugin.ts index 0553cbe817..5c227249af 100644 --- a/packages/plugin/webpack/src/WebpackPlugin.ts +++ b/packages/plugin/webpack/src/WebpackPlugin.ts @@ -253,7 +253,7 @@ the generated files). Instead, it is ${JSON.stringify(pj.main)}`); tab = logger.createTab('Main Process'); } await asyncOra('Compiling Main Process Code', async () => { - const mainConfig = this.configGenerator.getMainConfig(); + const mainConfig = await this.configGenerator.getMainConfig(); await new Promise((resolve, reject) => { const compiler = webpack(mainConfig); const [onceResolve, onceReject] = once(resolve, reject); diff --git a/packages/plugin/webpack/src/util/processConfig.ts b/packages/plugin/webpack/src/util/processConfig.ts new file mode 100644 index 0000000000..94577c3ae7 --- /dev/null +++ b/packages/plugin/webpack/src/util/processConfig.ts @@ -0,0 +1,18 @@ +import { Configuration } from 'webpack'; +import { ConfigurationFactory } from '../WebpackConfig'; + +const trivialConfigurationFactory = + (config: Configuration): ConfigurationFactory => + () => + config; + +export type ConfigProcessor = (config: ConfigurationFactory) => Promise; + +// Ensure processing logic is run for both `Configuration` and +// `ConfigurationFactory` config variants. +const processConfig = async (processor: ConfigProcessor, config: Configuration | ConfigurationFactory): Promise => { + const configFactory = typeof config === 'function' ? config : trivialConfigurationFactory(config); + return processor(configFactory); +}; + +export default processConfig; diff --git a/packages/plugin/webpack/test/AssetRelocatorPatch_spec.ts b/packages/plugin/webpack/test/AssetRelocatorPatch_spec.ts index f7e2a7d14e..e4d69bb900 100644 --- a/packages/plugin/webpack/test/AssetRelocatorPatch_spec.ts +++ b/packages/plugin/webpack/test/AssetRelocatorPatch_spec.ts @@ -132,7 +132,7 @@ describe('AssetRelocatorPatch', () => { const generator = new WebpackConfigGenerator(config, appPath, false, 3000); it('builds main', async () => { - await asyncWebpack(generator.getMainConfig()); + await asyncWebpack(await generator.getMainConfig()); await expectOutputFileToHaveTheCorrectNativeModulePath({ outDir: mainOut, @@ -186,7 +186,7 @@ describe('AssetRelocatorPatch', () => { let generator = new WebpackConfigGenerator(config, appPath, true, 3000); it('builds main', async () => { - const mainConfig = generator.getMainConfig(); + const mainConfig = await generator.getMainConfig(); await asyncWebpack(mainConfig); await expectOutputFileToHaveTheCorrectNativeModulePath({ diff --git a/packages/plugin/webpack/test/WebpackConfig_spec.ts b/packages/plugin/webpack/test/WebpackConfig_spec.ts index 720fef608a..e7dc7a5d05 100644 --- a/packages/plugin/webpack/test/WebpackConfig_spec.ts +++ b/packages/plugin/webpack/test/WebpackConfig_spec.ts @@ -1,9 +1,9 @@ -import { Compiler, Entry, WebpackPluginInstance } from 'webpack'; +import { Compiler, Entry, WebpackPluginInstance, Configuration } from 'webpack'; import { expect } from 'chai'; import path from 'path'; -import WebpackConfigGenerator from '../src/WebpackConfig'; -import { WebpackPluginConfig, WebpackPluginEntryPoint } from '../src/Config'; +import WebpackConfigGenerator, { ConfigurationFactory } from '../src/WebpackConfig'; +import { WebpackPluginConfig, WebpackPluginEntryPoint, WebpackConfiguration } from '../src/Config'; import AssetRelocatorPatch from '../src/util/AssetRelocatorPatch'; const mockProjectDir = process.platform === 'win32' ? 'C:\\path' : '/path'; @@ -14,6 +14,17 @@ function hasAssetRelocatorPatchPlugin(plugins?: WebpackPlugin[]): boolean { return (plugins || []).some((plugin: WebpackPlugin) => plugin instanceof AssetRelocatorPatch); } +const sampleWebpackConfig = { + module: { + rules: [ + { + test: /\.(png|jpg|gif|webp)$/, + use: 'file-loader', + }, + ], + }, +}; + describe('WebpackConfigGenerator', () => { describe('rendererTarget', () => { it('is web if undefined', () => { @@ -154,12 +165,12 @@ describe('WebpackConfigGenerator', () => { }); describe('getMainConfig', () => { - it('fails when there is no mainConfig.entry', () => { + it('fails when there is no mainConfig.entry', async () => { const config = { mainConfig: {}, } as WebpackPluginConfig; const generator = new WebpackConfigGenerator(config, '/', false, 3000); - expect(() => generator.getMainConfig()).to.throw('Required option "mainConfig.entry" has not been defined'); + await expect(generator.getMainConfig()).to.be.rejectedWith('Required option "mainConfig.entry" has not been defined'); }); it('generates a development config', async () => { @@ -245,6 +256,40 @@ describe('WebpackConfigGenerator', () => { const webpackConfig = await generator.getMainConfig(); expect(webpackConfig.entry).to.equal(path.resolve(baseDir, 'foo/main.js')); }); + + it('generates a config from function', async () => { + const generateWebpackConfig = (webpackConfig: WebpackConfiguration) => { + const config = { + mainConfig: webpackConfig, + renderer: { + entryPoints: [] as WebpackPluginEntryPoint[], + }, + } as WebpackPluginConfig; + const generator = new WebpackConfigGenerator(config, mockProjectDir, false, 3000); + return generator.getMainConfig(); + }; + + const modelWebpackConfig = await generateWebpackConfig({ + entry: 'main.js', + ...sampleWebpackConfig, + }); + + // Check fn form + expect( + await generateWebpackConfig(() => ({ + entry: 'main.js', + ...sampleWebpackConfig, + })) + ).to.deep.equal(modelWebpackConfig); + + // Check promise form + expect( + await generateWebpackConfig(async () => ({ + entry: 'main.js', + ...sampleWebpackConfig, + })) + ).to.deep.equal(modelWebpackConfig); + }); }); describe('getPreloadRendererConfig', () => { @@ -469,5 +514,121 @@ describe('WebpackConfigGenerator', () => { const webpackConfig = await generator.getRendererConfig(config.renderer.entryPoints); expect(webpackConfig.target).to.equal('web'); }); + + it('generates a config from function', async () => { + const generateWebpackConfig = (webpackConfig: WebpackConfiguration) => { + const config = { + renderer: { + config: webpackConfig, + entryPoints: [ + { + name: 'main', + js: 'rendererScript.js', + }, + ], + }, + } as WebpackPluginConfig; + const generator = new WebpackConfigGenerator(config, mockProjectDir, false, 3000); + return generator.getRendererConfig(config.renderer.entryPoints); + }; + + const modelWebpackConfig = await generateWebpackConfig({ + ...sampleWebpackConfig, + }); + + // Check fn form + expect( + await generateWebpackConfig(() => ({ + ...sampleWebpackConfig, + })) + ).to.deep.equal(modelWebpackConfig); + + // Check promise form + expect( + await generateWebpackConfig(async () => ({ + ...sampleWebpackConfig, + })) + ).to.deep.equal(modelWebpackConfig); + }); + }); + + describe('preprocessConfig', () => { + context('when overriden in subclass', () => { + const makeSubclass = () => { + let invoked = 0; + + class MyWebpackConfigGenerator extends WebpackConfigGenerator { + preprocessConfig = async (config: ConfigurationFactory): Promise => { + invoked += 1; + return config({ hello: 'world' }, {}); + }; + } + + return { + getInvokedCounter: () => invoked, + MyWebpackConfigGenerator, + }; + }; + + it('is not invoked for object config', async () => { + const { MyWebpackConfigGenerator, getInvokedCounter } = makeSubclass(); + + const config = { + mainConfig: { + entry: 'main.js', + ...sampleWebpackConfig, + }, + renderer: { + config: { ...sampleWebpackConfig }, + entryPoints: [ + { + name: 'main', + js: 'rendererScript.js', + }, + ], + }, + } as WebpackPluginConfig; + + const generator = new MyWebpackConfigGenerator(config, mockProjectDir, false, 3000); + + expect(getInvokedCounter()).to.equal(0); + + await generator.getMainConfig(); + expect(getInvokedCounter()).to.equal(1); + + await generator.getRendererConfig(config.renderer.entryPoints); + expect(getInvokedCounter()).to.equal(2); + }); + + it('is invoked for fn config', async () => { + const { MyWebpackConfigGenerator, getInvokedCounter } = makeSubclass(); + + const config = { + mainConfig: () => ({ + entry: 'main.js', + ...sampleWebpackConfig, + }), + renderer: { + config: () => ({ ...sampleWebpackConfig }), + entryPoints: [ + { + name: 'main', + js: 'rendererScript.js', + }, + ], + }, + } as WebpackPluginConfig; + + const generator = new MyWebpackConfigGenerator(config, mockProjectDir, false, 3000); + + expect(getInvokedCounter()).to.equal(0); + + await generator.getMainConfig(); + expect(getInvokedCounter()).to.equal(1); + + await generator.getRendererConfig(config.renderer.entryPoints); + expect(getInvokedCounter()).to.equal(2); + }); + }); }); }); diff --git a/packages/plugin/webpack/test/util/processConfig_spec.ts b/packages/plugin/webpack/test/util/processConfig_spec.ts new file mode 100644 index 0000000000..c74910402d --- /dev/null +++ b/packages/plugin/webpack/test/util/processConfig_spec.ts @@ -0,0 +1,63 @@ +import { expect } from 'chai'; +import { ConfigurationFactory } from '../../src/WebpackConfig'; +import processConfig, { ConfigProcessor } from '../../src/util/processConfig'; + +const sampleWebpackConfig = { + module: { + rules: [ + { + test: /\.(png|jpg|gif|webp)$/, + use: 'file-loader', + }, + ], + }, +}; + +const sampleConfigFactoryParams: Parameters = [{}, { mode: 'production' }]; + +describe('processConfig', () => { + it('works for object config', async () => { + let invoked = 0; + const processor: ConfigProcessor = async (configFactory) => { + invoked += 1; + return configFactory(sampleConfigFactoryParams[0], sampleConfigFactoryParams[1]); + }; + + expect(await processConfig(processor, sampleWebpackConfig)).to.deep.equal(sampleWebpackConfig); + expect(invoked).to.equal(1); + }); + + it('works for fn config', async () => { + let invoked = 0; + const processor: ConfigProcessor = async (configFactory) => { + invoked += 1; + return configFactory(sampleConfigFactoryParams[0], sampleConfigFactoryParams[1]); + }; + + const fnConfig: ConfigurationFactory = (arg0, arg1) => { + expect(arg0).to.be.equal(sampleConfigFactoryParams[0]); + expect(arg1).to.be.equal(sampleConfigFactoryParams[1]); + return sampleWebpackConfig; + }; + + expect(await processConfig(processor, fnConfig)).to.deep.equal(sampleWebpackConfig); + expect(invoked).to.equal(1); + }); + + it('works for promise config', async () => { + let invoked = 0; + const processor: ConfigProcessor = async (configFactory) => { + invoked += 1; + return configFactory(sampleConfigFactoryParams[0], sampleConfigFactoryParams[1]); + }; + + const promiseConfig: ConfigurationFactory = (arg0, arg1) => { + expect(arg0).to.be.equal(sampleConfigFactoryParams[0]); + expect(arg1).to.be.equal(sampleConfigFactoryParams[1]); + return sampleWebpackConfig; + }; + + expect(await processConfig(processor, promiseConfig)).to.deep.equal(sampleWebpackConfig); + expect(invoked).to.equal(1); + }); +});