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 1, 2023
1 parent c2aa6ef commit 43c8a02
Show file tree
Hide file tree
Showing 14 changed files with 1,497 additions and 1,079 deletions.
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,78 @@
import { Compiler } from 'webpack';

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

apply(compiler: Compiler): void {
this.addHotReload(compiler);

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

compiler.options.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
compiler.options.node = {
__dirname: true,
__filename: true,
};
}

private addHotReload(compiler: Compiler) {
const config = compiler.options;
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
private removeSvgLoaderIfPresent(compiler: Compiler) {
const svgLoaderIdx = compiler.options.module.rules.findIndex(
(rule) => typeof rule === 'object' && rule.test.toString().includes('svg')
);
if (svgLoaderIdx === -1) return;
compiler.options.module.rules.splice(svgLoaderIdx, 1);
}
}
73 changes: 2 additions & 71 deletions packages/react/plugins/with-react.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,14 @@
import type { Configuration } from 'webpack';
import type { WithWebOptions } from '@nx/webpack';
import type { NxWebpackExecutionContext } from '@nx/webpack';
import { NxReactWebpackPlugin } from './nx-react-webpack-plugin/nx-react-webpack-plugin';

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 +25,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,
};
config.plugins.push(new NxReactWebpackPlugin(pluginOptions));

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
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ export function normalizeOptions(
projectRoot,
sourceRoot,
target: options.target ?? 'web',
main: resolve(root, options.main),
outputPath: resolve(root, options.outputPath),
main: options.main ? resolve(root, options.main) : undefined,
outputPath: options.outputPath
? resolve(root, options.outputPath)
: undefined,
outputFileName: options.outputFileName ?? 'main.js',
tsConfig: resolve(root, options.tsConfig),
tsConfig: options.tsConfig ? resolve(root, options.tsConfig) : undefined,
fileReplacements: normalizeFileReplacements(root, options.fileReplacements),
assets: normalizeAssets(options.assets, root, sourceRoot),
webpackConfig: normalizePluginPath(options.webpackConfig, root),
Expand Down
12 changes: 8 additions & 4 deletions packages/webpack/src/executors/webpack/webpack.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ async function getWebpackConfigs(
}

let customWebpack = null;
if (options.webpackConfig) {
if (options.webpackConfig && options.tsConfig) {
customWebpack = resolveCustomWebpackConfig(
options.webpackConfig,
options.tsConfig.startsWith(context.root)
Expand All @@ -59,14 +59,18 @@ async function getWebpackConfigs(
? {}
: getWebpackConfig(context, options);

if (customWebpack) {
if (typeof customWebpack === 'function') {
// Old behavior, call the Nx-specific webpack config function that user exports
return await customWebpack(config, {
options,
context,
configuration: context.configurationName, // backwards compat
});
} else if (customWebpack) {
// New behavior, we want the webpack config to export object
return customWebpack;
} else {
// If the user has no webpackConfig specified then we always have to apply
// Fallback case, if we cannot find a webpack config path
return config;
}
}
Expand Down Expand Up @@ -144,7 +148,7 @@ export async function* webpackExecutor(
}

// Delete output path before bundling
if (options.deleteOutputPath) {
if (options.deleteOutputPath && options.outputPath) {
deleteOutputDir(context.root, options.outputPath);
}

Expand Down
54 changes: 30 additions & 24 deletions packages/webpack/src/plugins/generate-package-json-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,31 @@
import { type Compiler, sources, type WebpackPluginInstance } from 'webpack';
import { createLockFile, createPackageJson } from '@nx/js';
import {
detectPackageManager,
ExecutorContext,
type ProjectGraph,
serializeJson,
} from '@nx/devkit';
import {
createLockFile,
createPackageJson,
getHelperDependenciesFromProjectGraph,
getLockFileName,
HelperDependency,
readTsConfig,
} from '@nx/js';
import {
detectPackageManager,
type ProjectGraph,
serializeJson,
} from '@nx/devkit';

const pluginName = 'GeneratePackageJsonPlugin';

export class GeneratePackageJsonPlugin implements WebpackPluginInstance {
private readonly projectGraph: ProjectGraph;

constructor(
private readonly options: { tsConfig: string; outputFileName: string },
private readonly context: ExecutorContext
) {
this.projectGraph = context.projectGraph;
}
private readonly options: {
tsConfig: string;
outputFileName: string;
root: string;
projectName: string;
targetName: string;
projectGraph: ProjectGraph;
}
) {}

apply(compiler: Compiler): void {
compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
Expand All @@ -34,9 +36,9 @@ export class GeneratePackageJsonPlugin implements WebpackPluginInstance {
},
() => {
const helperDependencies = getHelperDependenciesFromProjectGraph(
this.context.root,
this.context.projectName,
this.projectGraph
this.options.root,
this.options.projectName,
this.options.projectGraph
);

const importHelpers = !!readTsConfig(this.options.tsConfig).options
Expand All @@ -50,17 +52,17 @@ export class GeneratePackageJsonPlugin implements WebpackPluginInstance {
if (shouldAddHelperDependency) {
helperDependencies.push({
type: 'static',
source: this.context.projectName,
source: this.options.projectName,
target: HelperDependency.tsc,
});
}

const packageJson = createPackageJson(
this.context.projectName,
this.projectGraph,
this.options.projectName,
this.options.projectGraph,
{
target: this.context.targetName,
root: this.context.root,
target: this.options.targetName,
root: this.options.root,
isProduction: true,
helperDependencies: helperDependencies.map((dep) => dep.target),
}
Expand All @@ -71,11 +73,15 @@ export class GeneratePackageJsonPlugin implements WebpackPluginInstance {
'package.json',
new sources.RawSource(serializeJson(packageJson))
);
const packageManager = detectPackageManager(this.context.root);
const packageManager = detectPackageManager(this.options.root);
compilation.emitAsset(
getLockFileName(packageManager),
new sources.RawSource(
createLockFile(packageJson, this.projectGraph, packageManager)
createLockFile(
packageJson,
this.options.projectGraph,
packageManager
)
)
);
}
Expand Down
Loading

0 comments on commit 43c8a02

Please sign in to comment.