nx/packages/webpack/src/utils/with-nx.ts

427 lines
13 KiB
TypeScript

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'];
const processed = new Set();
export interface WithNxOptions {
skipTypeChecking?: boolean;
}
/**
* @param {WithNxOptions} pluginOptions
* @returns {NxWebpackPlugin}
*/
export function withNx(pluginOptions?: WithNxOptions): NxWebpackPlugin {
return function configure(
config: Configuration,
{
options,
context,
}: {
options: NormalizedWebpackExecutorOptions;
context: ExecutorContext;
}
): 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),
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),
},
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: {
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,
},
};
processed.add(updated);
return updated;
};
}
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'] =
babelConfig.options?.['babelConfig'] ??
path.join(options.root, options.projectRoot, '.babelrc');
}
return babelConfig;
default:
return null;
}
}