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(webpack-plugin): webpack 5 configuration factory #2776

Merged
merged 12 commits into from
Jun 16, 2022
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 @@ -133,3 +134,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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice if this were ESM compatible, so that the config file here could be an ESM module (potentially w/ top-level await). I think @MarshallOfSound mentioned a module that will do the work of figuring out whether a file is CommonJS or ESM and "just do the right thing" i.e. require() or import()? Or maybe just import() is enough here?

: 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 @@ -249,7 +249,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