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 (#19984)
  • Loading branch information
jaysoo authored Nov 8, 2023
1 parent 304a6d1 commit 395eb70
Show file tree
Hide file tree
Showing 23 changed files with 1,640 additions and 1,170 deletions.
35 changes: 32 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,32 @@ 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/);
}, 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
69 changes: 2 additions & 67 deletions packages/webpack/src/executors/webpack/lib/normalize-options.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { basename, dirname, relative, resolve } from 'path';
import { statSync } from 'fs';
import { normalizePath } from '@nx/devkit';
import { resolve } from 'path';

import { normalizeAssets } from '../../../plugins/nx-webpack-plugin/lib/normalize-options';
import type {
AssetGlobPattern,
FileReplacement,
NormalizedWebpackExecutorOptions,
WebpackExecutorOptions,
} from '../schema';
Expand All @@ -21,11 +18,7 @@ export function normalizeOptions(
projectRoot,
sourceRoot,
target: options.target ?? 'web',
main: resolve(root, options.main),
outputPath: resolve(root, options.outputPath),
outputFileName: options.outputFileName ?? 'main.js',
tsConfig: resolve(root, options.tsConfig),
fileReplacements: normalizeFileReplacements(root, options.fileReplacements),
assets: normalizeAssets(options.assets, root, sourceRoot),
webpackConfig: normalizePluginPath(options.webpackConfig, root),
optimization:
Expand All @@ -35,20 +28,9 @@ export function normalizeOptions(
styles: options.optimization,
}
: options.optimization,
polyfills: options.polyfills ? resolve(root, options.polyfills) : undefined,
};
}

function normalizeFileReplacements(
root: string,
fileReplacements: FileReplacement[]
): FileReplacement[] {
return fileReplacements.map((fileReplacement) => ({
replace: resolve(root, fileReplacement.replace),
with: resolve(root, fileReplacement.with),
}));
}

export function normalizePluginPath(pluginPath: void | string, root: string) {
if (!pluginPath) {
return '';
Expand All @@ -59,50 +41,3 @@ export function normalizePluginPath(pluginPath: void | string, root: string) {
return resolve(root, pluginPath);
}
}

export function normalizeAssets(
assets: any[],
root: string,
sourceRoot: string
): AssetGlobPattern[] {
return assets.map((asset) => {
if (typeof asset === 'string') {
const assetPath = normalizePath(asset);
const resolvedAssetPath = resolve(root, assetPath);
const resolvedSourceRoot = resolve(root, sourceRoot);

if (!resolvedAssetPath.startsWith(resolvedSourceRoot)) {
throw new Error(
`The ${resolvedAssetPath} asset path must start with the project source root: ${sourceRoot}`
);
}

const isDirectory = statSync(resolvedAssetPath).isDirectory();
const input = isDirectory
? resolvedAssetPath
: dirname(resolvedAssetPath);
const output = relative(resolvedSourceRoot, resolve(root, input));
const glob = isDirectory ? '**/*' : basename(resolvedAssetPath);
return {
input,
output,
glob,
};
} else {
if (asset.output.startsWith('..')) {
throw new Error(
'An asset cannot be written to a location outside of the output path.'
);
}

const assetPath = normalizePath(asset.input);
const resolvedAssetPath = resolve(root, assetPath);
return {
...asset,
input: resolvedAssetPath,
// Now we remove starting slash to make Webpack place it from the output root.
output: asset.output.replace(/^\//, ''),
};
}
});
}
8 changes: 4 additions & 4 deletions packages/webpack/src/executors/webpack/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ export interface WebpackExecutorOptions {
export interface NormalizedWebpackExecutorOptions
extends WebpackExecutorOptions {
outputFileName: string;
assets?: AssetGlobPattern[];
root?: string;
projectRoot?: string;
sourceRoot?: string;
assets: AssetGlobPattern[];
root: string;
projectRoot: string;
sourceRoot: string;
}
Loading

0 comments on commit 395eb70

Please sign in to comment.