Skip to content

Commit

Permalink
feat(webpack): add NxWebpackPlugin that works with normal Webpack con…
Browse files Browse the repository at this point in the history
…figuration
  • Loading branch information
jaysoo committed Nov 8, 2023
1 parent 25d6ec3 commit e8a3c54
Show file tree
Hide file tree
Showing 23 changed files with 1,669 additions and 1,170 deletions.
64 changes: 61 additions & 3 deletions e2e/webpack/src/webpack.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
cleanupProject,
newProject,
packageInstall,
rmDist,
runCLI,
runCommand,
Expand All @@ -11,8 +12,8 @@ import {
import { join } from 'path';

describe('Webpack Plugin', () => {
beforeEach(() => newProject());
afterEach(() => cleanupProject());
beforeAll(() => newProject());
afterAll(() => cleanupProject());

it('should be able to setup project to build node programs with webpack and different compilers', async () => {
const myPkg = uniq('my-pkg');
Expand Down Expand Up @@ -86,7 +87,7 @@ module.exports = composePlugins(withNx(), (config) => {

updateFile(
`libs/${myPkg}/.babelrc`,
`{ "presets": ["@nx/js/babel", "./custom-preset"] } `
`{ 'presets': ['@nx/js/babel', './custom-preset'] } `
);
updateFile(
`libs/${myPkg}/custom-preset.js`,
Expand All @@ -106,4 +107,61 @@ module.exports = composePlugins(withNx(), (config) => {
});
expect(output).toContain('Babel env is babelEnv');
}, 500_000);

it('should be able to build with NxWebpackPlugin and a standard webpack config file', () => {
const appName = uniq('app');
runCLI(`generate @nx/web:app ${appName} --bundler webpack`);
updateFile(`apps/${appName}/src/main.ts`, `console.log('Hello');\n`);

updateFile(
`apps/${appName}/webpack.config.js`,
`
const path = require('path');
const { NxWebpackPlugin } = require('@nx/webpack');
module.exports = {
target: 'node',
output: {
path: path.join(__dirname, '../../dist/${appName}')
},
plugins: [
new NxWebpackPlugin()
]
};`
);

runCLI(`build ${appName} --outputHashing none`);

let output = runCommand(`node dist/${appName}/main.js`);
expect(output).toMatch(/Hello/);

// Check that app can build using Webpack CLI (without @nx/webpack:webpack executor)
packageInstall('webpack-cli', undefined, '^5.1.4', 'prod');

updateFile(
`apps/${appName}/webpack.config.js`,
`
const path = require('path');
const { NxWebpackPlugin } = require('@nx/webpack');
module.exports = {
target: 'node',
output: {
path: path.join(__dirname, '../../dist/${appName}')
},
plugins: [
new NxWebpackPlugin({
compiler: 'swc',
main: 'apps/${appName}/src/main.ts',
tsConfig: 'apps/${appName}/tsconfig.app.json',
})
]
};`
);

runCLI(`webpack-build ${appName}`);

output = runCommand(`node dist/${appName}/main.js`);
expect(output).toMatch(/Hello/);
}, 500_000);
});
1 change: 1 addition & 0 deletions packages/react/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ export { componentTestGenerator } from './src/generators/component-test/componen
export { setupTailwindGenerator } from './src/generators/setup-tailwind/setup-tailwind';
export type { SupportedStyles } from './typings/style';
export * from './plugins/with-react';
export { NxReactWebpackPlugin } from './plugins/nx-react-webpack-plugin/nx-react-webpack-plugin';
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Compiler, Configuration, WebpackOptionsNormalized } from 'webpack';

export function applyReactConfig(
options: { svgr?: boolean },
config: Partial<WebpackOptionsNormalized | Configuration> = {}
): void {
addHotReload(config);

if (options.svgr !== false) {
removeSvgLoaderIfPresent(config);

config.module.rules.push({
test: /\.svg$/,
issuer: /\.(js|ts|md)x?$/,
use: [
{
loader: require.resolve('@svgr/webpack'),
options: {
svgo: false,
titleProp: true,
ref: true,
},
},
{
loader: require.resolve('file-loader'),
options: {
name: '[name].[hash].[ext]',
},
},
],
});
}

// enable webpack node api
config.node = {
__dirname: true,
__filename: true,
};
}

function addHotReload(
config: Partial<WebpackOptionsNormalized | Configuration>
) {
if (config.mode === 'development' && config['devServer']?.hot) {
// add `react-refresh/babel` to babel loader plugin
const babelLoader = config.module.rules.find(
(rule) =>
rule &&
typeof rule !== 'string' &&
rule.loader?.toString().includes('babel-loader')
);

if (babelLoader && typeof babelLoader !== 'string') {
babelLoader.options['plugins'] = [
...(babelLoader.options['plugins'] || []),
[
require.resolve('react-refresh/babel'),
{
skipEnvCheck: true,
},
],
];
}

const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
config.plugins.push(new ReactRefreshPlugin());
}
}

// We remove potentially conflicting rules that target SVGs because we use @svgr/webpack loader
// See https://github.com/nrwl/nx/issues/14383
function removeSvgLoaderIfPresent(
config: Partial<WebpackOptionsNormalized | Configuration>
) {
const svgLoaderIdx = config.module.rules.findIndex(
(rule) => typeof rule === 'object' && rule.test.toString().includes('svg')
);
if (svgLoaderIdx === -1) return;
config.module.rules.splice(svgLoaderIdx, 1);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Compiler } from 'webpack';
import { applyReactConfig } from './lib/apply-react-config';

export class NxReactWebpackPlugin {
constructor(private options: { svgr?: boolean } = {}) {}

apply(compiler: Compiler): void {
applyReactConfig(this.options, compiler.options);
}
}
76 changes: 3 additions & 73 deletions packages/react/plugins/with-react.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,13 @@
import type { Configuration } from 'webpack';
import type { WithWebOptions } from '@nx/webpack';
import type { NxWebpackExecutionContext } from '@nx/webpack';
import type { NxWebpackExecutionContext, WithWebOptions } from '@nx/webpack';
import { applyReactConfig } from './nx-react-webpack-plugin/lib/apply-react-config';

const processed = new Set();

interface WithReactOptions extends WithWebOptions {
svgr?: false;
}

function addHotReload(config: Configuration) {
if (config.mode === 'development' && config['devServer']?.hot) {
// add `react-refresh/babel` to babel loader plugin
const babelLoader = config.module.rules.find(
(rule) =>
rule &&
typeof rule !== 'string' &&
rule.loader?.toString().includes('babel-loader')
);

if (babelLoader && typeof babelLoader !== 'string') {
babelLoader.options['plugins'] = [
...(babelLoader.options['plugins'] || []),
[
require.resolve('react-refresh/babel'),
{
skipEnvCheck: true,
},
],
];
}

const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
config.plugins.push(new ReactRefreshPlugin());
}
}

// We remove potentially conflicting rules that target SVGs because we use @svgr/webpack loader
// See https://github.com/nrwl/nx/issues/14383
function removeSvgLoaderIfPresent(config: Configuration) {
const svgLoaderIdx = config.module.rules.findIndex(
(rule) => typeof rule === 'object' && rule.test.toString().includes('svg')
);

if (svgLoaderIdx === -1) return;

config.module.rules.splice(svgLoaderIdx, 1);
}

/**
* @param {WithReactOptions} pluginOptions
* @returns {NxWebpackPlugin}
Expand All @@ -63,38 +24,7 @@ export function withReact(pluginOptions: WithReactOptions = {}) {
// Apply web config for CSS, JSX, index.html handling, etc.
config = withWeb(pluginOptions)(config, context);

addHotReload(config);

if (pluginOptions?.svgr !== false) {
removeSvgLoaderIfPresent(config);

config.module.rules.push({
test: /\.svg$/,
issuer: /\.(js|ts|md)x?$/,
use: [
{
loader: require.resolve('@svgr/webpack'),
options: {
svgo: false,
titleProp: true,
ref: true,
},
},
{
loader: require.resolve('file-loader'),
options: {
name: '[name].[hash].[ext]',
},
},
],
});
}

// enable webpack node api
config.node = {
__dirname: true,
__filename: true,
};
applyReactConfig(pluginOptions, config);

processed.add(config);
return config;
Expand Down
2 changes: 2 additions & 0 deletions packages/webpack/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ export * from './src/utils/get-css-module-local-ident';
export * from './src/utils/with-nx';
export * from './src/utils/with-web';
export * from './src/utils/module-federation/public-api';
export { NxWebpackPlugin } from './src/plugins/nx-webpack-plugin/nx-webpack-plugin';
export { NxTsconfigPathsWebpackPlugin } from './src/plugins/nx-typescript-webpack-plugin/nx-tsconfig-paths-webpack-plugin';
22 changes: 17 additions & 5 deletions packages/webpack/src/executors/dev-server/dev-server.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,23 @@ export async function* devServerExecutor(
customWebpack = await customWebpack;
}

config = await customWebpack(config, {
options: buildOptions,
context,
configuration: serveOptions.buildTarget.split(':')[2],
});
if (typeof customWebpack === 'function') {
// Old behavior, call the webpack function that is specific to Nx
config = await customWebpack(config, {
options: buildOptions,
context,
configuration: serveOptions.buildTarget.split(':')[2],
});
} else if (customWebpack) {
// New behavior, use the config object as is with devServer defaults
config = {
devServer: {
...customWebpack.devServer,
...config.devServer,
},
...customWebpack,
};
}
}

return yield* eachValueFrom(
Expand Down
Loading

0 comments on commit e8a3c54

Please sign in to comment.