<!-- Please make sure you have read the submission guidelines before posting an PR --> <!-- https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr --> <!-- Please make sure that your commit message follows our format --> <!-- Example: `fix(nx): must begin with lowercase` --> <!-- If this is a particularly complex change or feature addition, you can request a dedicated Nx release for this pull request branch. Mention someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they will confirm if the PR warrants its own release for testing purposes, and generate it for you if appropriate. --> ## Current Behavior <!-- This is the behavior we have today --> When a style path is passed to the `NxAppWebpackPlugin` we normalize it to the `projectRoot` ## Expected Behavior <!-- This is the behavior we should expect with the changes in this PR --> When a style path is passed to the `NxAppWebpackPlugin` if it is already normalized i.e. it is relative to the project we should assume the path is correct. ## Related Issue(s) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes #
431 lines
12 KiB
TypeScript
431 lines
12 KiB
TypeScript
import * as path from 'path';
|
|
import { SubresourceIntegrityPlugin } from 'webpack-subresource-integrity';
|
|
import {
|
|
Configuration,
|
|
DefinePlugin,
|
|
ids,
|
|
RuleSetRule,
|
|
WebpackOptionsNormalized,
|
|
WebpackPluginInstance,
|
|
} from 'webpack';
|
|
|
|
import { WriteIndexHtmlPlugin } from '../../write-index-html-plugin';
|
|
import { NormalizedNxAppWebpackPluginOptions } from '../nx-app-webpack-plugin-options';
|
|
import { getOutputHashFormat } from '../../../utils/hash-format';
|
|
import { getClientEnvironment } from '../../../utils/get-client-environment';
|
|
import { normalizeExtraEntryPoints } from '../../../utils/webpack/normalize-entry';
|
|
import {
|
|
getCommonLoadersForCssModules,
|
|
getCommonLoadersForGlobalCss,
|
|
getCommonLoadersForGlobalStyle,
|
|
} from './stylesheet-loaders';
|
|
import { instantiateScriptPlugins } from './instantiate-script-plugins';
|
|
import CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
|
|
import MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
|
|
|
export function applyWebConfig(
|
|
options: NormalizedNxAppWebpackPluginOptions,
|
|
config: Partial<WebpackOptionsNormalized | Configuration> = {},
|
|
{
|
|
useNormalizedEntry,
|
|
}: {
|
|
// webpack.Configuration allows arrays to be set on a single entry
|
|
// webpack then normalizes them to { import: "..." } objects
|
|
// This option allows use to preserve existing composePlugins behavior where entry.main is an array.
|
|
useNormalizedEntry?: boolean;
|
|
} = {}
|
|
): void {
|
|
if (!process.env['NX_TASK_TARGET_PROJECT']) return;
|
|
|
|
// Defaults that was applied from executor schema previously.
|
|
options.runtimeChunk ??= true; // need this for HMR and other things to work
|
|
options.extractCss ??= true;
|
|
options.generateIndexHtml ??= true;
|
|
options.styles ??= [];
|
|
options.scripts ??= [];
|
|
|
|
const plugins: WebpackPluginInstance[] = [];
|
|
|
|
const stylesOptimization =
|
|
typeof options.optimization === 'object'
|
|
? options.optimization.styles
|
|
: options.optimization;
|
|
|
|
if (Array.isArray(options.scripts)) {
|
|
plugins.push(...instantiateScriptPlugins(options));
|
|
}
|
|
if (options.index && options.generateIndexHtml) {
|
|
plugins.push(
|
|
new WriteIndexHtmlPlugin({
|
|
crossOrigin: options.crossOrigin,
|
|
sri: options.subresourceIntegrity,
|
|
outputPath: path.basename(options.index),
|
|
indexPath: path.join(options.root, options.index),
|
|
baseHref: options.baseHref,
|
|
deployUrl: options.deployUrl,
|
|
scripts: options.scripts,
|
|
styles: options.styles,
|
|
})
|
|
);
|
|
}
|
|
|
|
if (options.subresourceIntegrity) {
|
|
plugins.push(new SubresourceIntegrityPlugin());
|
|
}
|
|
|
|
const minimizer: WebpackPluginInstance[] = [new ids.HashedModuleIdsPlugin()];
|
|
if (stylesOptimization) {
|
|
minimizer.push(
|
|
new CssMinimizerPlugin({
|
|
test: /\.(?:css|scss|sass|less|styl)$/,
|
|
})
|
|
);
|
|
}
|
|
if (!options.ssr) {
|
|
plugins.push(
|
|
new DefinePlugin(getClientEnvironment(process.env.NODE_ENV).stringified)
|
|
);
|
|
}
|
|
|
|
const entries: { [key: string]: { import: string[] } } = {};
|
|
const globalStylePaths: string[] = [];
|
|
|
|
// Determine hashing format.
|
|
const hashFormat = getOutputHashFormat(options.outputHashing as string);
|
|
|
|
const includePaths: string[] = [];
|
|
if (options?.stylePreprocessorOptions?.includePaths?.length > 0) {
|
|
options.stylePreprocessorOptions.includePaths.forEach(
|
|
(includePath: string) =>
|
|
includePaths.push(path.resolve(options.root, includePath))
|
|
);
|
|
}
|
|
|
|
let lessPathOptions: { paths?: string[] } = {};
|
|
|
|
if (includePaths.length > 0) {
|
|
lessPathOptions = {
|
|
paths: includePaths,
|
|
};
|
|
}
|
|
|
|
// Process global styles.
|
|
if (options.styles.length > 0) {
|
|
normalizeExtraEntryPoints(options.styles, 'styles').forEach((style) => {
|
|
const resolvedPath = style.input.startsWith('.')
|
|
? style.input
|
|
: path.resolve(options.root, style.input);
|
|
// Add style entry points.
|
|
if (entries[style.bundleName]) {
|
|
entries[style.bundleName].import.push(resolvedPath);
|
|
} else {
|
|
entries[style.bundleName] = { import: [resolvedPath] };
|
|
}
|
|
|
|
// Add global css paths.
|
|
globalStylePaths.push(resolvedPath);
|
|
});
|
|
}
|
|
|
|
const cssModuleRules: RuleSetRule[] = [
|
|
{
|
|
test: /\.module\.css$/,
|
|
exclude: globalStylePaths,
|
|
use: getCommonLoadersForCssModules(options, includePaths),
|
|
},
|
|
{
|
|
test: /\.module\.(scss|sass)$/,
|
|
exclude: globalStylePaths,
|
|
use: [
|
|
...getCommonLoadersForCssModules(options, includePaths),
|
|
{
|
|
loader: require.resolve('sass-loader'),
|
|
options: {
|
|
implementation: require('sass'),
|
|
sassOptions: {
|
|
fiber: false,
|
|
precision: 8,
|
|
includePaths,
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
test: /\.module\.less$/,
|
|
exclude: globalStylePaths,
|
|
use: [
|
|
...getCommonLoadersForCssModules(options, includePaths),
|
|
{
|
|
loader: require.resolve('less-loader'),
|
|
options: {
|
|
lessOptions: {
|
|
paths: includePaths,
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
test: /\.module\.styl$/,
|
|
exclude: globalStylePaths,
|
|
use: [
|
|
...getCommonLoadersForCssModules(options, includePaths),
|
|
{
|
|
loader: path.join(
|
|
__dirname,
|
|
'../../../utils/webpack/deprecated-stylus-loader.js'
|
|
),
|
|
options: {
|
|
stylusOptions: {
|
|
include: includePaths,
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const globalCssRules: RuleSetRule[] = [
|
|
{
|
|
test: /\.css$/,
|
|
exclude: globalStylePaths,
|
|
use: getCommonLoadersForGlobalCss(options, includePaths),
|
|
},
|
|
{
|
|
test: /\.scss$|\.sass$/,
|
|
exclude: globalStylePaths,
|
|
use: [
|
|
...getCommonLoadersForGlobalCss(options, includePaths),
|
|
{
|
|
loader: require.resolve('sass-loader'),
|
|
options: {
|
|
implementation: require('sass'),
|
|
sourceMap: !!options.sourceMap,
|
|
sassOptions: {
|
|
fiber: false,
|
|
// bootstrap-sass requires a minimum precision of 8
|
|
precision: 8,
|
|
includePaths,
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
test: /\.less$/,
|
|
exclude: globalStylePaths,
|
|
use: [
|
|
...getCommonLoadersForGlobalCss(options, includePaths),
|
|
{
|
|
loader: require.resolve('less-loader'),
|
|
options: {
|
|
sourceMap: !!options.sourceMap,
|
|
lessOptions: {
|
|
javascriptEnabled: true,
|
|
...lessPathOptions,
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
test: /\.styl$/,
|
|
exclude: globalStylePaths,
|
|
use: [
|
|
...getCommonLoadersForGlobalCss(options, includePaths),
|
|
{
|
|
loader: path.join(
|
|
__dirname,
|
|
'../../../utils/webpack/deprecated-stylus-loader.js'
|
|
),
|
|
options: {
|
|
sourceMap: !!options.sourceMap,
|
|
stylusOptions: {
|
|
include: includePaths,
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const globalStyleRules: RuleSetRule[] = [
|
|
{
|
|
test: /\.css$/,
|
|
include: globalStylePaths,
|
|
use: getCommonLoadersForGlobalStyle(options, includePaths),
|
|
},
|
|
{
|
|
test: /\.scss$|\.sass$/,
|
|
include: globalStylePaths,
|
|
use: [
|
|
...getCommonLoadersForGlobalStyle(options, includePaths),
|
|
{
|
|
loader: require.resolve('sass-loader'),
|
|
options: {
|
|
implementation: require('sass'),
|
|
sourceMap: !!options.sourceMap,
|
|
sassOptions: {
|
|
fiber: false,
|
|
// bootstrap-sass requires a minimum precision of 8
|
|
precision: 8,
|
|
includePaths,
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
test: /\.less$/,
|
|
include: globalStylePaths,
|
|
use: [
|
|
...getCommonLoadersForGlobalStyle(options, includePaths),
|
|
{
|
|
loader: require.resolve('less-loader'),
|
|
options: {
|
|
sourceMap: !!options.sourceMap,
|
|
lessOptions: {
|
|
javascriptEnabled: true,
|
|
...lessPathOptions,
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
test: /\.styl$/,
|
|
include: globalStylePaths,
|
|
use: [
|
|
...getCommonLoadersForGlobalStyle(options, includePaths),
|
|
{
|
|
loader: path.join(
|
|
__dirname,
|
|
'../../../utils/webpack/deprecated-stylus-loader.js'
|
|
),
|
|
options: {
|
|
sourceMap: !!options.sourceMap,
|
|
stylusOptions: {
|
|
include: includePaths,
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const rules: RuleSetRule[] = [
|
|
{
|
|
test: /\.css$|\.scss$|\.sass$|\.less$|\.styl$/,
|
|
oneOf: [...cssModuleRules, ...globalCssRules, ...globalStyleRules],
|
|
},
|
|
];
|
|
|
|
if (options.extractCss) {
|
|
plugins.push(
|
|
// extract global css from js files into own css file
|
|
new MiniCssExtractPlugin({
|
|
filename: `[name]${hashFormat.extract}.css`,
|
|
})
|
|
);
|
|
}
|
|
|
|
config.output = {
|
|
...config.output,
|
|
assetModuleFilename: '[name].[contenthash:20][ext]',
|
|
crossOriginLoading: options.subresourceIntegrity
|
|
? ('anonymous' as const)
|
|
: (false as const),
|
|
};
|
|
|
|
// In case users customize their webpack config with unsupported entry.
|
|
if (typeof config.entry === 'function')
|
|
throw new Error('Entry function is not supported. Use an object.');
|
|
if (typeof config.entry === 'string')
|
|
throw new Error('Entry string is not supported. Use an object.');
|
|
if (Array.isArray(config.entry))
|
|
throw new Error('Entry array is not supported. Use an object.');
|
|
|
|
Object.entries(entries).forEach(([entryName, entryData]) => {
|
|
if (useNormalizedEntry) {
|
|
config.entry[entryName] = { import: entryData.import };
|
|
} else {
|
|
config.entry[entryName] = entryData.import;
|
|
}
|
|
});
|
|
|
|
config.optimization = {
|
|
...config.optimization,
|
|
minimizer: [...config.optimization.minimizer, ...minimizer],
|
|
emitOnErrors: false,
|
|
moduleIds: 'deterministic' as const,
|
|
runtimeChunk: options.runtimeChunk ? { name: 'runtime' } : false,
|
|
splitChunks: {
|
|
defaultSizeTypes:
|
|
config.optimization.splitChunks !== false
|
|
? config.optimization.splitChunks?.defaultSizeTypes
|
|
: ['...'],
|
|
maxAsyncRequests: Infinity,
|
|
cacheGroups: {
|
|
default: !!options.commonChunk && {
|
|
chunks: 'async' as const,
|
|
minChunks: 2,
|
|
priority: 10,
|
|
},
|
|
common: !!options.commonChunk && {
|
|
name: 'common',
|
|
chunks: 'async' as const,
|
|
minChunks: 2,
|
|
enforce: true,
|
|
priority: 5,
|
|
},
|
|
vendors: false as const,
|
|
vendor: !!options.vendorChunk && {
|
|
name: 'vendor',
|
|
chunks: (chunk) => chunk.name === 'main',
|
|
enforce: true,
|
|
test: /[\\/]node_modules[\\/]/,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
config.resolve.mainFields = ['browser', 'module', 'main'];
|
|
|
|
config.module = {
|
|
...config.module,
|
|
rules: [
|
|
...(config.module.rules ?? []),
|
|
// Images: Inline small images, and emit a separate file otherwise.
|
|
{
|
|
test: /\.(avif|bmp|gif|ico|jpe?g|png|webp)$/,
|
|
type: 'asset',
|
|
parser: {
|
|
dataUrlCondition: {
|
|
maxSize: 10_000, // 10 kB
|
|
},
|
|
},
|
|
},
|
|
// SVG: same as image but we need to separate it so it can be swapped for SVGR in the React plugin.
|
|
{
|
|
test: /\.svg$/,
|
|
type: 'asset',
|
|
parser: {
|
|
dataUrlCondition: {
|
|
maxSize: 10_000, // 10 kB
|
|
},
|
|
},
|
|
},
|
|
// Fonts: Emit separate file and export the URL.
|
|
{
|
|
test: /\.(eot|otf|ttf|woff|woff2)$/,
|
|
type: 'asset/resource',
|
|
},
|
|
...rules,
|
|
],
|
|
};
|
|
|
|
config.plugins ??= [];
|
|
config.plugins.push(...plugins);
|
|
}
|