Skip to content

Commit

Permalink
feat(webpack-plugin): webpack 5 configuration factory (#2776)
Browse files Browse the repository at this point in the history
* Add support for fn-based webpack config

* Bind the preprocessConfig function

* Correct getMainConfig test for async fn

* Fix typo

* Expose proper types at the API

* Add tests

* Add processConfig test

* Add preprocessConfig overwrite test

* fixes

* promise form

* await

* await

Co-authored-by: MOZGIII <mike-n@narod.ru>
  • Loading branch information
erickzhao and MOZGIII authored Jun 16, 2022
1 parent beb9305 commit f4a7774
Show file tree
Hide file tree
Showing 7 changed files with 281 additions and 19 deletions.
5 changes: 4 additions & 1 deletion packages/plugin/webpack/src/Config.ts
Original file line number Diff line number Diff line change
@@ -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 {
/**
Expand Down Expand Up @@ -139,3 +140,5 @@ export interface WebpackPluginConfig {
devServer?: Record<string, unknown>;
// TODO: use webpack-dev-server.Configuration when @types/webpack-dev-server upgrades to v4
}

export type WebpackConfiguration = RawWebpackConfiguration | WebpackConfigurationFactory;
37 changes: 27 additions & 10 deletions packages/plugin/webpack/src/WebpackConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string | string[]>;
type WebpackMode = 'production' | 'development';

const d = debug('electron-forge:plugin:webpack:webpackconfig');

export type ConfigurationFactory = (
env: string | Record<string, string | boolean | number> | unknown,
args: Record<string, unknown>
) => Configuration | Promise<Configuration>;

export default class WebpackConfigGenerator {
private isProd: boolean;

Expand All @@ -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<Configuration> {
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<Configuration> =>
config(
{},
{
mode: this.mode,
}
);

get mode(): WebpackMode {
return this.isProd ? 'production' : 'development';
}
Expand Down Expand Up @@ -102,8 +119,8 @@ export default class WebpackConfigGenerator {
return defines;
}

getMainConfig(): Configuration {
const mainConfig = this.resolveConfig(this.pluginConfig.mainConfig);
async getMainConfig(): Promise<Configuration> {
const mainConfig = await this.resolveConfig(this.pluginConfig.mainConfig);

if (!mainConfig.entry) {
throw new Error('Required option "mainConfig.entry" has not been defined');
Expand Down Expand Up @@ -142,7 +159,7 @@ export default class WebpackConfigGenerator {
}

async getPreloadRendererConfig(parentPoint: WebpackPluginEntryPoint, entryPoint: WebpackPreloadEntryPoint): Promise<Configuration> {
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(
Expand All @@ -165,7 +182,7 @@ export default class WebpackConfigGenerator {
}

async getRendererConfig(entryPoints: WebpackPluginEntryPoint[]): Promise<Configuration> {
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 || [];
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin/webpack/src/WebpackPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
18 changes: 18 additions & 0 deletions packages/plugin/webpack/src/util/processConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Configuration } from 'webpack';
import { ConfigurationFactory } from '../WebpackConfig';

const trivialConfigurationFactory =
(config: Configuration): ConfigurationFactory =>
() =>
config;

export type ConfigProcessor = (config: ConfigurationFactory) => Promise<Configuration>;

// Ensure processing logic is run for both `Configuration` and
// `ConfigurationFactory` config variants.
const processConfig = async (processor: ConfigProcessor, config: Configuration | ConfigurationFactory): Promise<Configuration> => {
const configFactory = typeof config === 'function' ? config : trivialConfigurationFactory(config);
return processor(configFactory);
};

export default processConfig;
4 changes: 2 additions & 2 deletions packages/plugin/webpack/test/AssetRelocatorPatch_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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({
Expand Down
171 changes: 166 additions & 5 deletions packages/plugin/webpack/test/WebpackConfig_spec.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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<Configuration> => {
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);
});
});
});
});
Loading

0 comments on commit f4a7774

Please sign in to comment.