feat(webpack): add NxWebpackPlugin that works with normal Webpack configuration (#19984)
This commit is contained in:
parent
304a6d14d6
commit
395eb70336
@ -1,6 +1,7 @@
|
||||
import {
|
||||
cleanupProject,
|
||||
newProject,
|
||||
packageInstall,
|
||||
rmDist,
|
||||
runCLI,
|
||||
runCommand,
|
||||
@ -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');
|
||||
@ -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`,
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
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();
|
||||
|
||||
@ -8,45 +8,6 @@ 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}
|
||||
@ -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;
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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';
|
||||
@ -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:
|
||||
@ -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 '';
|
||||
@ -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(/^\//, ''),
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -1,9 +1,4 @@
|
||||
import {
|
||||
ExecutorContext,
|
||||
logger,
|
||||
stripIndents,
|
||||
workspaceRoot,
|
||||
} from '@nx/devkit';
|
||||
import { ExecutorContext, logger, stripIndents } from '@nx/devkit';
|
||||
import { eachValueFrom } from '@nx/devkit/src/utils/rxjs-for-await';
|
||||
import type { Configuration, Stats } from 'webpack';
|
||||
import { from, of } from 'rxjs';
|
||||
@ -42,7 +37,7 @@ async function getWebpackConfigs(
|
||||
}
|
||||
|
||||
let customWebpack = null;
|
||||
if (options.webpackConfig) {
|
||||
if (options.webpackConfig && options.tsConfig) {
|
||||
customWebpack = resolveCustomWebpackConfig(
|
||||
options.webpackConfig,
|
||||
options.tsConfig.startsWith(context.root)
|
||||
@ -59,14 +54,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;
|
||||
}
|
||||
}
|
||||
@ -87,6 +86,9 @@ export async function* webpackExecutor(
|
||||
_options: WebpackExecutorOptions,
|
||||
context: ExecutorContext
|
||||
): AsyncGenerator<WebpackExecutorEvent, WebpackExecutorEvent, undefined> {
|
||||
// Pass to NxWebpackPlugin so we can get the CLI overrides.
|
||||
process.env['NX_WEBPACK_EXECUTOR_RAW_OPTIONS'] = JSON.stringify(_options);
|
||||
|
||||
const metadata = context.projectsConfigurations.projects[context.projectName];
|
||||
const sourceRoot = metadata.sourceRoot;
|
||||
const options = normalizeOptions(
|
||||
@ -144,7 +146,7 @@ export async function* webpackExecutor(
|
||||
}
|
||||
|
||||
// Delete output path before bundling
|
||||
if (options.deleteOutputPath) {
|
||||
if (options.deleteOutputPath && options.outputPath) {
|
||||
deleteOutputDir(context.root, options.outputPath);
|
||||
}
|
||||
|
||||
|
||||
@ -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) => {
|
||||
@ -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
|
||||
@ -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),
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
import * as path from 'path';
|
||||
import { Compiler } from 'webpack';
|
||||
import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin';
|
||||
import { workspaceRoot } from '@nx/devkit';
|
||||
|
||||
export class NxTsconfigPathsWebpackPlugin {
|
||||
constructor(
|
||||
private options: {
|
||||
tsConfig: string;
|
||||
}
|
||||
) {
|
||||
if (!this.options.tsConfig)
|
||||
throw new Error(
|
||||
`Missing "tsConfig" option. Set this option in your Nx webpack plugin.`
|
||||
);
|
||||
}
|
||||
|
||||
apply(compiler: Compiler): void {
|
||||
const extensions = new Set([
|
||||
...['.ts', '.tsx', '.mjs', '.js', '.jsx'],
|
||||
...(compiler.options?.resolve?.extensions ?? []),
|
||||
]);
|
||||
compiler.options.resolve = {
|
||||
...compiler.options.resolve,
|
||||
plugins: compiler.options.resolve?.plugins ?? [],
|
||||
};
|
||||
compiler.options.resolve.plugins.push(
|
||||
new TsconfigPathsPlugin({
|
||||
configFile: !path.isAbsolute(this.options.tsConfig)
|
||||
? path.join(workspaceRoot, this.options.tsConfig)
|
||||
: this.options.tsConfig,
|
||||
extensions: Array.from(extensions),
|
||||
mainFields: ['module', 'main'],
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,341 @@
|
||||
import * as path from 'path';
|
||||
import { ExecutorContext } from 'nx/src/config/misc-interfaces';
|
||||
import { LicenseWebpackPlugin } from 'license-webpack-plugin';
|
||||
import * as CopyWebpackPlugin from 'copy-webpack-plugin';
|
||||
import {
|
||||
Configuration,
|
||||
ProgressPlugin,
|
||||
WebpackOptionsNormalized,
|
||||
WebpackPluginInstance,
|
||||
} from 'webpack';
|
||||
|
||||
import { StatsJsonPlugin } from '../../stats-json-plugin';
|
||||
import { GeneratePackageJsonPlugin } from '../../generate-package-json-plugin';
|
||||
import { getOutputHashFormat } from '../../../utils/hash-format';
|
||||
import { NxTsconfigPathsWebpackPlugin } from '../../nx-typescript-webpack-plugin/nx-tsconfig-paths-webpack-plugin';
|
||||
import { getTerserEcmaVersion } from './get-terser-ecma-version';
|
||||
import { createLoaderFromCompiler } from './compiler-loaders';
|
||||
import { NormalizedNxWebpackPluginOptions } from '../nx-webpack-plugin-options';
|
||||
import TerserPlugin = require('terser-webpack-plugin');
|
||||
import nodeExternals = require('webpack-node-externals');
|
||||
import ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
|
||||
|
||||
const IGNORED_WEBPACK_WARNINGS = [
|
||||
/The comment file/i,
|
||||
/could not find any license/i,
|
||||
];
|
||||
|
||||
const extensions = ['.ts', '.tsx', '.mjs', '.js', '.jsx'];
|
||||
const mainFields = ['module', 'main'];
|
||||
|
||||
export function applyBaseConfig(
|
||||
options: NormalizedNxWebpackPluginOptions,
|
||||
config: Partial<WebpackOptionsNormalized | Configuration> = {}
|
||||
): void {
|
||||
const plugins: WebpackPluginInstance[] = [
|
||||
new NxTsconfigPathsWebpackPlugin(options),
|
||||
];
|
||||
const executorContext: Partial<ExecutorContext> = {
|
||||
projectName: options.projectName,
|
||||
targetName: options.targetName,
|
||||
projectGraph: options.projectGraph,
|
||||
configurationName: options.configurationName,
|
||||
root: options.root,
|
||||
};
|
||||
|
||||
if (!options?.skipTypeChecking) {
|
||||
plugins.push(
|
||||
new ForkTsCheckerWebpackPlugin({
|
||||
typescript: {
|
||||
configFile: path.isAbsolute(options.tsConfig)
|
||||
? options.tsConfig
|
||||
: path.join(options.root, options.tsConfig),
|
||||
memoryLimit: options.memoryLimit || 2018,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
const entries: Array<{ name: string; import: string[] }> = [];
|
||||
|
||||
if (options.main) {
|
||||
const mainEntry = options.outputFileName
|
||||
? path.parse(options.outputFileName).name
|
||||
: 'main';
|
||||
entries.push({
|
||||
name: mainEntry,
|
||||
import: [path.resolve(options.root, options.main)],
|
||||
});
|
||||
}
|
||||
|
||||
if (options.additionalEntryPoints) {
|
||||
for (const { entryName, entryPath } of options.additionalEntryPoints) {
|
||||
entries.push({
|
||||
name: entryName,
|
||||
import: [path.resolve(options.root, entryPath)],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (options.polyfills) {
|
||||
entries.push({
|
||||
name: 'polyfills',
|
||||
import: [path.resolve(options.root, options.polyfills)],
|
||||
});
|
||||
}
|
||||
|
||||
config.entry ??= {};
|
||||
entries.forEach((entry) => {
|
||||
config.entry[entry.name] = { import: entry.import };
|
||||
});
|
||||
|
||||
if (options.progress) {
|
||||
plugins.push(new ProgressPlugin({ profile: options.verbose }));
|
||||
}
|
||||
|
||||
if (options.extractLicenses) {
|
||||
plugins.push(
|
||||
new LicenseWebpackPlugin({
|
||||
stats: {
|
||||
warnings: false,
|
||||
errors: false,
|
||||
},
|
||||
perChunkOutput: false,
|
||||
outputFilename: `3rdpartylicenses.txt`,
|
||||
}) as unknown as WebpackPluginInstance
|
||||
);
|
||||
}
|
||||
|
||||
if (Array.isArray(options.assets) && options.assets.length > 0) {
|
||||
plugins.push(
|
||||
new CopyWebpackPlugin({
|
||||
patterns: options.assets.map((asset) => {
|
||||
return {
|
||||
context: asset.input,
|
||||
// Now we remove starting slash to make Webpack place it from the output root.
|
||||
to: asset.output,
|
||||
from: asset.glob,
|
||||
globOptions: {
|
||||
ignore: [
|
||||
'.gitkeep',
|
||||
'**/.DS_Store',
|
||||
'**/Thumbs.db',
|
||||
...(asset.ignore ?? []),
|
||||
],
|
||||
dot: true,
|
||||
},
|
||||
};
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
if (options.generatePackageJson && executorContext) {
|
||||
plugins.push(new GeneratePackageJsonPlugin(options));
|
||||
}
|
||||
|
||||
if (options.statsJson) {
|
||||
plugins.push(new StatsJsonPlugin());
|
||||
}
|
||||
|
||||
const externals = [];
|
||||
if (options.target === 'node' && options.externalDependencies === 'all') {
|
||||
const modulesDir = `${options.root}/node_modules`;
|
||||
externals.push(nodeExternals({ modulesDir }));
|
||||
} else if (Array.isArray(options.externalDependencies)) {
|
||||
externals.push(function (ctx, callback: Function) {
|
||||
if (options.externalDependencies.includes(ctx.request)) {
|
||||
// not bundled
|
||||
return callback(null, `commonjs ${ctx.request}`);
|
||||
}
|
||||
// bundled
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
const hashFormat = getOutputHashFormat(options.outputHashing as string);
|
||||
config.context = path.join(options.root, options.projectRoot);
|
||||
config.target ??= options.target;
|
||||
config.node = false;
|
||||
config.mode =
|
||||
// When the target is Node avoid any optimizations, such as replacing `process.env.NODE_ENV` with build time value.
|
||||
config.target === 'node'
|
||||
? 'none'
|
||||
: // Otherwise, make sure it matches `process.env.NODE_ENV`.
|
||||
// When mode is development or production, webpack will automatically
|
||||
// configure DefinePlugin to replace `process.env.NODE_ENV` with the
|
||||
// build-time value. Thus, we need to make sure it's the same value to
|
||||
// avoid conflicts.
|
||||
//
|
||||
// When the NODE_ENV is something else (e.g. test), then set it to none
|
||||
// to prevent extra behavior from webpack.
|
||||
process.env.NODE_ENV === 'development' ||
|
||||
process.env.NODE_ENV === 'production'
|
||||
? (process.env.NODE_ENV as 'development' | 'production')
|
||||
: 'none';
|
||||
// When target is Node, the Webpack mode will be set to 'none' which disables in memory caching and causes a full rebuild on every change.
|
||||
// So to mitigate this we enable in memory caching when target is Node and in watch mode.
|
||||
config.cache =
|
||||
options.target === 'node' && options.watch ? { type: 'memory' } : undefined;
|
||||
|
||||
config.devtool =
|
||||
options.sourceMap === 'hidden'
|
||||
? 'hidden-source-map'
|
||||
: options.sourceMap
|
||||
? 'source-map'
|
||||
: false;
|
||||
|
||||
config.output = {
|
||||
...config.output,
|
||||
path:
|
||||
config.output?.path ??
|
||||
(options.outputPath
|
||||
? path.join(options.root, options.outputPath)
|
||||
: undefined),
|
||||
filename:
|
||||
config.output?.filename ?? options.outputHashing
|
||||
? `[name]${hashFormat.script}.js`
|
||||
: '[name].js',
|
||||
chunkFilename:
|
||||
config.output?.chunkFilename ?? options.outputHashing
|
||||
? `[name]${hashFormat.chunk}.js`
|
||||
: '[name].js',
|
||||
hashFunction: config.output?.hashFunction ?? 'xxhash64',
|
||||
// Disabled for performance
|
||||
pathinfo: config.output?.pathinfo ?? false,
|
||||
// Use CJS for Node since it has the widest support.
|
||||
scriptType:
|
||||
config.output?.scriptType ?? options.target === 'node'
|
||||
? undefined
|
||||
: 'module',
|
||||
};
|
||||
|
||||
config.watch = options.watch;
|
||||
|
||||
config.watchOptions = {
|
||||
poll: options.poll,
|
||||
};
|
||||
|
||||
config.profile = options.statsJson;
|
||||
|
||||
config.resolve = {
|
||||
...config.resolve,
|
||||
extensions: [...extensions, ...(config?.resolve?.extensions ?? [])],
|
||||
alias: options.fileReplacements.reduce(
|
||||
(aliases, replacement) => ({
|
||||
...aliases,
|
||||
[replacement.replace]: replacement.with,
|
||||
}),
|
||||
{}
|
||||
),
|
||||
mainFields,
|
||||
};
|
||||
|
||||
config.externals = externals;
|
||||
|
||||
config.optimization = {
|
||||
...config.optimization,
|
||||
sideEffects: true,
|
||||
minimize:
|
||||
typeof options.optimization === 'object'
|
||||
? !!options.optimization.scripts
|
||||
: !!options.optimization,
|
||||
minimizer: [
|
||||
options.compiler !== 'swc'
|
||||
? new TerserPlugin({
|
||||
parallel: true,
|
||||
terserOptions: {
|
||||
keep_classnames: true,
|
||||
ecma: getTerserEcmaVersion(
|
||||
path.join(options.root, options.projectRoot)
|
||||
),
|
||||
safari10: true,
|
||||
format: {
|
||||
ascii_only: true,
|
||||
comments: false,
|
||||
webkit: true,
|
||||
},
|
||||
},
|
||||
extractComments: false,
|
||||
})
|
||||
: new TerserPlugin({
|
||||
minify: TerserPlugin.swcMinify,
|
||||
// `terserOptions` options will be passed to `swc`
|
||||
terserOptions: {
|
||||
module: true,
|
||||
mangle: false,
|
||||
},
|
||||
}),
|
||||
],
|
||||
runtimeChunk: false,
|
||||
concatenateModules: true,
|
||||
};
|
||||
|
||||
config.performance = {
|
||||
...config.performance,
|
||||
hints: false,
|
||||
};
|
||||
|
||||
config.experiments = { ...config.experiments, cacheUnaffected: true };
|
||||
|
||||
config.ignoreWarnings = [
|
||||
(x) =>
|
||||
IGNORED_WEBPACK_WARNINGS.some((r) =>
|
||||
typeof x === 'string' ? r.test(x) : r.test(x.message)
|
||||
),
|
||||
];
|
||||
|
||||
config.module = {
|
||||
...config.module,
|
||||
// Enabled for performance
|
||||
unsafeCache: true,
|
||||
rules: [
|
||||
...(config?.module?.rules ?? []),
|
||||
options.sourceMap && {
|
||||
test: /\.js$/,
|
||||
enforce: 'pre' as const,
|
||||
loader: require.resolve('source-map-loader'),
|
||||
},
|
||||
{
|
||||
// There's an issue resolving paths without fully specified extensions
|
||||
// See: https://github.com/graphql/graphql-js/issues/2721
|
||||
// TODO(jack): Add a flag to turn this option on like Next.js does via experimental flag.
|
||||
// See: https://github.com/vercel/next.js/pull/29880
|
||||
test: /\.m?jsx?$/,
|
||||
resolve: {
|
||||
fullySpecified: false,
|
||||
},
|
||||
},
|
||||
// There's an issue when using buildable libs and .js files (instead of .ts files),
|
||||
// where the wrong type is used (commonjs vs esm) resulting in export-imports throwing errors.
|
||||
// See: https://github.com/nrwl/nx/issues/10990
|
||||
{
|
||||
test: /\.js$/,
|
||||
type: 'javascript/auto',
|
||||
},
|
||||
createLoaderFromCompiler(options),
|
||||
].filter((r) => !!r),
|
||||
};
|
||||
|
||||
config.stats = {
|
||||
hash: true,
|
||||
timings: false,
|
||||
cached: false,
|
||||
cachedAssets: false,
|
||||
modules: false,
|
||||
warnings: true,
|
||||
errors: true,
|
||||
colors: !options.verbose && !options.statsJson,
|
||||
chunks: !options.verbose,
|
||||
assets: !!options.verbose,
|
||||
chunkOrigins: !!options.verbose,
|
||||
chunkModules: !!options.verbose,
|
||||
children: !!options.verbose,
|
||||
reasons: !!options.verbose,
|
||||
version: !!options.verbose,
|
||||
errorDetails: !!options.verbose,
|
||||
moduleTrace: !!options.verbose,
|
||||
usedExports: !!options.verbose,
|
||||
};
|
||||
|
||||
config.plugins ??= [];
|
||||
config.plugins.push(...plugins);
|
||||
}
|
||||
@ -0,0 +1,402 @@
|
||||
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 { NormalizedNxWebpackPluginOptions } from '../nx-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: NormalizedNxWebpackPluginOptions,
|
||||
config: Partial<WebpackOptionsNormalized | Configuration> = {}
|
||||
): void {
|
||||
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 entry: { [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 = path.resolve(options.root, style.input);
|
||||
// Add style entry points.
|
||||
if (entry[style.bundleName]) {
|
||||
entry[style.bundleName].import.push(resolvedPath);
|
||||
} else {
|
||||
entry[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, '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, '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: require.resolve('stylus-loader'),
|
||||
options: {
|
||||
sourceMap: !!options.sourceMap,
|
||||
stylusOptions: {
|
||||
include: includePaths,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const rules: RuleSetRule[] = [
|
||||
{
|
||||
test: /\.css$|\.scss$|\.sass$|\.less$|\.styl$/,
|
||||
oneOf: [...cssModuleRules, ...globalCssRules, ...globalStyleRules],
|
||||
},
|
||||
];
|
||||
|
||||
plugins.push(
|
||||
// extract global css from js files into own css file
|
||||
new MiniCssExtractPlugin({
|
||||
filename: `[name]${hashFormat.extract}.css`,
|
||||
})
|
||||
);
|
||||
|
||||
config.output = {
|
||||
...config.output,
|
||||
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.');
|
||||
|
||||
config.entry = { ...config.entry, ...entry };
|
||||
|
||||
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
|
||||
},
|
||||
},
|
||||
generator: {
|
||||
filename: `[name]${hashFormat.file}[ext]`,
|
||||
},
|
||||
},
|
||||
// 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
|
||||
},
|
||||
},
|
||||
generator: {
|
||||
filename: `[name]${hashFormat.file}[ext]`,
|
||||
},
|
||||
},
|
||||
// Fonts: Emit separate file and export the URL.
|
||||
{
|
||||
test: /\.(eot|otf|ttf|woff|woff2)$/,
|
||||
type: 'asset/resource',
|
||||
generator: {
|
||||
filename: `[name]${hashFormat.file}[ext]`,
|
||||
},
|
||||
},
|
||||
...rules,
|
||||
],
|
||||
};
|
||||
|
||||
config.plugins ??= [];
|
||||
config.plugins.push(...plugins);
|
||||
}
|
||||
@ -0,0 +1,87 @@
|
||||
import * as path from 'path';
|
||||
import { readTsConfig } from '@nx/js';
|
||||
|
||||
import { NormalizedNxWebpackPluginOptions } from '../nx-webpack-plugin-options';
|
||||
|
||||
export function createLoaderFromCompiler(
|
||||
options: NormalizedNxWebpackPluginOptions
|
||||
) {
|
||||
switch (options.compiler) {
|
||||
case 'swc':
|
||||
return {
|
||||
test: /\.([jt])sx?$/,
|
||||
loader: require.resolve('swc-loader'),
|
||||
exclude: /node_modules/,
|
||||
options: {
|
||||
jsc: {
|
||||
parser: {
|
||||
syntax: 'typescript',
|
||||
decorators: true,
|
||||
tsx: true,
|
||||
},
|
||||
transform: {
|
||||
react: {
|
||||
runtime: 'automatic',
|
||||
},
|
||||
},
|
||||
loose: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
case 'tsc':
|
||||
const { loadTsTransformers } = require('@nx/js');
|
||||
const { compilerPluginHooks, hasPlugin } = loadTsTransformers(
|
||||
options.transformers
|
||||
);
|
||||
return {
|
||||
test: /\.([jt])sx?$/,
|
||||
loader: require.resolve(`ts-loader`),
|
||||
exclude: /node_modules/,
|
||||
options: {
|
||||
configFile: options.tsConfig,
|
||||
transpileOnly: !hasPlugin,
|
||||
// https://github.com/TypeStrong/ts-loader/pull/685
|
||||
experimentalWatchApi: true,
|
||||
getCustomTransformers: (program) => ({
|
||||
before: compilerPluginHooks.beforeHooks.map((hook) =>
|
||||
hook(program)
|
||||
),
|
||||
after: compilerPluginHooks.afterHooks.map((hook) => hook(program)),
|
||||
afterDeclarations: compilerPluginHooks.afterDeclarationsHooks.map(
|
||||
(hook) => hook(program)
|
||||
),
|
||||
}),
|
||||
},
|
||||
};
|
||||
case 'babel':
|
||||
const tsConfig = readTsConfig(options.tsConfig);
|
||||
|
||||
const babelConfig = {
|
||||
test: /\.([jt])sx?$/,
|
||||
loader: path.join(__dirname, '../../../utils/web-babel-loader'),
|
||||
exclude: /node_modules/,
|
||||
options: {
|
||||
cwd: path.join(options.root, options.sourceRoot),
|
||||
emitDecoratorMetadata: tsConfig.options.emitDecoratorMetadata,
|
||||
isModern: true,
|
||||
isTest: process.env.NX_CYPRESS_COMPONENT_TEST === 'true',
|
||||
envName: process.env.BABEL_ENV ?? process.env.NODE_ENV,
|
||||
cacheDirectory: true,
|
||||
cacheCompression: false,
|
||||
},
|
||||
};
|
||||
|
||||
if (options.babelUpwardRootMode) {
|
||||
babelConfig.options['rootMode'] = 'upward';
|
||||
babelConfig.options['babelrc'] = true;
|
||||
} else {
|
||||
babelConfig.options['configFile'] = options.babelConfig
|
||||
? path.join(options.root, options.babelConfig)
|
||||
: path.join(options.root, options.projectRoot, '.babelrc');
|
||||
}
|
||||
|
||||
return babelConfig;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import browserslist = require('browserslist');
|
||||
|
||||
const VALID_BROWSERSLIST_FILES = ['.browserslistrc', 'browserslist'];
|
||||
|
||||
const ES5_BROWSERS = [
|
||||
'ie 10',
|
||||
'ie 11',
|
||||
'safari 11',
|
||||
'safari 11.1',
|
||||
'safari 12',
|
||||
'safari 12.1',
|
||||
'safari 13',
|
||||
'ios_saf 13.0',
|
||||
'ios_saf 13.3',
|
||||
];
|
||||
|
||||
export function getTerserEcmaVersion(projectRoot: string): 2020 | 5 {
|
||||
let pathToBrowserslistFile = '';
|
||||
for (const browserslistFile of VALID_BROWSERSLIST_FILES) {
|
||||
const fullPathToFile = path.join(projectRoot, browserslistFile);
|
||||
if (fs.existsSync(fullPathToFile)) {
|
||||
pathToBrowserslistFile = fullPathToFile;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!pathToBrowserslistFile) {
|
||||
return 2020;
|
||||
}
|
||||
|
||||
const env = browserslist.loadConfig({ path: pathToBrowserslistFile });
|
||||
const browsers = browserslist(env);
|
||||
return browsers.some((b) => ES5_BROWSERS.includes(b)) ? 5 : 2020;
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
import * as path from 'path';
|
||||
import { WebpackPluginInstance } from 'webpack';
|
||||
|
||||
import { getOutputHashFormat } from '../../../utils/hash-format';
|
||||
import { ScriptsWebpackPlugin } from '../../../utils/webpack/plugins/scripts-webpack-plugin';
|
||||
import { normalizeExtraEntryPoints } from '../../../utils/webpack/normalize-entry';
|
||||
import { NormalizedNxWebpackPluginOptions } from '../nx-webpack-plugin-options';
|
||||
|
||||
export function instantiateScriptPlugins(
|
||||
options: NormalizedNxWebpackPluginOptions
|
||||
): WebpackPluginInstance[] {
|
||||
// process global scripts
|
||||
const globalScriptsByBundleName = normalizeExtraEntryPoints(
|
||||
options.scripts || [],
|
||||
'scripts'
|
||||
).reduce(
|
||||
(
|
||||
prev: { inject: boolean; bundleName: string; paths: string[] }[],
|
||||
curr
|
||||
) => {
|
||||
const bundleName = curr.bundleName;
|
||||
const resolvedPath = path.resolve(options.root, curr.input);
|
||||
const existingEntry = prev.find((el) => el.bundleName === bundleName);
|
||||
if (existingEntry) {
|
||||
existingEntry.paths.push(resolvedPath);
|
||||
} else {
|
||||
prev.push({
|
||||
inject: curr.inject,
|
||||
bundleName,
|
||||
paths: [resolvedPath],
|
||||
});
|
||||
}
|
||||
|
||||
return prev;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const hashFormat = getOutputHashFormat(options.outputHashing as string);
|
||||
const plugins = [];
|
||||
// Add a new asset for each entry.
|
||||
globalScriptsByBundleName.forEach((script) => {
|
||||
const hash = script.inject ? hashFormat.script : '';
|
||||
const bundleName = script.bundleName;
|
||||
|
||||
plugins.push(
|
||||
new ScriptsWebpackPlugin({
|
||||
name: bundleName,
|
||||
sourceMap: !!options.sourceMap,
|
||||
filename: `${path.basename(bundleName)}${hash}.js`,
|
||||
scripts: script.paths,
|
||||
basePath: options.sourceRoot,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
return plugins;
|
||||
}
|
||||
@ -0,0 +1,155 @@
|
||||
import { basename, dirname, relative, resolve } from 'path';
|
||||
import { statSync } from 'fs';
|
||||
import {
|
||||
normalizePath,
|
||||
readCachedProjectGraph,
|
||||
workspaceRoot,
|
||||
} from '@nx/devkit';
|
||||
import {
|
||||
AssetGlobPattern,
|
||||
FileReplacement,
|
||||
NormalizedNxWebpackPluginOptions,
|
||||
NxWebpackPluginOptions,
|
||||
} from '../nx-webpack-plugin-options';
|
||||
|
||||
export function normalizeOptions(
|
||||
options: NxWebpackPluginOptions
|
||||
): NormalizedNxWebpackPluginOptions {
|
||||
const combinedOptions: Partial<NormalizedNxWebpackPluginOptions> = {};
|
||||
const isProd = process.env.NODE_ENV === 'production';
|
||||
const projectName = process.env.NX_TASK_TARGET_PROJECT;
|
||||
const targetName = process.env.NX_TASK_TARGET_TARGET;
|
||||
const configurationName = process.env.NX_TASK_TARGET_CONFIGURATION;
|
||||
|
||||
// Since this is invoked by the executor, the graph has already been created and cached.
|
||||
const projectGraph = readCachedProjectGraph();
|
||||
|
||||
const projectNode = projectGraph.nodes[projectName];
|
||||
const targetConfig = projectNode.data.targets[targetName];
|
||||
|
||||
// Merge options from `@nx/webpack:webpack` into plugin options.
|
||||
// Options from `@nx/webpack:webpack` take precedence.
|
||||
const originalTargetOptions = targetConfig.options;
|
||||
if (configurationName) {
|
||||
Object.assign(
|
||||
originalTargetOptions,
|
||||
targetConfig.configurations?.[configurationName]
|
||||
);
|
||||
}
|
||||
// This could be called from dev-server which means we need to read `buildTarget` to get actual build options.
|
||||
// Otherwise, the options are passed from the `@nx/webpack:webpack` executor.
|
||||
if (originalTargetOptions.buildTarget) {
|
||||
const buildTargetOptions = targetConfig.options;
|
||||
if (configurationName) {
|
||||
Object.assign(
|
||||
buildTargetOptions,
|
||||
targetConfig.configurations?.[configurationName]
|
||||
);
|
||||
}
|
||||
Object.assign(combinedOptions, buildTargetOptions);
|
||||
} else {
|
||||
Object.assign(combinedOptions, originalTargetOptions, options);
|
||||
}
|
||||
|
||||
const sourceRoot = projectNode.data.sourceRoot ?? projectNode.data.root;
|
||||
|
||||
if (!options.main)
|
||||
throw new Error(
|
||||
`Missing "main" option for the entry file. Set this option in your Nx webpack plugin.`
|
||||
);
|
||||
|
||||
return {
|
||||
...options,
|
||||
assets: options.assets
|
||||
? normalizeAssets(options.assets, workspaceRoot, sourceRoot)
|
||||
: [],
|
||||
baseHref: options.baseHref ?? '/',
|
||||
commonChunk: options.commonChunk ?? true,
|
||||
compiler: options.compiler ?? 'babel',
|
||||
configurationName,
|
||||
deleteOutputPath: options.deleteOutputPath ?? true,
|
||||
extractCss: options.extractCss ?? true,
|
||||
fileReplacements: normalizeFileReplacements(
|
||||
workspaceRoot,
|
||||
options.fileReplacements
|
||||
),
|
||||
generateIndexHtml: options.generateIndexHtml ?? true,
|
||||
main: options.main,
|
||||
namedChunks: options.namedChunks ?? !isProd,
|
||||
optimization: options.optimization ?? isProd,
|
||||
outputFileName: options.outputFileName ?? 'main.js',
|
||||
outputHashing: options.outputHashing ?? (isProd ? 'all' : 'none'),
|
||||
outputPath: options.outputPath,
|
||||
projectGraph,
|
||||
projectName,
|
||||
projectRoot: projectNode.data.root,
|
||||
root: workspaceRoot,
|
||||
runtimeChunk: options.runtimeChunk ?? true,
|
||||
scripts: options.scripts ?? [],
|
||||
sourceMap: options.sourceMap ?? !isProd,
|
||||
sourceRoot,
|
||||
styles: options.styles ?? [],
|
||||
target: options.target ?? 'web',
|
||||
targetName,
|
||||
vendorChunk: options.vendorChunk ?? !isProd,
|
||||
};
|
||||
}
|
||||
|
||||
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(/^\//, ''),
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeFileReplacements(
|
||||
root: string,
|
||||
fileReplacements: FileReplacement[]
|
||||
): FileReplacement[] {
|
||||
return fileReplacements
|
||||
? fileReplacements.map((fileReplacement) => ({
|
||||
replace: resolve(root, fileReplacement.replace),
|
||||
with: resolve(root, fileReplacement.with),
|
||||
}))
|
||||
: [];
|
||||
}
|
||||
@ -0,0 +1,142 @@
|
||||
import * as path from 'path';
|
||||
import autoprefixer = require('autoprefixer');
|
||||
import postcssImports = require('postcss-import');
|
||||
import MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
|
||||
import { getCSSModuleLocalIdent } from '../../../utils/get-css-module-local-ident';
|
||||
import { getOutputHashFormat } from '../../../utils/hash-format';
|
||||
import { NormalizedNxWebpackPluginOptions } from '../nx-webpack-plugin-options';
|
||||
import { PostcssCliResources } from '../../../utils/webpack/plugins/postcss-cli-resources';
|
||||
|
||||
interface PostcssOptions {
|
||||
(loader: any): any;
|
||||
|
||||
config?: string;
|
||||
}
|
||||
|
||||
export function getCommonLoadersForCssModules(
|
||||
options: NormalizedNxWebpackPluginOptions,
|
||||
includePaths: string[]
|
||||
) {
|
||||
// load component css as raw strings
|
||||
return [
|
||||
{
|
||||
loader: options.extractCss
|
||||
? MiniCssExtractPlugin.loader
|
||||
: require.resolve('style-loader'),
|
||||
},
|
||||
{
|
||||
loader: require.resolve('css-loader'),
|
||||
options: {
|
||||
modules: {
|
||||
mode: 'local',
|
||||
getLocalIdent: getCSSModuleLocalIdent,
|
||||
},
|
||||
importLoaders: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: require.resolve('postcss-loader'),
|
||||
options: {
|
||||
implementation: require('postcss'),
|
||||
postcssOptions: postcssOptionsCreator(options, {
|
||||
includePaths,
|
||||
forCssModules: true,
|
||||
}),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function getCommonLoadersForGlobalCss(
|
||||
options: NormalizedNxWebpackPluginOptions,
|
||||
includePaths: string[]
|
||||
) {
|
||||
return [
|
||||
{
|
||||
loader: options.extractCss
|
||||
? MiniCssExtractPlugin.loader
|
||||
: require.resolve('style-loader'),
|
||||
},
|
||||
{ loader: require.resolve('css-loader'), options: { url: false } },
|
||||
{
|
||||
loader: require.resolve('postcss-loader'),
|
||||
options: {
|
||||
implementation: require('postcss'),
|
||||
postcssOptions: postcssOptionsCreator(options, {
|
||||
includePaths,
|
||||
}),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function getCommonLoadersForGlobalStyle(
|
||||
options: NormalizedNxWebpackPluginOptions,
|
||||
includePaths: string[]
|
||||
) {
|
||||
return [
|
||||
{
|
||||
loader: MiniCssExtractPlugin.loader,
|
||||
options: { esModule: true },
|
||||
},
|
||||
{ loader: require.resolve('css-loader'), options: { url: false } },
|
||||
{
|
||||
loader: require.resolve('postcss-loader'),
|
||||
options: {
|
||||
implementation: require('postcss'),
|
||||
postcssOptions: postcssOptionsCreator(options, {
|
||||
includePaths,
|
||||
}),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function postcssOptionsCreator(
|
||||
options: NormalizedNxWebpackPluginOptions,
|
||||
{
|
||||
includePaths,
|
||||
forCssModules = false,
|
||||
}: {
|
||||
includePaths: string[];
|
||||
forCssModules?: boolean;
|
||||
}
|
||||
) {
|
||||
const hashFormat = getOutputHashFormat(options.outputHashing as string);
|
||||
// PostCSS options depend on the webpack loader, but we need to set the `config` path as a string due to this check:
|
||||
// https://github.com/webpack-contrib/postcss-loader/blob/0d342b1/src/utils.js#L36
|
||||
|
||||
const postcssOptions: PostcssOptions = (loader) => ({
|
||||
map: options.sourceMap &&
|
||||
options.sourceMap !== 'hidden' && {
|
||||
inline: true,
|
||||
annotation: false,
|
||||
},
|
||||
plugins: [
|
||||
postcssImports({
|
||||
addModulesDirectories: includePaths,
|
||||
resolve: (url: string) => (url.startsWith('~') ? url.slice(1) : url),
|
||||
}),
|
||||
...(forCssModules
|
||||
? []
|
||||
: [
|
||||
PostcssCliResources({
|
||||
baseHref: options.baseHref,
|
||||
deployUrl: options.deployUrl,
|
||||
loader,
|
||||
filename: `[name]${hashFormat.file}.[ext]`,
|
||||
}),
|
||||
autoprefixer(),
|
||||
]),
|
||||
],
|
||||
});
|
||||
|
||||
// If a path to postcssConfig is passed in, set it for app and all libs, otherwise
|
||||
// use automatic detection.
|
||||
if (typeof options.postcssConfig === 'string') {
|
||||
postcssOptions.config = path.join(options.root, options.postcssConfig);
|
||||
}
|
||||
|
||||
return postcssOptions;
|
||||
}
|
||||
@ -0,0 +1,100 @@
|
||||
import { ProjectGraph } from '@nx/devkit';
|
||||
import { AssetGlob } from '@nx/js/src/utils/assets/assets';
|
||||
|
||||
export interface AssetGlobPattern {
|
||||
glob: string;
|
||||
input: string;
|
||||
output: string;
|
||||
ignore?: string[];
|
||||
}
|
||||
|
||||
export interface ExtraEntryPointClass {
|
||||
bundleName?: string;
|
||||
inject?: boolean;
|
||||
input: string;
|
||||
lazy?: boolean;
|
||||
}
|
||||
|
||||
export interface FileReplacement {
|
||||
replace: string;
|
||||
with: string;
|
||||
}
|
||||
|
||||
export interface AdditionalEntryPoint {
|
||||
entryName: string;
|
||||
entryPath: string;
|
||||
}
|
||||
|
||||
export interface TransformerPlugin {
|
||||
name: string;
|
||||
options: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export type TransformerEntry = string | TransformerPlugin;
|
||||
|
||||
export interface OptimizationOptions {
|
||||
scripts: boolean;
|
||||
styles: boolean;
|
||||
}
|
||||
|
||||
export interface NxWebpackPluginOptions {
|
||||
// Required options
|
||||
main: string;
|
||||
outputPath: string;
|
||||
tsConfig: string;
|
||||
|
||||
// Optional options
|
||||
additionalEntryPoints?: AdditionalEntryPoint[];
|
||||
assets?: Array<AssetGlob | string>;
|
||||
babelConfig?: string;
|
||||
babelUpwardRootMode?: boolean;
|
||||
baseHref?: string;
|
||||
commonChunk?: boolean;
|
||||
compiler?: 'babel' | 'swc' | 'tsc';
|
||||
crossOrigin?: 'none' | 'anonymous' | 'use-credentials';
|
||||
deleteOutputPath?: boolean;
|
||||
deployUrl?: string;
|
||||
externalDependencies?: 'all' | 'none' | string[];
|
||||
extractCss?: boolean;
|
||||
extractLicenses?: boolean;
|
||||
fileReplacements?: FileReplacement[];
|
||||
generateIndexHtml?: boolean;
|
||||
generatePackageJson?: boolean;
|
||||
index?: string;
|
||||
memoryLimit?: number;
|
||||
namedChunks?: boolean;
|
||||
optimization?: boolean | OptimizationOptions;
|
||||
outputFileName?: string;
|
||||
outputHashing?: any;
|
||||
poll?: number;
|
||||
polyfills?: string;
|
||||
postcssConfig?: string;
|
||||
progress?: boolean;
|
||||
runtimeChunk?: boolean;
|
||||
scripts?: Array<ExtraEntryPointClass | string>;
|
||||
skipTypeChecking?: boolean;
|
||||
sourceMap?: boolean | 'hidden';
|
||||
ssr?: boolean;
|
||||
statsJson?: boolean;
|
||||
stylePreprocessorOptions?: any;
|
||||
styles?: Array<ExtraEntryPointClass | string>;
|
||||
subresourceIntegrity?: boolean;
|
||||
target?: string | string[];
|
||||
transformers?: TransformerEntry[];
|
||||
vendorChunk?: boolean;
|
||||
verbose?: boolean;
|
||||
watch?: boolean;
|
||||
}
|
||||
|
||||
export interface NormalizedNxWebpackPluginOptions
|
||||
extends NxWebpackPluginOptions {
|
||||
projectName: string;
|
||||
root: string;
|
||||
projectRoot: string;
|
||||
sourceRoot: string;
|
||||
configurationName: string;
|
||||
targetName: string;
|
||||
projectGraph: ProjectGraph;
|
||||
outputFileName: string;
|
||||
assets: AssetGlobPattern[];
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
import { Compiler } from 'webpack';
|
||||
import {
|
||||
NormalizedNxWebpackPluginOptions,
|
||||
NxWebpackPluginOptions,
|
||||
} from './nx-webpack-plugin-options';
|
||||
import { normalizeOptions } from './lib/normalize-options';
|
||||
import { deleteOutputDir } from '../../utils/fs';
|
||||
import { applyBaseConfig } from './lib/apply-base-config';
|
||||
import { applyWebConfig } from './lib/apply-web-config';
|
||||
|
||||
/**
|
||||
* This plugin provides features to build Node and Web applications.
|
||||
* - TS support (including tsconfig paths)
|
||||
* - Different compiler options
|
||||
* - Assets handling
|
||||
* - Stylesheets handling
|
||||
* - index.html and package.json generation
|
||||
*
|
||||
* Web-only features, such as stylesheets and images, are only supported when `target` is 'web' or 'webworker'.
|
||||
*/
|
||||
export class NxWebpackPlugin {
|
||||
private readonly options: NormalizedNxWebpackPluginOptions;
|
||||
|
||||
constructor(options: NxWebpackPluginOptions) {
|
||||
this.options = normalizeOptions({
|
||||
...options,
|
||||
...this.readExecutorOptions(),
|
||||
});
|
||||
}
|
||||
|
||||
apply(compiler: Compiler): void {
|
||||
const target = this.options.target ?? compiler.options.target;
|
||||
this.options.outputPath ??= compiler.options.output?.path;
|
||||
if (typeof target === 'string') {
|
||||
this.options.target = target;
|
||||
}
|
||||
|
||||
if (this.options.deleteOutputPath) {
|
||||
deleteOutputDir(this.options.root, this.options.outputPath);
|
||||
}
|
||||
|
||||
applyBaseConfig(this.options, compiler.options);
|
||||
|
||||
if (compiler.options.target) {
|
||||
this.options.target = compiler.options.target;
|
||||
}
|
||||
|
||||
if (this.options.target === 'web' || this.options.target === 'webworker') {
|
||||
applyWebConfig(this.options, compiler.options);
|
||||
}
|
||||
}
|
||||
|
||||
private readExecutorOptions() {
|
||||
const fromExecutor = process.env['NX_WEBPACK_EXECUTOR_RAW_OPTIONS'] ?? '{}';
|
||||
try {
|
||||
return JSON.parse(fromExecutor);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,63 +1,6 @@
|
||||
import * as path from 'path';
|
||||
import { join } from 'path';
|
||||
import { Configuration, ProgressPlugin, WebpackPluginInstance } from 'webpack';
|
||||
import { ExecutorContext } from 'nx/src/config/misc-interfaces';
|
||||
import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin';
|
||||
import { readTsConfig } from '@nx/js';
|
||||
import { LicenseWebpackPlugin } from 'license-webpack-plugin';
|
||||
|
||||
import { NormalizedWebpackExecutorOptions } from '../executors/webpack/schema';
|
||||
import { StatsJsonPlugin } from '../plugins/stats-json-plugin';
|
||||
import { createCopyPlugin } from './create-copy-plugin';
|
||||
import { GeneratePackageJsonPlugin } from '../plugins/generate-package-json-plugin';
|
||||
import { getOutputHashFormat } from './hash-format';
|
||||
import { NxWebpackPlugin } from './config';
|
||||
import { existsSync } from 'fs';
|
||||
import TerserPlugin = require('terser-webpack-plugin');
|
||||
import nodeExternals = require('webpack-node-externals');
|
||||
import ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
|
||||
import browserslist = require('browserslist');
|
||||
|
||||
const VALID_BROWSERSLIST_FILES = ['.browserslistrc', 'browserslist'];
|
||||
|
||||
const ES5_BROWSERS = [
|
||||
'ie 10',
|
||||
'ie 11',
|
||||
'safari 11',
|
||||
'safari 11.1',
|
||||
'safari 12',
|
||||
'safari 12.1',
|
||||
'safari 13',
|
||||
'ios_saf 13.0',
|
||||
'ios_saf 13.3',
|
||||
];
|
||||
|
||||
function getTerserEcmaVersion(projectRoot: string) {
|
||||
let pathToBrowserslistFile = '';
|
||||
for (const browserslistFile of VALID_BROWSERSLIST_FILES) {
|
||||
const fullPathToFile = join(projectRoot, browserslistFile);
|
||||
if (existsSync(fullPathToFile)) {
|
||||
pathToBrowserslistFile = fullPathToFile;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!pathToBrowserslistFile) {
|
||||
return 2020;
|
||||
}
|
||||
|
||||
const env = browserslist.loadConfig({ path: pathToBrowserslistFile });
|
||||
const browsers = browserslist(env);
|
||||
return browsers.some((b) => ES5_BROWSERS.includes(b)) ? 5 : 2020;
|
||||
}
|
||||
|
||||
const IGNORED_WEBPACK_WARNINGS = [
|
||||
/The comment file/i,
|
||||
/could not find any license/i,
|
||||
];
|
||||
|
||||
const extensions = ['.ts', '.tsx', '.mjs', '.js', '.jsx'];
|
||||
const mainFields = ['module', 'main'];
|
||||
import { Configuration } from 'webpack';
|
||||
import { NxWebpackExecutionContext, NxWebpackPlugin } from './config';
|
||||
import { applyBaseConfig } from '../plugins/nx-webpack-plugin/lib/apply-base-config';
|
||||
|
||||
const processed = new Set();
|
||||
|
||||
@ -72,362 +15,24 @@ export interface WithNxOptions {
|
||||
export function withNx(pluginOptions?: WithNxOptions): NxWebpackPlugin {
|
||||
return function configure(
|
||||
config: Configuration,
|
||||
{
|
||||
options,
|
||||
context,
|
||||
}: {
|
||||
options: NormalizedWebpackExecutorOptions;
|
||||
context: ExecutorContext;
|
||||
}
|
||||
{ options, context }: NxWebpackExecutionContext
|
||||
): Configuration {
|
||||
if (processed.has(config)) return config;
|
||||
|
||||
const plugins: WebpackPluginInstance[] = [];
|
||||
|
||||
if (!pluginOptions?.skipTypeChecking) {
|
||||
plugins.push(
|
||||
new ForkTsCheckerWebpackPlugin({
|
||||
typescript: {
|
||||
configFile: options.tsConfig,
|
||||
memoryLimit: options.memoryLimit || 2018,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
const entry = {};
|
||||
|
||||
if (options.main) {
|
||||
const mainEntry = options.outputFileName
|
||||
? path.parse(options.outputFileName).name
|
||||
: 'main';
|
||||
entry[mainEntry] = [options.main];
|
||||
}
|
||||
|
||||
if (options.additionalEntryPoints) {
|
||||
for (const { entryName, entryPath } of options.additionalEntryPoints) {
|
||||
entry[entryName] = entryPath;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.polyfills) {
|
||||
entry['polyfills'] = [
|
||||
...(entry['polyfills'] || []),
|
||||
path.resolve(options.root, options.polyfills),
|
||||
];
|
||||
}
|
||||
|
||||
if (options.progress) {
|
||||
plugins.push(new ProgressPlugin({ profile: options.verbose }));
|
||||
}
|
||||
|
||||
if (options.extractLicenses) {
|
||||
plugins.push(
|
||||
new LicenseWebpackPlugin({
|
||||
stats: {
|
||||
warnings: false,
|
||||
errors: false,
|
||||
},
|
||||
perChunkOutput: false,
|
||||
outputFilename: `3rdpartylicenses.txt`,
|
||||
}) as unknown as WebpackPluginInstance
|
||||
);
|
||||
}
|
||||
|
||||
if (Array.isArray(options.assets) && options.assets.length > 0) {
|
||||
plugins.push(createCopyPlugin(options.assets));
|
||||
}
|
||||
|
||||
if (options.generatePackageJson && context) {
|
||||
plugins.push(new GeneratePackageJsonPlugin(options, context));
|
||||
}
|
||||
|
||||
if (options.statsJson) {
|
||||
plugins.push(new StatsJsonPlugin());
|
||||
}
|
||||
|
||||
let externals = [];
|
||||
if (options.target === 'node' && options.externalDependencies === 'all') {
|
||||
const modulesDir = `${options.root}/node_modules`;
|
||||
externals.push(nodeExternals({ modulesDir }));
|
||||
} else if (Array.isArray(options.externalDependencies)) {
|
||||
externals.push(function (ctx, callback: Function) {
|
||||
if (options.externalDependencies.includes(ctx.request)) {
|
||||
// not bundled
|
||||
return callback(null, `commonjs ${ctx.request}`);
|
||||
}
|
||||
// bundled
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
const hashFormat = getOutputHashFormat(options.outputHashing as string);
|
||||
const filename = options.outputHashing
|
||||
? `[name]${hashFormat.script}.js`
|
||||
: '[name].js';
|
||||
const chunkFilename = options.outputHashing
|
||||
? `[name]${hashFormat.chunk}.js`
|
||||
: '[name].js';
|
||||
|
||||
const updated = {
|
||||
...config,
|
||||
context: context
|
||||
? path.join(context.root, options.projectRoot)
|
||||
: undefined,
|
||||
target: options.target,
|
||||
node: false as const,
|
||||
mode:
|
||||
// When the target is Node avoid any optimizations, such as replacing `process.env.NODE_ENV` with build time value.
|
||||
options.target === ('node' as const)
|
||||
? 'none'
|
||||
: // Otherwise, make sure it matches `process.env.NODE_ENV`.
|
||||
// When mode is development or production, webpack will automatically
|
||||
// configure DefinePlugin to replace `process.env.NODE_ENV` with the
|
||||
// build-time value. Thus, we need to make sure it's the same value to
|
||||
// avoid conflicts.
|
||||
//
|
||||
// When the NODE_ENV is something else (e.g. test), then set it to none
|
||||
// to prevent extra behavior from webpack.
|
||||
process.env.NODE_ENV === 'development' ||
|
||||
process.env.NODE_ENV === 'production'
|
||||
? (process.env.NODE_ENV as 'development' | 'production')
|
||||
: ('none' as const),
|
||||
// When target is Node, the Webpack mode will be set to 'none' which disables in memory caching and causes a full rebuild on every change.
|
||||
// So to mitigate this we enable in memory caching when target is Node and in watch mode.
|
||||
cache:
|
||||
options.target === ('node' as const) && options.watch
|
||||
? { type: 'memory' as const }
|
||||
: undefined,
|
||||
devtool:
|
||||
options.sourceMap === 'hidden'
|
||||
? 'hidden-source-map'
|
||||
: options.sourceMap
|
||||
? 'source-map'
|
||||
: (false as const),
|
||||
entry,
|
||||
output: {
|
||||
...config.output,
|
||||
libraryTarget: options.target === 'node' ? 'commonjs' : undefined,
|
||||
path: options.outputPath,
|
||||
filename,
|
||||
chunkFilename,
|
||||
hashFunction: 'xxhash64',
|
||||
// Disabled for performance
|
||||
pathinfo: false,
|
||||
// Use CJS for Node since it has the widest support.
|
||||
scriptType: options.target === 'node' ? undefined : ('module' as const),
|
||||
applyBaseConfig(
|
||||
{
|
||||
...options,
|
||||
...pluginOptions,
|
||||
root: context.root,
|
||||
projectName: context.projectName,
|
||||
targetName: context.targetName,
|
||||
configurationName: context.configurationName,
|
||||
projectGraph: context.projectGraph,
|
||||
},
|
||||
watch: options.watch,
|
||||
watchOptions: {
|
||||
poll: options.poll,
|
||||
},
|
||||
profile: options.statsJson,
|
||||
resolve: {
|
||||
...config.resolve,
|
||||
extensions: [...extensions, ...(config?.resolve?.extensions ?? [])],
|
||||
alias: options.fileReplacements.reduce(
|
||||
(aliases, replacement) => ({
|
||||
...aliases,
|
||||
[replacement.replace]: replacement.with,
|
||||
}),
|
||||
{}
|
||||
),
|
||||
plugins: [
|
||||
...(config.resolve?.plugins ?? []),
|
||||
new TsconfigPathsPlugin({
|
||||
configFile: options.tsConfig,
|
||||
extensions: [...extensions, ...(config?.resolve?.extensions ?? [])],
|
||||
mainFields,
|
||||
}),
|
||||
],
|
||||
mainFields,
|
||||
},
|
||||
externals,
|
||||
optimization: {
|
||||
...config.optimization,
|
||||
sideEffects: true,
|
||||
minimize:
|
||||
typeof options.optimization === 'object'
|
||||
? !!options.optimization.scripts
|
||||
: !!options.optimization,
|
||||
minimizer: [
|
||||
options.compiler !== 'swc'
|
||||
? new TerserPlugin({
|
||||
parallel: true,
|
||||
terserOptions: {
|
||||
keep_classnames: true,
|
||||
ecma: getTerserEcmaVersion(
|
||||
join(options.root, options.projectRoot)
|
||||
),
|
||||
safari10: true,
|
||||
format: {
|
||||
ascii_only: true,
|
||||
comments: false,
|
||||
webkit: true,
|
||||
},
|
||||
},
|
||||
extractComments: false,
|
||||
})
|
||||
: new TerserPlugin({
|
||||
minify: TerserPlugin.swcMinify,
|
||||
// `terserOptions` options will be passed to `swc`
|
||||
terserOptions: {
|
||||
module: true,
|
||||
mangle: false,
|
||||
},
|
||||
}),
|
||||
],
|
||||
runtimeChunk: false,
|
||||
concatenateModules: true,
|
||||
},
|
||||
performance: {
|
||||
...config.performance,
|
||||
hints: false as const,
|
||||
},
|
||||
experiments: { ...config.experiments, cacheUnaffected: true },
|
||||
ignoreWarnings: [
|
||||
(x) =>
|
||||
IGNORED_WEBPACK_WARNINGS.some((r) =>
|
||||
typeof x === 'string' ? r.test(x) : r.test(x.message)
|
||||
),
|
||||
],
|
||||
module: {
|
||||
...config.module,
|
||||
// Enabled for performance
|
||||
unsafeCache: true,
|
||||
rules: [
|
||||
...(config?.module?.rules ?? []),
|
||||
options.sourceMap && {
|
||||
test: /\.js$/,
|
||||
enforce: 'pre' as const,
|
||||
loader: require.resolve('source-map-loader'),
|
||||
},
|
||||
{
|
||||
// There's an issue resolving paths without fully specified extensions
|
||||
// See: https://github.com/graphql/graphql-js/issues/2721
|
||||
// TODO(jack): Add a flag to turn this option on like Next.js does via experimental flag.
|
||||
// See: https://github.com/vercel/next.js/pull/29880
|
||||
test: /\.m?jsx?$/,
|
||||
resolve: {
|
||||
fullySpecified: false,
|
||||
},
|
||||
},
|
||||
// There's an issue when using buildable libs and .js files (instead of .ts files),
|
||||
// where the wrong type is used (commonjs vs esm) resulting in export-imports throwing errors.
|
||||
// See: https://github.com/nrwl/nx/issues/10990
|
||||
{
|
||||
test: /\.js$/,
|
||||
type: 'javascript/auto',
|
||||
},
|
||||
createLoaderFromCompiler(options),
|
||||
].filter((r) => !!r),
|
||||
},
|
||||
plugins: (config.plugins ?? []).concat(plugins),
|
||||
stats: {
|
||||
hash: true,
|
||||
timings: false,
|
||||
cached: false,
|
||||
cachedAssets: false,
|
||||
modules: false,
|
||||
warnings: true,
|
||||
errors: true,
|
||||
colors: !options.verbose && !options.statsJson,
|
||||
chunks: !options.verbose,
|
||||
assets: !!options.verbose,
|
||||
chunkOrigins: !!options.verbose,
|
||||
chunkModules: !!options.verbose,
|
||||
children: !!options.verbose,
|
||||
reasons: !!options.verbose,
|
||||
version: !!options.verbose,
|
||||
errorDetails: !!options.verbose,
|
||||
moduleTrace: !!options.verbose,
|
||||
usedExports: !!options.verbose,
|
||||
},
|
||||
};
|
||||
config
|
||||
);
|
||||
|
||||
processed.add(updated);
|
||||
return updated;
|
||||
processed.add(config);
|
||||
return config;
|
||||
};
|
||||
}
|
||||
|
||||
export function createLoaderFromCompiler(
|
||||
options: NormalizedWebpackExecutorOptions
|
||||
) {
|
||||
switch (options.compiler) {
|
||||
case 'swc':
|
||||
return {
|
||||
test: /\.([jt])sx?$/,
|
||||
loader: require.resolve('swc-loader'),
|
||||
exclude: /node_modules/,
|
||||
options: {
|
||||
jsc: {
|
||||
parser: {
|
||||
syntax: 'typescript',
|
||||
decorators: true,
|
||||
tsx: true,
|
||||
},
|
||||
transform: {
|
||||
react: {
|
||||
runtime: 'automatic',
|
||||
},
|
||||
},
|
||||
loose: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
case 'tsc':
|
||||
const { loadTsTransformers } = require('@nx/js');
|
||||
const { compilerPluginHooks, hasPlugin } = loadTsTransformers(
|
||||
options.transformers
|
||||
);
|
||||
return {
|
||||
test: /\.([jt])sx?$/,
|
||||
loader: require.resolve(`ts-loader`),
|
||||
exclude: /node_modules/,
|
||||
options: {
|
||||
configFile: options.tsConfig,
|
||||
transpileOnly: !hasPlugin,
|
||||
// https://github.com/TypeStrong/ts-loader/pull/685
|
||||
experimentalWatchApi: true,
|
||||
getCustomTransformers: (program) => ({
|
||||
before: compilerPluginHooks.beforeHooks.map((hook) =>
|
||||
hook(program)
|
||||
),
|
||||
after: compilerPluginHooks.afterHooks.map((hook) => hook(program)),
|
||||
afterDeclarations: compilerPluginHooks.afterDeclarationsHooks.map(
|
||||
(hook) => hook(program)
|
||||
),
|
||||
}),
|
||||
},
|
||||
};
|
||||
case 'babel':
|
||||
const tsConfig = readTsConfig(options.tsConfig);
|
||||
|
||||
const babelConfig = {
|
||||
test: /\.([jt])sx?$/,
|
||||
loader: path.join(__dirname, './web-babel-loader'),
|
||||
exclude: /node_modules/,
|
||||
options: {
|
||||
cwd: path.join(options.root, options.sourceRoot),
|
||||
emitDecoratorMetadata: tsConfig.options.emitDecoratorMetadata,
|
||||
isModern: true,
|
||||
isTest: process.env.NX_CYPRESS_COMPONENT_TEST === 'true',
|
||||
envName: process.env.BABEL_ENV ?? process.env.NODE_ENV,
|
||||
cacheDirectory: true,
|
||||
cacheCompression: false,
|
||||
},
|
||||
};
|
||||
|
||||
if (options.babelUpwardRootMode) {
|
||||
babelConfig.options['rootMode'] = 'upward';
|
||||
babelConfig.options['babelrc'] = true;
|
||||
} else {
|
||||
babelConfig.options['configFile'] = options.babelConfig
|
||||
? path.join(options.root, options.babelConfig)
|
||||
: path.join(options.root, options.projectRoot, '.babelrc');
|
||||
}
|
||||
|
||||
return babelConfig;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,36 +1,11 @@
|
||||
import * as webpack from 'webpack';
|
||||
import {
|
||||
Configuration,
|
||||
ids,
|
||||
RuleSetRule,
|
||||
WebpackPluginInstance,
|
||||
} from 'webpack';
|
||||
import { SubresourceIntegrityPlugin } from 'webpack-subresource-integrity';
|
||||
import * as path from 'path';
|
||||
import { basename, join } from 'path';
|
||||
import { getOutputHashFormat } from './hash-format';
|
||||
import { PostcssCliResources } from './webpack/plugins/postcss-cli-resources';
|
||||
import { normalizeExtraEntryPoints } from './webpack/normalize-entry';
|
||||
import { Configuration } from 'webpack';
|
||||
|
||||
import { NxWebpackExecutionContext, NxWebpackPlugin } from './config';
|
||||
import {
|
||||
ExtraEntryPointClass,
|
||||
NormalizedWebpackExecutorOptions,
|
||||
} from '../executors/webpack/schema';
|
||||
import { getClientEnvironment } from './get-client-environment';
|
||||
import { ScriptsWebpackPlugin } from './webpack/plugins/scripts-webpack-plugin';
|
||||
import { getCSSModuleLocalIdent } from './get-css-module-local-ident';
|
||||
import { WriteIndexHtmlPlugin } from '../plugins/write-index-html-plugin';
|
||||
import CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
|
||||
import MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
import autoprefixer = require('autoprefixer');
|
||||
import postcssImports = require('postcss-import');
|
||||
|
||||
interface PostcssOptions {
|
||||
(loader: any): any;
|
||||
|
||||
config?: string;
|
||||
}
|
||||
import { applyWebConfig } from '../plugins/nx-webpack-plugin/lib/apply-web-config';
|
||||
|
||||
const processed = new Set();
|
||||
|
||||
@ -63,559 +38,23 @@ export type MergedOptions = Omit<
|
||||
export function withWeb(pluginOptions: WithWebOptions = {}): NxWebpackPlugin {
|
||||
return function configure(
|
||||
config: Configuration,
|
||||
{ options: executorOptions, context }: NxWebpackExecutionContext
|
||||
{ options, context }: NxWebpackExecutionContext
|
||||
): Configuration {
|
||||
if (processed.has(config)) return config;
|
||||
|
||||
const mergedOptions: MergedOptions = {
|
||||
...executorOptions,
|
||||
...pluginOptions,
|
||||
};
|
||||
const plugins = [];
|
||||
|
||||
const stylesOptimization =
|
||||
typeof mergedOptions.optimization === 'object'
|
||||
? mergedOptions.optimization.styles
|
||||
: mergedOptions.optimization;
|
||||
|
||||
if (Array.isArray(mergedOptions.scripts)) {
|
||||
plugins.push(...createScriptsPlugin(mergedOptions));
|
||||
}
|
||||
if (mergedOptions.index && mergedOptions.generateIndexHtml) {
|
||||
plugins.push(
|
||||
new WriteIndexHtmlPlugin({
|
||||
crossOrigin: mergedOptions.crossOrigin,
|
||||
sri: mergedOptions.subresourceIntegrity,
|
||||
outputPath: basename(mergedOptions.index),
|
||||
indexPath: join(context.root, mergedOptions.index),
|
||||
baseHref: mergedOptions.baseHref,
|
||||
deployUrl: mergedOptions.deployUrl,
|
||||
scripts: mergedOptions.scripts,
|
||||
styles: mergedOptions.styles,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (mergedOptions.subresourceIntegrity) {
|
||||
plugins.push(new SubresourceIntegrityPlugin());
|
||||
}
|
||||
|
||||
const minimizer: WebpackPluginInstance[] = [
|
||||
new ids.HashedModuleIdsPlugin(),
|
||||
];
|
||||
if (stylesOptimization) {
|
||||
minimizer.push(
|
||||
new CssMinimizerPlugin({
|
||||
test: /\.(?:css|scss|sass|less|styl)$/,
|
||||
})
|
||||
);
|
||||
}
|
||||
if (!pluginOptions.ssr) {
|
||||
plugins.push(
|
||||
new webpack.DefinePlugin(
|
||||
getClientEnvironment(process.env.NODE_ENV).stringified
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const entry: { [key: string]: string[] } = {};
|
||||
const globalStylePaths: string[] = [];
|
||||
|
||||
// Determine hashing format.
|
||||
const hashFormat = getOutputHashFormat(
|
||||
mergedOptions.outputHashing as string
|
||||
applyWebConfig(
|
||||
{
|
||||
...options,
|
||||
...pluginOptions,
|
||||
projectName: context.projectName,
|
||||
targetName: context.targetName,
|
||||
configurationName: context.configurationName,
|
||||
projectGraph: context.projectGraph,
|
||||
},
|
||||
config
|
||||
);
|
||||
|
||||
const includePaths: string[] = [];
|
||||
if (mergedOptions?.stylePreprocessorOptions?.includePaths?.length > 0) {
|
||||
mergedOptions.stylePreprocessorOptions.includePaths.forEach(
|
||||
(includePath: string) =>
|
||||
includePaths.push(path.resolve(mergedOptions.root, includePath))
|
||||
);
|
||||
}
|
||||
|
||||
let lessPathOptions: { paths?: string[] } = {};
|
||||
|
||||
if (includePaths.length > 0) {
|
||||
lessPathOptions = {
|
||||
paths: includePaths,
|
||||
};
|
||||
}
|
||||
|
||||
// Process global styles.
|
||||
if (mergedOptions.styles.length > 0) {
|
||||
normalizeExtraEntryPoints(mergedOptions.styles, 'styles').forEach(
|
||||
(style) => {
|
||||
const resolvedPath = path.resolve(mergedOptions.root, style.input);
|
||||
// Add style entry points.
|
||||
if (entry[style.bundleName]) {
|
||||
entry[style.bundleName].push(resolvedPath);
|
||||
} else {
|
||||
entry[style.bundleName] = [resolvedPath];
|
||||
}
|
||||
|
||||
// Add global css paths.
|
||||
globalStylePaths.push(resolvedPath);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const cssModuleRules: RuleSetRule[] = [
|
||||
{
|
||||
test: /\.module\.css$/,
|
||||
exclude: globalStylePaths,
|
||||
use: getCommonLoadersForCssModules(mergedOptions, includePaths),
|
||||
},
|
||||
{
|
||||
test: /\.module\.(scss|sass)$/,
|
||||
exclude: globalStylePaths,
|
||||
use: [
|
||||
...getCommonLoadersForCssModules(mergedOptions, includePaths),
|
||||
{
|
||||
loader: require.resolve('sass-loader'),
|
||||
options: {
|
||||
implementation: require('sass'),
|
||||
sassOptions: {
|
||||
fiber: false,
|
||||
precision: 8,
|
||||
includePaths,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.module\.less$/,
|
||||
exclude: globalStylePaths,
|
||||
use: [
|
||||
...getCommonLoadersForCssModules(mergedOptions, includePaths),
|
||||
{
|
||||
loader: require.resolve('less-loader'),
|
||||
options: {
|
||||
lessOptions: {
|
||||
paths: includePaths,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.module\.styl$/,
|
||||
exclude: globalStylePaths,
|
||||
use: [
|
||||
...getCommonLoadersForCssModules(mergedOptions, includePaths),
|
||||
{
|
||||
loader: join(__dirname, 'webpack/deprecated-stylus-loader.js'),
|
||||
options: {
|
||||
stylusOptions: {
|
||||
include: includePaths,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const globalCssRules: RuleSetRule[] = [
|
||||
{
|
||||
test: /\.css$/,
|
||||
exclude: globalStylePaths,
|
||||
use: getCommonLoadersForGlobalCss(mergedOptions, includePaths),
|
||||
},
|
||||
{
|
||||
test: /\.scss$|\.sass$/,
|
||||
exclude: globalStylePaths,
|
||||
use: [
|
||||
...getCommonLoadersForGlobalCss(mergedOptions, includePaths),
|
||||
{
|
||||
loader: require.resolve('sass-loader'),
|
||||
options: {
|
||||
implementation: require('sass'),
|
||||
sourceMap: !!mergedOptions.sourceMap,
|
||||
sassOptions: {
|
||||
fiber: false,
|
||||
// bootstrap-sass requires a minimum precision of 8
|
||||
precision: 8,
|
||||
includePaths,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.less$/,
|
||||
exclude: globalStylePaths,
|
||||
use: [
|
||||
...getCommonLoadersForGlobalCss(mergedOptions, includePaths),
|
||||
{
|
||||
loader: require.resolve('less-loader'),
|
||||
options: {
|
||||
sourceMap: !!mergedOptions.sourceMap,
|
||||
lessOptions: {
|
||||
javascriptEnabled: true,
|
||||
...lessPathOptions,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.styl$/,
|
||||
exclude: globalStylePaths,
|
||||
use: [
|
||||
...getCommonLoadersForGlobalCss(mergedOptions, includePaths),
|
||||
{
|
||||
loader: join(__dirname, 'webpack/deprecated-stylus-loader.js'),
|
||||
options: {
|
||||
sourceMap: !!mergedOptions.sourceMap,
|
||||
stylusOptions: {
|
||||
include: includePaths,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const globalStyleRules: RuleSetRule[] = [
|
||||
{
|
||||
test: /\.css$/,
|
||||
include: globalStylePaths,
|
||||
use: getCommonLoadersForGlobalStyle(mergedOptions, includePaths),
|
||||
},
|
||||
{
|
||||
test: /\.scss$|\.sass$/,
|
||||
include: globalStylePaths,
|
||||
use: [
|
||||
...getCommonLoadersForGlobalStyle(mergedOptions, includePaths),
|
||||
{
|
||||
loader: require.resolve('sass-loader'),
|
||||
options: {
|
||||
implementation: require('sass'),
|
||||
sourceMap: !!mergedOptions.sourceMap,
|
||||
sassOptions: {
|
||||
fiber: false,
|
||||
// bootstrap-sass requires a minimum precision of 8
|
||||
precision: 8,
|
||||
includePaths,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.less$/,
|
||||
include: globalStylePaths,
|
||||
use: [
|
||||
...getCommonLoadersForGlobalStyle(mergedOptions, includePaths),
|
||||
{
|
||||
loader: require.resolve('less-loader'),
|
||||
options: {
|
||||
sourceMap: !!mergedOptions.sourceMap,
|
||||
lessOptions: {
|
||||
javascriptEnabled: true,
|
||||
...lessPathOptions,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.styl$/,
|
||||
include: globalStylePaths,
|
||||
use: [
|
||||
...getCommonLoadersForGlobalStyle(mergedOptions, includePaths),
|
||||
{
|
||||
loader: require.resolve('stylus-loader'),
|
||||
options: {
|
||||
sourceMap: !!mergedOptions.sourceMap,
|
||||
stylusOptions: {
|
||||
include: includePaths,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const rules: RuleSetRule[] = [
|
||||
{
|
||||
test: /\.css$|\.scss$|\.sass$|\.less$|\.styl$/,
|
||||
oneOf: [...cssModuleRules, ...globalCssRules, ...globalStyleRules],
|
||||
},
|
||||
];
|
||||
|
||||
plugins.push(
|
||||
// extract global css from js files into own css file
|
||||
new MiniCssExtractPlugin({
|
||||
filename: `[name]${hashFormat.extract}.css`,
|
||||
})
|
||||
);
|
||||
|
||||
config.output = {
|
||||
...config.output,
|
||||
crossOriginLoading: mergedOptions.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.');
|
||||
|
||||
config.entry = { ...config.entry, ...entry };
|
||||
|
||||
config.optimization = {
|
||||
...config.optimization,
|
||||
minimizer: [...config.optimization.minimizer, ...minimizer],
|
||||
emitOnErrors: false,
|
||||
moduleIds: 'deterministic' as const,
|
||||
runtimeChunk: mergedOptions.runtimeChunk ? ('single' as const) : false,
|
||||
splitChunks: {
|
||||
maxAsyncRequests: Infinity,
|
||||
cacheGroups: {
|
||||
default: !!mergedOptions.commonChunk && {
|
||||
chunks: 'async' as const,
|
||||
minChunks: 2,
|
||||
priority: 10,
|
||||
},
|
||||
common: !!mergedOptions.commonChunk && {
|
||||
name: 'common',
|
||||
chunks: 'async' as const,
|
||||
minChunks: 2,
|
||||
enforce: true,
|
||||
priority: 5,
|
||||
},
|
||||
vendors: false as const,
|
||||
vendor: !!mergedOptions.vendorChunk && {
|
||||
name: 'vendor',
|
||||
chunks: (chunk) => chunk.name === 'main',
|
||||
enforce: true,
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
config.plugins.push(...plugins);
|
||||
|
||||
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
|
||||
},
|
||||
},
|
||||
generator: {
|
||||
filename: `[name]${hashFormat.file}[ext]`,
|
||||
},
|
||||
},
|
||||
// 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
|
||||
},
|
||||
},
|
||||
generator: {
|
||||
filename: `[name]${hashFormat.file}[ext]`,
|
||||
},
|
||||
},
|
||||
// Fonts: Emit separate file and export the URL.
|
||||
{
|
||||
test: /\.(eot|otf|ttf|woff|woff2)$/,
|
||||
type: 'asset/resource',
|
||||
generator: {
|
||||
filename: `[name]${hashFormat.file}[ext]`,
|
||||
},
|
||||
},
|
||||
...rules,
|
||||
],
|
||||
};
|
||||
processed.add(config);
|
||||
return config;
|
||||
};
|
||||
}
|
||||
|
||||
function createScriptsPlugin(options: MergedOptions): WebpackPluginInstance[] {
|
||||
// process global scripts
|
||||
const globalScriptsByBundleName = normalizeExtraEntryPoints(
|
||||
options.scripts || [],
|
||||
'scripts'
|
||||
).reduce(
|
||||
(
|
||||
prev: { inject: boolean; bundleName: string; paths: string[] }[],
|
||||
curr
|
||||
) => {
|
||||
const bundleName = curr.bundleName;
|
||||
const resolvedPath = path.resolve(options.root, curr.input);
|
||||
const existingEntry = prev.find((el) => el.bundleName === bundleName);
|
||||
if (existingEntry) {
|
||||
existingEntry.paths.push(resolvedPath);
|
||||
} else {
|
||||
prev.push({
|
||||
inject: curr.inject,
|
||||
bundleName,
|
||||
paths: [resolvedPath],
|
||||
});
|
||||
}
|
||||
|
||||
return prev;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const hashFormat = getOutputHashFormat(options.outputHashing as string);
|
||||
const plugins = [];
|
||||
// Add a new asset for each entry.
|
||||
globalScriptsByBundleName.forEach((script) => {
|
||||
const hash = script.inject ? hashFormat.script : '';
|
||||
const bundleName = script.bundleName;
|
||||
|
||||
plugins.push(
|
||||
new ScriptsWebpackPlugin({
|
||||
name: bundleName,
|
||||
sourceMap: !!options.sourceMap,
|
||||
filename: `${basename(bundleName)}${hash}.js`,
|
||||
scripts: script.paths,
|
||||
basePath: options.sourceRoot,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
return plugins;
|
||||
}
|
||||
|
||||
function getCommonLoadersForCssModules(
|
||||
options: MergedOptions,
|
||||
includePaths: string[]
|
||||
) {
|
||||
// load component css as raw strings
|
||||
return [
|
||||
{
|
||||
loader: options.extractCss
|
||||
? MiniCssExtractPlugin.loader
|
||||
: require.resolve('style-loader'),
|
||||
},
|
||||
{
|
||||
loader: require.resolve('css-loader'),
|
||||
options: {
|
||||
modules: {
|
||||
mode: 'local',
|
||||
getLocalIdent: getCSSModuleLocalIdent,
|
||||
},
|
||||
importLoaders: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: require.resolve('postcss-loader'),
|
||||
options: {
|
||||
implementation: require('postcss'),
|
||||
postcssOptions: postcssOptionsCreator(options, {
|
||||
includePaths,
|
||||
forCssModules: true,
|
||||
}),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function getCommonLoadersForGlobalCss(
|
||||
options: MergedOptions,
|
||||
includePaths: string[]
|
||||
) {
|
||||
return [
|
||||
{
|
||||
loader: options.extractCss
|
||||
? MiniCssExtractPlugin.loader
|
||||
: require.resolve('style-loader'),
|
||||
},
|
||||
{ loader: require.resolve('css-loader'), options: { url: false } },
|
||||
{
|
||||
loader: require.resolve('postcss-loader'),
|
||||
options: {
|
||||
implementation: require('postcss'),
|
||||
postcssOptions: postcssOptionsCreator(options, { includePaths }),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function getCommonLoadersForGlobalStyle(
|
||||
options: MergedOptions,
|
||||
includePaths: string[]
|
||||
) {
|
||||
return [
|
||||
{
|
||||
loader: MiniCssExtractPlugin.loader,
|
||||
options: { esModule: true },
|
||||
},
|
||||
{ loader: require.resolve('css-loader'), options: { url: false } },
|
||||
{
|
||||
loader: require.resolve('postcss-loader'),
|
||||
options: {
|
||||
implementation: require('postcss'),
|
||||
postcssOptions: postcssOptionsCreator(options, { includePaths }),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function postcssOptionsCreator(
|
||||
options: MergedOptions,
|
||||
{
|
||||
includePaths,
|
||||
forCssModules = false,
|
||||
}: { includePaths: string[]; forCssModules?: boolean }
|
||||
) {
|
||||
const hashFormat = getOutputHashFormat(options.outputHashing as string);
|
||||
// PostCSS options depend on the webpack loader, but we need to set the `config` path as a string due to this check:
|
||||
// https://github.com/webpack-contrib/postcss-loader/blob/0d342b1/src/utils.js#L36
|
||||
|
||||
const postcssOptions: PostcssOptions = (loader) => ({
|
||||
map: options.sourceMap &&
|
||||
options.sourceMap !== 'hidden' && {
|
||||
inline: true,
|
||||
annotation: false,
|
||||
},
|
||||
plugins: [
|
||||
postcssImports({
|
||||
addModulesDirectories: includePaths,
|
||||
resolve: (url: string) => (url.startsWith('~') ? url.slice(1) : url),
|
||||
}),
|
||||
...(forCssModules
|
||||
? []
|
||||
: [
|
||||
PostcssCliResources({
|
||||
baseHref: options.baseHref,
|
||||
deployUrl: options.deployUrl,
|
||||
loader,
|
||||
filename: `[name]${hashFormat.file}.[ext]`,
|
||||
}),
|
||||
autoprefixer(),
|
||||
]),
|
||||
],
|
||||
});
|
||||
|
||||
// If a path to postcssConfig is passed in, set it for app and all libs, otherwise
|
||||
// use automatic detection.
|
||||
if (typeof options.postcssConfig === 'string') {
|
||||
postcssOptions.config = path.join(options.root, options.postcssConfig);
|
||||
}
|
||||
|
||||
return postcssOptions;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user