427 lines
13 KiB
TypeScript
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;
|
|
}
|
|
}
|