feat(webpack): remove support for legacy browsers (#14190) (#14257)

This commit is contained in:
Jack Hsu 2023-01-11 03:13:35 -05:00 committed by GitHub
parent 0bc93ee83d
commit 6feb56e014
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1068 additions and 1568 deletions

View File

@ -311,7 +311,7 @@ describe('CLI - Environment Variables', () => {
updateFile(main2, `${newCode2}\n${content2}`);
runCLI(`run-many --target build --all --no-optimization`, {
runCLI(`run-many --target build --all --outputHashing=none`, {
env: {
...process.env,
NODE_ENV: 'test',

View File

@ -199,19 +199,13 @@ function buildTargetWebpack(
const options = normalizeOptions(
withSchemaDefaults(parsed, context),
workspaceRoot,
buildableProjectConfig.root!,
buildableProjectConfig.sourceRoot!
);
if (options.webpackConfig) {
let customWebpack: any;
const isScriptOptimizeOn =
typeof options.optimization === 'boolean'
? options.optimization
: options.optimization && options.optimization.scripts
? options.optimization.scripts
: false;
customWebpack = resolveCustomWebpackConfig(
options.webpackConfig,
options.tsConfig
@ -219,16 +213,13 @@ function buildTargetWebpack(
return async () => {
customWebpack = await customWebpack;
const defaultWebpack = getWebpackConfig(
context,
options,
isScriptOptimizeOn,
{
root: ctProjectConfig.root,
sourceRoot: ctProjectConfig.sourceRoot,
configuration: parsed.configuration,
}
);
// TODO(jack): Once webpackConfig is always set in @nrwl/webpack:webpack, we no longer need this default.
const defaultWebpack = getWebpackConfig(context, {
...options,
root: workspaceRoot,
projectRoot: ctProjectConfig.root,
sourceRoot: ctProjectConfig.sourceRoot,
});
if (customWebpack) {
return await customWebpack(defaultWebpack, {

View File

@ -1,6 +1,6 @@
import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin';
import { Configuration } from 'webpack';
import { getCSSModuleLocalIdent } from '@nrwl/webpack/src/executors/webpack/lib/get-webpack-config';
import { getCSSModuleLocalIdent } from '@nrwl/webpack';
export function buildBaseWebpackConfig({
tsConfigPath = 'tsconfig.cy.json',

View File

@ -1,33 +0,0 @@
import { webpack } from './index';
import { join } from 'path';
jest.mock('@nrwl/webpack/src/executors/webpack/lib/get-webpack-config', () => {
return {
getStylesPartial: () => ({}),
};
});
describe('Storybook webpack config', () => {
it('should skip type checking', async () => {
const config = await webpack(
{
resolve: {
plugins: [],
},
plugins: [],
module: {
rules: [],
},
},
{
configDir: join(__dirname, '../..'),
}
);
expect(
config.plugins.find(
(p) => p.constructor.name === 'ForkTsCheckerWebpackPlugin'
)
).toBeFalsy();
});
});

View File

@ -4,17 +4,19 @@ import {
readJsonFile,
workspaceRoot,
} from '@nrwl/devkit';
import { getBaseWebpackPartial } from '@nrwl/webpack/src/utils/config';
import {
composePlugins,
getBaseWebpackPartial,
} from '@nrwl/webpack/src/utils/config';
import { NormalizedWebpackExecutorOptions } from '@nrwl/webpack/src/executors/webpack/schema';
import { getStylesPartial } from '@nrwl/webpack/src/executors/webpack/lib/get-webpack-config';
import { checkAndCleanWithSemver } from '@nrwl/workspace/src/utilities/version-utils';
import { join } from 'path';
import { gte } from 'semver';
import { Configuration, DefinePlugin, WebpackPluginInstance } from 'webpack';
import * as mergeWebpack from 'webpack-merge';
import { mergePlugins } from './merge-plugins';
const reactWebpackConfig = require('../webpack');
import { withReact } from '../webpack';
import { withNx, withWeb } from '@nrwl/webpack';
// This is shamelessly taken from CRA and modified for NX use
// https://github.com/facebook/create-react-app/blob/4784997f0682e75eb32a897b4ffe34d735912e6c/packages/react-scripts/config/env.js#L71
@ -99,6 +101,8 @@ export const webpack = async (
const builderOptions: NormalizedWebpackExecutorOptions = {
...options,
root: options.configDir,
// These are blank because root is the absolute path to .storybook folder
projectRoot: '',
sourceRoot: '',
fileReplacements: [],
sourceMap: {
@ -118,21 +122,13 @@ export const webpack = async (
const extractCss = storybookWebpackConfig.mode === 'production';
// ESM build for modern browsers.
const baseWebpackConfig = mergeWebpack.merge([
getBaseWebpackPartial(builderOptions, {
isScriptOptimizeOn,
skipTypeCheck: true,
}),
getStylesPartial(
options.workspaceRoot,
options.configDir,
builderOptions,
extractCss
),
]);
// run it through the React customizations
const finalConfig = reactWebpackConfig(baseWebpackConfig);
let baseWebpackConfig: Configuration = {};
const configure = composePlugins(
withNx({ skipTypeChecking: true }),
withWeb(),
withReact()
);
const finalConfig = configure(baseWebpackConfig, { options: builderOptions });
// Check whether the project .babelrc uses @emotion/babel-plugin. There's currently
// a Storybook issue (https://github.com/storybookjs/storybook/issues/13277) which apparently
@ -197,7 +193,8 @@ export const webpack = async (
plugins: mergePlugins(
...((storybookWebpackConfig.resolve.plugins ??
[]) as unknown as WebpackPluginInstance[]),
...(finalConfig.resolve.plugins ?? [])
...((finalConfig.resolve
.plugins as unknown as WebpackPluginInstance[]) ?? [])
),
},
plugins: mergePlugins(

View File

@ -1,58 +1,77 @@
import type { Configuration } from 'webpack';
import ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
import { NormalizedWebpackExecutorOptions } from '@nrwl/webpack';
import { ExecutorContext } from 'nx/src/config/misc-interfaces';
// Add React-specific configuration
export function getWebpackConfig(config: Configuration) {
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]',
},
},
],
});
if (config.mode === 'development' && config['devServer']?.hot) {
// add `react-refresh/babel` to babel loader plugin
const babelLoader = config.module.rules.find(
(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,
},
],
];
export function withReact() {
return function configure(
config: Configuration,
_ctx?: {
options: NormalizedWebpackExecutorOptions;
context: ExecutorContext;
}
// add https://github.com/pmmmwh/react-refresh-webpack-plugin to webpack plugin
config.plugins.push(new ReactRefreshPlugin());
}
): Configuration {
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,
if (config.mode === 'development' && config['devServer']?.hot) {
// add `react-refresh/babel` to babel loader plugin
const babelLoader = config.module.rules.find(
(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,
},
],
];
}
// add https://github.com/pmmmwh/react-refresh-webpack-plugin to webpack plugin
config.plugins.push(new ReactRefreshPlugin());
}
// enable webpack node api
config.node = {
__dirname: true,
__filename: true,
};
return config;
};
return config;
}
module.exports = getWebpackConfig;
// Support existing default exports as well as new named export.
const legacyExport: any = withReact();
legacyExport.withReact = withReact;
/** @deprecated use `import { withReact } from '@nrwl/react'` */
// This is here for backward compatibility if anyone imports {getWebpackConfig} directly.
// TODO(jack): Remove in Nx 16
legacyExport.getWebpackConfig = withReact();
module.exports = legacyExport;

View File

@ -1,3 +1,4 @@
export * from './src/utils/create-copy-plugin';
export * from './src/utils/config';
export * from './src/generators/init/init';
export * from './src/generators/webpack-project/webpack-project';
@ -11,3 +12,6 @@ export type {
FileReplacement,
} from './src/executors/webpack/schema';
export * from './src/executors/webpack/webpack.impl';
export * from './src/utils/get-css-module-local-ident';
export * from './src/utils/with-nx';
export * from './src/utils/with-web';

View File

@ -54,7 +54,6 @@
"postcss": "^8.4.14",
"postcss-import": "~14.1.0",
"postcss-loader": "^6.1.1",
"raw-loader": "^4.0.2",
"rxjs": "^6.5.4",
"sass": "^1.42.1",
"sass-loader": "^12.2.0",

View File

@ -30,6 +30,7 @@ export async function* devServerExecutor(
const buildOptions = normalizeOptions(
getBuildOptions(serveOptions, context),
context.root,
projectRoot,
sourceRoot
);

View File

@ -20,13 +20,7 @@ export function getDevServerConfig(
const workspaceRoot = context.root;
const { root: projectRoot, sourceRoot } =
context.projectsConfigurations.projects[context.projectName];
const webpackConfig = getWebpackConfig(
context,
buildOptions,
typeof buildOptions.optimization === 'boolean'
? buildOptions.optimization
: buildOptions.optimization?.scripts
);
const webpackConfig = getWebpackConfig(context, buildOptions);
(webpackConfig as any).devServer = getDevServerPartial(
workspaceRoot,

View File

@ -1,28 +1,10 @@
import * as path from 'path';
import { posix, resolve } from 'path';
import { readTsConfig } from '@nrwl/workspace/src/utilities/typescript';
import { getHashDigest, interpolateName } from 'loader-utils';
import type { Configuration } from 'webpack';
import { ExecutorContext } from '@nrwl/devkit';
import { NormalizedWebpackExecutorOptions } from '../schema';
// TODO(jack): These should be inlined in a single function so it is easier to understand
import { getBaseWebpackPartial } from '../../../utils/config';
import { getBrowserConfig } from '../../../utils/webpack/partials/browser';
import { getCommonConfig } from '../../../utils/webpack/partials/common';
import { getStylesConfig } from '../../../utils/webpack/partials/styles';
import { ExecutorContext } from '@nrwl/devkit';
import MiniCssExtractPlugin = require('mini-css-extract-plugin');
import webpackMerge = require('webpack-merge');
import postcssImports = require('postcss-import');
// 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
interface PostcssOptions {
(loader: any): any;
config?: string;
}
import { withNx } from '../../../utils/with-nx';
import { withWeb } from '../../../utils/with-web';
import { composePlugins } from '@nrwl/webpack';
interface GetWebpackConfigOverrides {
root: string;
@ -30,288 +12,14 @@ interface GetWebpackConfigOverrides {
configuration?: string;
}
/** @deprecated Use withNx, withWeb, or withReact */
// TODO(jack): Remove in Nx 16
export function getWebpackConfig(
context: ExecutorContext,
options: NormalizedWebpackExecutorOptions,
isScriptOptimizeOn?: boolean,
overrides?: GetWebpackConfigOverrides
options: NormalizedWebpackExecutorOptions
): Configuration {
const tsConfig = readTsConfig(options.tsConfig);
const workspaceRoot = context.root;
let sourceRoot: string;
let projectRoot: string;
if (overrides) {
projectRoot = overrides.root;
sourceRoot = overrides.sourceRoot;
} else {
const project =
context.projectsConfigurations.projects[context.projectName];
projectRoot = project.root;
sourceRoot = project.sourceRoot;
}
const wco: any = {
root: workspaceRoot,
projectRoot: resolve(workspaceRoot, projectRoot),
sourceRoot: resolve(workspaceRoot, sourceRoot),
buildOptions: convertBuildOptions(options),
console,
tsConfig,
tsConfigPath: options.tsConfig,
};
// TODO(jack): Replace merge behavior with an inlined config so it is easier to understand.
return webpackMerge.merge([
_getBaseWebpackPartial(
context,
options,
isScriptOptimizeOn,
tsConfig.options.emitDecoratorMetadata,
overrides
),
options.target === 'web'
? getPolyfillsPartial(options.polyfills, isScriptOptimizeOn)
: {},
options.target === 'web'
? getStylesPartial(
wco.root,
wco.projectRoot,
wco.buildOptions,
options.extractCss,
options.postcssConfig
)
: {},
getCommonPartial(wco),
options.target === 'web' ? getBrowserConfig(wco) : {},
]);
}
function _getBaseWebpackPartial(
context: ExecutorContext,
options: NormalizedWebpackExecutorOptions,
isScriptOptimizeOn: boolean,
emitDecoratorMetadata: boolean,
overrides?: GetWebpackConfigOverrides
) {
let partial = getBaseWebpackPartial(
options,
{
isScriptOptimizeOn,
emitDecoratorMetadata,
configuration: overrides?.configuration ?? context.configurationName,
},
context
);
delete partial.resolve.mainFields;
return partial;
}
function getCommonPartial(wco: any): Configuration {
const commonConfig: Configuration = <Configuration>getCommonConfig(wco);
delete commonConfig.entry;
delete commonConfig.resolve.modules;
delete commonConfig.resolve.extensions;
delete commonConfig.output.path;
delete commonConfig.module;
return commonConfig;
}
export function getStylesPartial(
workspaceRoot: string,
projectRoot: string,
options: any,
extractCss: boolean,
postcssConfig?: string
): Configuration {
const includePaths: string[] = [];
if (options?.stylePreprocessorOptions?.includePaths?.length > 0) {
options.stylePreprocessorOptions.includePaths.forEach(
(includePath: string) =>
includePaths.push(path.resolve(workspaceRoot, includePath))
);
}
const partial = getStylesConfig(workspaceRoot, options, includePaths);
const rules = partial.module.rules.map((rule) => {
if (!Array.isArray(rule.use)) {
return rule;
}
rule.use = rule.use.map((loaderConfig) => {
if (
typeof loaderConfig === 'object' &&
loaderConfig.loader === require.resolve('raw-loader')
) {
return {
loader: require.resolve('style-loader'),
};
}
return loaderConfig;
});
return rule;
});
const loaderModulesOptions = {
modules: {
mode: 'local',
getLocalIdent: getCSSModuleLocalIdent,
},
importLoaders: 1,
};
const postcssOptions: PostcssOptions = () => ({
plugins: [
postcssImports({
addModulesDirectories: includePaths,
resolve: (url: string) => (url.startsWith('~') ? url.slice(1) : url),
}),
],
});
// If a path to postcssConfig is passed in, set it for app and all libs, otherwise
// use automatic detection.
if (typeof postcssConfig === 'string') {
postcssOptions.config = path.join(workspaceRoot, postcssConfig);
}
const commonLoaders = [
{
loader: extractCss
? MiniCssExtractPlugin.loader
: require.resolve('style-loader'),
},
{
loader: require.resolve('css-loader'),
options: loaderModulesOptions,
},
{
loader: require.resolve('postcss-loader'),
options: {
implementation: require('postcss'),
postcssOptions: postcssOptions,
},
},
];
partial.module.rules = [
{
test: /\.css$|\.scss$|\.sass$|\.less$|\.styl$/,
oneOf: [
{
test: /\.module\.css$/,
use: commonLoaders,
},
{
test: /\.module\.(scss|sass)$/,
use: [
...commonLoaders,
{
loader: require.resolve('sass-loader'),
options: {
implementation: require('sass'),
sassOptions: {
fiber: false,
precision: 8,
includePaths,
},
},
},
],
},
{
test: /\.module\.less$/,
use: [
...commonLoaders,
{
loader: require.resolve('less-loader'),
options: {
lessOptions: {
paths: includePaths,
},
},
},
],
},
{
test: /\.module\.styl$/,
use: [
...commonLoaders,
{
loader: require.resolve('stylus-loader'),
options: {
stylusOptions: {
include: includePaths,
},
},
},
],
},
...rules,
],
},
];
return partial;
}
export function getPolyfillsPartial(
polyfills: string,
isScriptOptimizeOn: boolean
): Configuration {
const config = {
entry: {} as { [key: string]: string[] },
};
if (polyfills && isScriptOptimizeOn) {
// Safari 10.1 supports <script type="module"> but not <script nomodule>.
// Need to patch it up so the browser doesn't load both sets.
config.entry.polyfills = [
require.resolve('@nrwl/webpack/src/utils/webpack/safari-nomodule.js'),
...(polyfills ? [polyfills] : []),
];
} else {
if (polyfills) {
config.entry.polyfills = [polyfills];
}
}
return config;
}
export function getCSSModuleLocalIdent(
context,
localIdentName,
localName,
options
) {
// Use the filename or folder name, based on some uses the index.js / index.module.(css|scss|sass) project style
const fileNameOrFolder = context.resourcePath.match(
/index\.module\.(css|scss|sass|styl)$/
)
? '[folder]'
: '[name]';
// Create a hash based on a the file location and class name. Will be unique across a project, and close to globally unique.
const hash = getHashDigest(
posix.relative(context.rootContext, context.resourcePath) + localName,
'md5',
'base64',
5
);
// Use loaderUtils to find the file or folder name
const className = interpolateName(
context,
`${fileNameOrFolder}_${localName}__${hash}`,
options
);
// Remove the .module that appears in every classname when based on the file and replace all "." with "_".
return className.replace('.module_', '_').replace(/\./g, '_');
}
export function convertBuildOptions(
buildOptions: NormalizedWebpackExecutorOptions
): any {
const options = buildOptions as any;
return {
...options,
buildOptimizer: options.optimization,
forkTypeChecker: false,
lazyModules: [] as string[],
};
const config: Configuration = {};
const configure =
options.target === 'node' ? withNx() : composePlugins(withNx(), withWeb());
return configure(config, { options, context });
}

View File

@ -12,11 +12,13 @@ import type {
export function normalizeOptions(
options: WebpackExecutorOptions,
root: string,
projectRoot: string,
sourceRoot: string
): NormalizedWebpackExecutorOptions {
return {
...options,
root,
projectRoot,
sourceRoot,
target: options.target ?? 'web',
main: resolve(root, options.main),

View File

@ -85,5 +85,6 @@ export interface NormalizedWebpackExecutorOptions
extends WebpackExecutorOptions {
assets?: AssetGlobPattern[];
root?: string;
projectRoot?: string;
sourceRoot?: string;
}

View File

@ -49,7 +49,7 @@ async function getWebpackConfigs(
}
}
const config = getWebpackConfig(context, options, isScriptOptimizeOn);
const config = getWebpackConfig(context, options);
if (customWebpack) {
return await customWebpack(config, {
@ -81,7 +81,12 @@ export async function* webpackExecutor(
): AsyncGenerator<WebpackExecutorEvent, WebpackExecutorEvent, undefined> {
const metadata = context.projectsConfigurations.projects[context.projectName];
const sourceRoot = metadata.sourceRoot;
const options = normalizeOptions(_options, context.root, sourceRoot);
const options = normalizeOptions(
_options,
context.root,
metadata.root,
sourceRoot
);
const isScriptOptimizeOn =
typeof options.optimization === 'boolean'
? options.optimization

View File

@ -0,0 +1,10 @@
import { Compiler, sources } from 'webpack';
export class StatsJsonPlugin {
apply(compiler: Compiler) {
compiler.hooks.emit.tap('angular-cli-stats', (compilation) => {
const data = JSON.stringify(compilation.getStats().toJson('verbose'));
compilation.assets[`stats.json`] = new sources.RawSource(data);
});
}
}

View File

@ -1,427 +1,39 @@
import { join, parse } from 'path';
import * as webpack from 'webpack';
import { Configuration, WebpackPluginInstance } from 'webpack';
import { LicenseWebpackPlugin } from 'license-webpack-plugin';
import * as CopyWebpackPlugin from 'copy-webpack-plugin';
import { getOutputHashFormat } from './hash-format';
import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin';
import { Configuration } from 'webpack';
import { ExecutorContext } from '@nrwl/devkit';
import { loadTsTransformers } from '@nrwl/js';
import {
AssetGlobPattern,
NormalizedWebpackExecutorOptions,
} from '../executors/webpack/schema';
import { GeneratePackageJsonWebpackPlugin } from './generate-package-json-webpack-plugin';
import nodeExternals = require('webpack-node-externals');
import TerserPlugin = require('terser-webpack-plugin');
import ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const IGNORED_WEBPACK_WARNINGS = [
/The comment file/i,
/could not find any license/i,
];
export interface InternalBuildOptions {
isScriptOptimizeOn?: boolean;
emitDecoratorMetadata?: boolean;
configuration?: string;
skipTypeCheck?: boolean;
}
const extensions = ['.ts', '.tsx', '.mjs', '.js', '.jsx'];
import { NormalizedWebpackExecutorOptions } from '../executors/webpack/schema';
import { withNx } from './with-nx';
import { withWeb } from './with-web';
/** @deprecated use withNx and withWeb plugins directly */
export function getBaseWebpackPartial(
options: NormalizedWebpackExecutorOptions,
internalOptions: InternalBuildOptions,
context?: ExecutorContext
): Configuration {
// If the function is called directly and not through `@nrwl/webpack:webpack` then this target may not be set.
options.target ??= 'web';
const config: Configuration = {};
const configure = composePlugins(withNx(), withWeb());
return configure(config, { options, context });
}
const mainFields = ['es2015', 'module', 'main'];
const hashFormat = getOutputHashFormat(options.outputHashing);
const filename = internalOptions.isScriptOptimizeOn
? `[name]${hashFormat.script}.js`
: '[name].js';
const chunkFilename = internalOptions.isScriptOptimizeOn
? `[name]${hashFormat.chunk}.js`
: '[name].js';
const mode = internalOptions.isScriptOptimizeOn
? 'production'
: 'development';
let mainEntry = 'main';
if (options.outputFileName) {
mainEntry = parse(options.outputFileName).name;
export type NxWebpackPlugin = (
config: Configuration,
ctx?: {
options: NormalizedWebpackExecutorOptions;
context?: ExecutorContext;
}
const additionalEntryPoints =
options.additionalEntryPoints?.reduce(
(obj, current) => ({
...obj,
[current.entryName]: current.entryPath,
}),
{} as { [entryName: string]: string }
) ?? {};
) => Configuration;
const webpackConfig: Configuration = {
target: options.target,
entry: {
[mainEntry]: [options.main],
...additionalEntryPoints,
},
devtool:
options.sourceMap === 'hidden'
? 'hidden-source-map'
: options.sourceMap
? 'source-map'
: false,
mode,
output: {
path: options.outputPath,
filename,
chunkFilename,
hashFunction: 'xxhash64',
// Disabled for performance
pathinfo: false,
scriptType: 'module',
},
module: {
// Enabled for performance
unsafeCache: true,
rules: [
options.target === 'web' && {
test: /\.(bmp|png|jpe?g|gif|webp|avif)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10_000, // 10 kB
},
},
},
{
// 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, internalOptions),
].filter(Boolean),
},
resolve: {
extensions,
alias: getAliases(options),
plugins: [
new TsconfigPathsPlugin({
configFile: options.tsConfig,
extensions,
mainFields,
}) as never, // TODO: Remove never type when 'tsconfig-paths-webpack-plugin' types fixed
],
mainFields,
},
performance: {
hints: false,
},
plugins: [],
watch: options.watch,
watchOptions: {
poll: options.poll,
},
stats: getStatsConfig(options),
ignoreWarnings: [
(x) =>
IGNORED_WEBPACK_WARNINGS.some((r) =>
typeof x === 'string' ? r.test(x) : r.test(x.message)
),
],
experiments: {
cacheUnaffected: true,
},
};
if (options.target === 'node') {
webpackConfig.output.libraryTarget = 'commonjs';
webpackConfig.node = false;
// could be an object { scripts: boolean; styles: boolean }
if (internalOptions.isScriptOptimizeOn) {
webpackConfig.optimization = {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
mangle: false,
keep_classnames: true,
},
}),
],
concatenateModules: true,
};
export function composePlugins(...plugins: NxWebpackPlugin[]) {
return function combined(
config: Configuration,
ctx?: {
options: NormalizedWebpackExecutorOptions;
context?: ExecutorContext;
}
} else {
webpackConfig.plugins.push(
new webpack.DefinePlugin(getClientEnvironment(mode).stringified)
);
webpackConfig.optimization ??= {};
webpackConfig.optimization.nodeEnv = process.env.NODE_ENV ?? mode;
if (internalOptions.isScriptOptimizeOn) {
// Always check sideEffects field in package.json for tree-shaking to work.
webpackConfig.optimization.sideEffects = true;
if (options.compiler !== 'swc') {
webpackConfig.optimization = {
sideEffects: true,
minimizer: [
new TerserPlugin({
parallel: true,
terserOptions: {
ecma: 2020,
safari10: true,
output: {
ascii_only: true,
comments: false,
webkit: true,
},
},
}),
],
runtimeChunk: true,
};
}
): Configuration {
for (const plugin of plugins) {
config = plugin(config, ctx);
}
}
const extraPlugins: WebpackPluginInstance[] = [];
if (!internalOptions.skipTypeCheck) {
extraPlugins.push(
new ForkTsCheckerWebpackPlugin({
typescript: {
configFile: options.tsConfig,
memoryLimit: options.memoryLimit || 2018,
},
})
);
}
if (options.progress) {
extraPlugins.push(new webpack.ProgressPlugin());
}
// TODO LicenseWebpackPlugin needs a PR for proper typing
if (options.extractLicenses) {
extraPlugins.push(
new LicenseWebpackPlugin({
stats: {
errors: false,
},
perChunkOutput: false,
outputFilename: `3rdpartylicenses.txt`,
}) as unknown as WebpackPluginInstance
);
}
if (Array.isArray(options.assets) && options.assets.length > 0) {
extraPlugins.push(createCopyPlugin(options.assets));
}
if (
options.target === 'node' &&
options.externalDependencies === 'all' &&
context
) {
const modulesDir = `${context.root}/node_modules`;
webpackConfig.externals = [nodeExternals({ modulesDir })];
} else if (Array.isArray(options.externalDependencies)) {
webpackConfig.externals = [
function (context, callback: Function) {
if (options.externalDependencies.includes(context.request)) {
// not bundled
return callback(null, `commonjs ${context.request}`);
}
// bundled
callback();
},
];
}
if (options.generatePackageJson && context) {
extraPlugins.push(new GeneratePackageJsonWebpackPlugin(context, options));
}
webpackConfig.plugins = [...webpackConfig.plugins, ...extraPlugins];
return webpackConfig;
}
function getAliases(options: NormalizedWebpackExecutorOptions): {
[key: string]: string;
} {
return options.fileReplacements.reduce(
(aliases, replacement) => ({
...aliases,
[replacement.replace]: replacement.with,
}),
{}
);
}
function getStatsConfig(options: NormalizedWebpackExecutorOptions) {
return {
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,
return config;
};
}
export function getClientEnvironment(mode) {
// Grab NODE_ENV and NX_* environment variables and prepare them to be
// injected into the application via DefinePlugin in webpack configuration.
const NX_APP = /^NX_/i;
const raw = Object.keys(process.env)
.filter((key) => NX_APP.test(key))
.reduce(
(env, key) => {
env[key] = process.env[key];
return env;
},
{
// Useful for determining whether were running in production mode.
NODE_ENV: process.env.NODE_ENV || mode,
}
);
// Stringify all values so we can feed into webpack DefinePlugin
const stringified = {
'process.env': Object.keys(raw).reduce((env, key) => {
env[key] = JSON.stringify(raw[key]);
return env;
}, {}),
};
return { stringified };
}
export function createCopyPlugin(assets: AssetGlobPattern[]) {
return new CopyWebpackPlugin({
patterns: 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,
},
};
}),
});
}
export function createLoaderFromCompiler(
options: NormalizedWebpackExecutorOptions,
extraOptions: InternalBuildOptions
) {
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 { 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':
return {
test: /\.([jt])sx?$/,
loader: join(__dirname, 'web-babel-loader'),
exclude: /node_modules/,
options: {
rootMode: 'upward',
cwd: join(options.root, options.sourceRoot),
emitDecoratorMetadata: extraOptions.emitDecoratorMetadata,
isModern: true,
envName: extraOptions.isScriptOptimizeOn
? 'production'
: extraOptions.configuration,
babelrc: true,
cacheDirectory: true,
cacheCompression: false,
},
};
default:
return null;
}
}

View File

@ -0,0 +1,24 @@
import * as CopyWebpackPlugin from 'copy-webpack-plugin';
import { AssetGlobPattern } from '../executors/webpack/schema';
export function createCopyPlugin(assets: AssetGlobPattern[]) {
return new CopyWebpackPlugin({
patterns: 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,
},
};
}),
});
}

View File

@ -0,0 +1,30 @@
export function getClientEnvironment(mode?: string) {
// Grab NODE_ENV and NX_* environment variables and prepare them to be
// injected into the application via DefinePlugin in webpack configuration.
const NX_APP = /^NX_/i;
const raw = Object.keys(process.env)
.filter((key) => NX_APP.test(key))
.reduce(
(env, key) => {
env[key] = process.env[key];
return env;
},
// If mode is undefined or is dev or prod then webpack already defines this variable for us.
!mode || mode === 'development' || mode === 'production'
? {}
: {
NODE_ENV: process.env.NODE_ENV,
}
);
// Stringify all values so we can feed into webpack DefinePlugin
const stringified = {
'process.env': Object.keys(raw).reduce((env, key) => {
env[key] = JSON.stringify(raw[key]);
return env;
}, {}),
};
return { stringified };
}

View File

@ -0,0 +1,31 @@
import { posix } from 'path';
import { getHashDigest, interpolateName } from 'loader-utils';
export function getCSSModuleLocalIdent(
ctx,
localIdentName,
localName,
options
) {
// Use the filename or folder name, based on some uses the index.js / index.module.(css|scss|sass) project style
const fileNameOrFolder = ctx.resourcePath.match(
/index\.module\.(css|scss|sass|styl)$/
)
? '[folder]'
: '[name]';
// Create a hash based on a the file location and class name. Will be unique across a project, and close to globally unique.
const hash = getHashDigest(
posix.relative(ctx.rootContext, ctx.resourcePath) + localName,
'md5',
'base64',
5
);
// Use loaderUtils to find the file or folder name
const className = interpolateName(
ctx,
`${fileNameOrFolder}_${localName}__${hash}`,
options
);
// Remove the .module that appears in every classname when based on the file and replace all "." with "_".
return className.replace('.module_', '_').replace(/\./g, '_');
}

View File

@ -4,10 +4,9 @@ export interface ExtraEntryPointClass {
bundleName?: string;
inject?: boolean;
input: string;
lazy?: boolean;
}
export type NormalizedEntryPoint = Required<Omit<ExtraEntryPointClass, 'lazy'>>;
export type NormalizedEntryPoint = Required<ExtraEntryPointClass>;
export interface EmittedFile {
id?: string;

View File

@ -1,6 +1,3 @@
import { basename } from 'path';
import { normalizePath } from '@nrwl/devkit';
import { ExtraEntryPoint, NormalizedEntryPoint } from '../models';
export function normalizeExtraEntryPoints(
@ -16,24 +13,16 @@ export function normalizeExtraEntryPoints(
bundleName: defaultBundleName,
};
} else {
const { lazy, inject = true, ...newEntry } = entry;
const injectNormalized = entry.lazy !== undefined ? !entry.lazy : inject;
const { inject = true, ...newEntry } = entry;
let bundleName;
if (entry.bundleName) {
bundleName = entry.bundleName;
} else if (!injectNormalized) {
// Lazy entry points use the file name as bundle name.
bundleName = basename(
normalizePath(
entry.input.replace(/\.(js|css|scss|sass|less|styl)$/i, '')
)
);
} else {
bundleName = defaultBundleName;
}
normalizedEntry = { ...newEntry, inject: injectNormalized, bundleName };
normalizedEntry = { ...newEntry, bundleName };
}
return normalizedEntry;

View File

@ -13,9 +13,7 @@ export function generateEntryPoints(appConfig: {
const entryPoints = normalizeExtraEntryPoints(
extraEntryPoints,
defaultBundleName
)
.filter((entry) => entry.inject)
.map((entry) => entry.bundleName);
).map((entry) => entry.bundleName);
// remove duplicates
return [...new Set(entryPoints)];

View File

@ -1,86 +0,0 @@
import { LicenseWebpackPlugin } from 'license-webpack-plugin';
import { ids } from 'webpack';
import { SubresourceIntegrityPlugin } from 'webpack-subresource-integrity';
import { CreateWebpackConfigOptions } from '../../models';
import CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
export function getBrowserConfig(wco: CreateWebpackConfigOptions) {
const { buildOptions } = wco;
const extraPlugins = [];
const stylesOptimization =
typeof buildOptions.optimization === 'object'
? buildOptions.optimization.styles
: buildOptions.optimization;
if (buildOptions.subresourceIntegrity) {
extraPlugins.push(new SubresourceIntegrityPlugin());
}
if (buildOptions.extractLicenses) {
extraPlugins.push(
new LicenseWebpackPlugin({
stats: {
warnings: false,
errors: false,
},
perChunkOutput: false,
outputFilename: `3rdpartylicenses.txt`,
})
);
}
const extraMinimizers = [];
if (stylesOptimization) {
extraMinimizers.push(
new CssMinimizerPlugin({
test: /\.(?:css|scss|sass|less|styl)$/,
})
);
}
return {
resolve: {
mainFields: ['browser', 'module', 'main'],
},
output: {
crossOriginLoading: buildOptions.subresourceIntegrity
? ('anonymous' as const)
: (false as const),
},
// context needs to be set for babel to pick up correct babelrc
context: wco.projectRoot,
optimization: {
minimizer: [new ids.HashedModuleIdsPlugin(), ...extraMinimizers],
emitOnErrors: false,
moduleIds: 'deterministic' as const,
runtimeChunk: buildOptions.runtimeChunk ? ('single' as const) : false,
splitChunks: {
maxAsyncRequests: Infinity,
cacheGroups: {
default: !!buildOptions.commonChunk && {
chunks: 'async' as const,
minChunks: 2,
priority: 10,
},
common: !!buildOptions.commonChunk && {
name: 'common',
chunks: 'async' as const,
minChunks: 2,
enforce: true,
priority: 5,
},
vendors: false as const,
vendor: !!buildOptions.vendorChunk && {
name: 'vendor',
chunks: (chunk) => chunk.name === 'main',
enforce: true,
test: /[\\/]node_modules[\\/]/,
},
},
},
},
plugins: extraPlugins,
node: false as false,
};
}

View File

@ -1,191 +0,0 @@
import { basename, resolve } from 'path';
import type { Compiler, Configuration } from 'webpack';
import { ProgressPlugin, sources } from 'webpack';
import { normalizeExtraEntryPoints } from '../normalize-entry';
import { ScriptsWebpackPlugin } from '../plugins/scripts-webpack-plugin';
import { getOutputHashFormat } from '../../hash-format';
import { findAllNodeModules, findUp } from '../../fs';
import type { CreateWebpackConfigOptions } from '../../models';
export function getCommonConfig(
wco: CreateWebpackConfigOptions
): Configuration {
const { root, projectRoot, sourceRoot, buildOptions } = wco;
let stylesOptimization: boolean;
let scriptsOptimization: boolean;
if (typeof buildOptions.optimization === 'object') {
scriptsOptimization = buildOptions.optimization.scripts;
stylesOptimization = buildOptions.optimization.styles;
} else {
scriptsOptimization = stylesOptimization = !!buildOptions.optimization;
}
const nodeModules = findUp('node_modules', projectRoot);
if (!nodeModules) {
throw new Error('Cannot locate node_modules directory.');
}
// tslint:disable-next-line:no-any
const extraPlugins: any[] = [];
const entryPoints: { [key: string]: string[] } = {};
if (buildOptions.main) {
entryPoints['main'] = [resolve(root, buildOptions.main)];
}
if (buildOptions.polyfills) {
entryPoints['polyfills'] = [
...(entryPoints['polyfills'] || []),
resolve(root, buildOptions.polyfills),
];
}
// determine hashing format
const hashFormat = getOutputHashFormat(buildOptions.outputHashing || 'none');
// process global scripts
const globalScriptsByBundleName = normalizeExtraEntryPoints(
buildOptions.scripts || [],
'scripts'
).reduce(
(
prev: { bundleName: string; paths: string[]; inject: boolean }[],
curr
) => {
const bundleName = curr.bundleName;
const resolvedPath = resolve(root, curr.input);
const existingEntry = prev.find((el) => el.bundleName === bundleName);
if (existingEntry) {
if (existingEntry.inject && !curr.inject) {
// All entries have to be lazy for the bundle to be lazy.
throw new Error(
`The ${curr.bundleName} bundle is mixing injected and non-injected scripts.`
);
}
existingEntry.paths.push(resolvedPath);
} else {
prev.push({
bundleName,
paths: [resolvedPath],
inject: curr.inject,
});
}
return prev;
},
[]
);
if (globalScriptsByBundleName.length > 0) {
// Add a new asset for each entry.
globalScriptsByBundleName.forEach((script) => {
// Lazy scripts don't get a hash, otherwise they can't be loaded by name.
const hash = script.inject ? hashFormat.script : '';
const bundleName = script.bundleName;
extraPlugins.push(
new ScriptsWebpackPlugin({
name: bundleName,
sourceMap: !!buildOptions.sourceMap,
filename: `${basename(bundleName)}${hash}.js`,
scripts: script.paths,
basePath: sourceRoot,
})
);
});
}
if (buildOptions.progress) {
extraPlugins.push(new ProgressPlugin({ profile: buildOptions.verbose }));
}
// TODO Needs source exported from webpack
if (buildOptions.statsJson) {
extraPlugins.push(
new (class {
apply(compiler: Compiler) {
compiler.hooks.emit.tap('angular-cli-stats', (compilation) => {
const data = JSON.stringify(
compilation.getStats().toJson('verbose')
);
compilation.assets[`stats.json`] = new sources.RawSource(data);
});
}
})()
);
}
let sourceMapUseRule;
if (!!buildOptions.sourceMap) {
sourceMapUseRule = {
use: [
{
loader: require.resolve('source-map-loader'),
},
],
};
}
// Allow loaders to be in a node_modules nested inside the devkit/build-angular package.
// This is important in case loaders do not get hoisted.
// If this file moves to another location, alter potentialNodeModules as well.
const loaderNodeModules = findAllNodeModules(__dirname, projectRoot);
loaderNodeModules.unshift('node_modules');
return {
profile: buildOptions.statsJson,
resolve: {
extensions: ['.ts', '.tsx', '.mjs', '.js'],
symlinks: true,
modules: [wco.tsConfig.options.baseUrl || projectRoot, 'node_modules'],
},
resolveLoader: {
modules: loaderNodeModules,
},
entry: entryPoints,
output: {
path: resolve(root, buildOptions.outputPath as string),
publicPath: buildOptions.deployUrl,
},
watch: buildOptions.watch,
performance: {
hints: false,
},
module: {
// Show an error for missing exports instead of a warning.
strictExportPresence: true,
rules: [
{
test: /\.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)$/,
loader: require.resolve('file-loader'),
options: {
name: `[name]${hashFormat.file}.[ext]`,
},
},
{
test: /[\/\\]hot[\/\\]emitter\.js$/,
parser: { node: { events: true } },
},
{
test: /[\/\\]webpack-dev-server[\/\\]client[\/\\]utils[\/\\]createSocketUrl\.js$/,
parser: { node: { querystring: true } },
},
{
test: /\.js$/,
// Factory files are processed by BO in the rules added in typescript.ts.
exclude: /(ngfactory|ngstyle)\.js$/,
},
{
test: /\.js$/,
exclude: /(ngfactory|ngstyle)\.js$/,
enforce: 'pre',
...sourceMapUseRule,
},
],
},
plugins: extraPlugins,
};
}

View File

@ -1,219 +0,0 @@
import * as path from 'path';
import { RuleSetRule } from 'webpack';
import { RemoveEmptyScriptsPlugin } from '../plugins/remove-empty-scripts-plugin';
import { getOutputHashFormat } from '../../hash-format';
import { PostcssCliResources } from '../plugins/postcss-cli-resources';
import { RemoveHashPlugin } from '../plugins/remove-hash-plugin';
import { NormalizedWebpackExecutorOptions } from '../../../executors/webpack/schema';
import { normalizeExtraEntryPoints } from '../normalize-entry';
import MiniCssExtractPlugin = require('mini-css-extract-plugin');
const autoprefixer = require('autoprefixer');
const postcssImports = require('postcss-import');
export function getStylesConfig(
root: string,
buildOptions: NormalizedWebpackExecutorOptions,
includePaths: string[]
) {
const RawCssLoader = require.resolve(
path.join(__dirname, '../plugins/raw-css-loader.js')
);
const entryPoints: { [key: string]: string[] } = {};
const globalStylePaths: string[] = [];
const extraPlugins = [];
const cssSourceMap = !!buildOptions.sourceMap;
// Determine hashing format.
const hashFormat = getOutputHashFormat(buildOptions.outputHashing as string);
const postcssOptionsCreator = (sourceMap: boolean) => {
return (loader) => ({
map: sourceMap && {
inline: true,
annotation: false,
},
plugins: [
postcssImports({
addModulesDirectories: includePaths,
resolve: (url: string) => (url.startsWith('~') ? url.slice(1) : url),
}),
PostcssCliResources({
baseHref: buildOptions.baseHref,
deployUrl: buildOptions.deployUrl,
loader,
filename: `[name]${hashFormat.file}.[ext]`,
}),
autoprefixer(),
],
});
};
let lessPathOptions: { paths?: string[] } = {};
if (includePaths.length > 0) {
lessPathOptions = {
paths: includePaths,
};
}
// Process global styles.
if (buildOptions.styles.length > 0) {
const chunkNames: string[] = [];
normalizeExtraEntryPoints(buildOptions.styles, 'styles').forEach(
(style) => {
const resolvedPath = path.resolve(root, style.input);
// Add style entry points.
if (entryPoints[style.bundleName]) {
entryPoints[style.bundleName].push(resolvedPath);
} else {
entryPoints[style.bundleName] = [resolvedPath];
}
// Add non injected styles to the list.
if (!style.inject) {
chunkNames.push(style.bundleName);
}
// Add global css paths.
globalStylePaths.push(resolvedPath);
}
);
if (chunkNames.length > 0) {
// Add plugin to remove hashes from lazy styles.
extraPlugins.push(new RemoveHashPlugin({ chunkNames, hashFormat }));
}
}
// set base rules to derive final rules from
const baseRules: RuleSetRule[] = [
{ test: /\.css$/, use: [] },
{
test: /\.scss$|\.sass$/,
use: [
{
loader: require.resolve('sass-loader'),
options: {
implementation: require('sass'),
sourceMap: cssSourceMap,
sassOptions: {
fiber: false,
// bootstrap-sass requires a minimum precision of 8
precision: 8,
includePaths,
},
},
},
],
},
{
test: /\.less$/,
use: [
{
loader: require.resolve('less-loader'),
options: {
sourceMap: cssSourceMap,
lessOptions: {
javascriptEnabled: true,
...lessPathOptions,
},
},
},
],
},
{
test: /\.styl$/,
use: [
{
loader: require.resolve('stylus-loader'),
options: {
sourceMap: cssSourceMap,
stylusOptions: {
include: includePaths,
},
},
},
],
},
];
// load component css as raw strings
const componentsSourceMap = !!(cssSourceMap &&
// Never use component css sourcemap when style optimizations are on.
// It will just increase bundle size without offering good debug experience.
typeof buildOptions.optimization === 'undefined'
? true
: typeof buildOptions.optimization === 'boolean'
? !buildOptions.optimization
: buildOptions.optimization?.styles &&
// Inline all sourcemap types except hidden ones, which are the same as no sourcemaps
// for component css.
buildOptions.sourceMap !== 'hidden');
const rules: RuleSetRule[] = baseRules.map(({ test, use }) => ({
exclude: globalStylePaths,
test,
use: [
{ loader: require.resolve('raw-loader') },
{ loader: RawCssLoader },
{
loader: require.resolve('postcss-loader'),
options: {
implementation: require('postcss'),
postcssOptions: postcssOptionsCreator(componentsSourceMap),
},
},
...(Array.isArray(use) ? use : []),
],
}));
// load global css as css files
if (globalStylePaths.length > 0) {
const globalSourceMap =
!!cssSourceMap && buildOptions.sourceMap !== 'hidden';
rules.push(
...baseRules.map(({ test, use }) => {
return {
include: globalStylePaths,
test,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: { esModule: true },
},
{ loader: RawCssLoader },
{
loader: require.resolve('postcss-loader'),
options: {
implementation: require('postcss'),
postcssOptions: postcssOptionsCreator(globalSourceMap),
},
},
...(use as any[]),
],
};
})
);
}
extraPlugins.push(
// extract global css from js files into own css file
new MiniCssExtractPlugin({
filename: `[name]${hashFormat.extract}.css`,
}),
// suppress empty .js files in css only entry points
new RemoveEmptyScriptsPlugin()
);
return {
entry: entryPoints,
module: { rules },
plugins: extraPlugins,
};
}

View File

@ -1,5 +0,0 @@
export default function RawCssLoader(content: string, map: object) {
const stringifiedContent = JSON.stringify(content);
const stringifiedMap = map ? JSON.stringify(map) : `''`;
return `module.exports = [[module.id, ${stringifiedContent}, '', ${stringifiedMap}]]`;
}

View File

@ -1,179 +0,0 @@
// Removes empty script bundles (e.g. styles.js)
// FROM: https://github.com/webdiscus/webpack-remove-empty-scripts/blame/1ff513bd6146a6b2d01fdc7f7da15c5b14fed14b/index.js
const NAME = 'webpack-remove-empty-scripts';
const defaultOptions = {
verbose: false,
extensions: ['css', 'scss', 'sass', 'less', 'styl'],
scriptExtensions: ['js', 'mjs'],
ignore: [],
};
// Save unique id in dependency object as marker of 'analysed module'
// to avoid the infinite recursion by collect of resources.
let dependencyId = 1;
export class RemoveEmptyScriptsPlugin {
constructor(private options: any = {}) {
this.apply = this.apply.bind(this);
Object.assign(this.options, defaultOptions, this.options);
// Deprecation of option `silent`.
if (options && options.hasOwnProperty('silent')) {
this.options.verbose = !options.silent;
console.warn(
'[DEPRECATION] the `silent` option is deprecated and will be removed on Juni 30, 2021. Use option `verbose: true` to show in console each removed empty file. Defaults, `verbose: false`.'
);
}
// if by assigned option the `ignore` was not array, then set as array
if (!Array.isArray(this.options.ignore)) {
this.options.ignore = [this.options.ignore];
}
}
apply(compiler) {
const customIgnore = this.options.ignore;
const extensionsWithoutDots = this.options.extensions.map((e) =>
e[0] === '.' ? e.substring(1) : e
);
const patternOneOfExtensions = extensionsWithoutDots
.map((ext) => escapeRegExp(ext))
.join('|');
const reStylesResource = new RegExp(
`[.](${patternOneOfExtensions})([?].*)?$`
);
compiler.hooks.compilation.tap(NAME, (compilation) => {
const resourcesCache = [];
compilation.hooks.chunkAsset.tap(NAME, (chunk, file) => {
const isNotScript = defaultOptions.scriptExtensions.every(
(ext) => file.lastIndexOf('.' + ext) < 0
);
if (isNotScript) return;
const chunkGraph = compilation.chunkGraph;
let entryResources = [];
for (const module of chunkGraph.getChunkEntryModulesIterable(chunk)) {
if (!compilation.modules.has(module)) {
throw new Error(
'checkConstraints: entry module in chunk but not in compilation ' +
` ${chunk.debugId} ${module.debugId}`
);
}
const moduleResources = collectEntryResources(
compilation,
module,
resourcesCache
);
entryResources = entryResources.concat(moduleResources);
}
const resources =
customIgnore.length > 0
? entryResources.filter((res) =>
customIgnore.every((ignore) => !res.match(ignore))
)
: entryResources;
const isStyleOnly =
resources.length &&
resources.every((resource) => reStylesResource.test(resource));
if (isStyleOnly) {
if (this.options.verbose) {
console.log('[remove-empty-scripts] remove empty js file: ' + file);
}
chunk.files.delete(file);
compilation.deleteAsset(file);
}
});
});
}
}
function collectEntryResources(compilation, module, cache) {
const moduleGraph = compilation.moduleGraph,
index = moduleGraph.getPreOrderIndex(module),
propNameDependencyId = '__dependencyWebpackRemoveEmptyScriptsUniqueId',
resources = [];
// the index can be null
if (index == null) {
return resources;
}
// index of module is unique per compilation
// module.id can be null, not used here
if (cache[index] !== undefined) {
return cache[index];
}
if (typeof module.resource === 'string') {
const resources = [module.resource];
cache[index] = resources;
return resources;
}
if (module.dependencies) {
module.dependencies.forEach((dependency) => {
let module = moduleGraph.getModule(dependency),
originModule = moduleGraph.getParentModule(dependency),
nextModule = module || originModule,
useNextModule = false;
if (!dependency.hasOwnProperty(propNameDependencyId)) {
dependency[propNameDependencyId] = dependencyId++;
useNextModule = true;
}
// debug info
//console.log('::: module ::: ', useNextModule ? '' : '-----', dependency[propNameDependencyId]);
if (nextModule && useNextModule) {
const dependencyResources = collectEntryResources(
compilation,
nextModule,
cache
);
for (
let i = 0, length = dependencyResources.length;
i !== length;
i++
) {
const file = dependencyResources[i];
if (resources.indexOf(file) < 0) {
resources.push(file);
}
}
}
});
}
if (resources.length > 0) {
cache[index] = resources;
}
return resources;
}
const reRegExpChar = /[\\^$.*+?()[\]{}|]/g;
const reHasRegExpChar = RegExp(reRegExpChar.source);
function escapeRegExp(string) {
string = String(string);
return string && reHasRegExpChar.test(string)
? string.replace(reRegExpChar, '\\$&')
: string;
}

View File

@ -1,23 +0,0 @@
(function () {
var check = document.createElement('script');
if (!('noModule' in check) && 'onbeforeload' in check) {
var support = false;
document.addEventListener(
'beforeload',
function (e) {
if (e.target === check) {
support = true;
} else if (!e.target.hasAttribute('nomodule') || !support) {
return;
}
e.preventDefault();
},
true
);
check.type = 'module';
check.src = '.';
document.head.appendChild(check);
check.remove();
}
})();

View File

@ -0,0 +1,345 @@
import * as path from 'path';
import { Configuration, WebpackPluginInstance, ProgressPlugin } from 'webpack';
import { ExecutorContext } from 'nx/src/config/misc-interfaces';
import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin';
import { readTsConfig } from '@nrwl/workspace/src/utilities/typescript';
import { LicenseWebpackPlugin } from 'license-webpack-plugin';
import TerserPlugin = require('terser-webpack-plugin');
import nodeExternals = require('webpack-node-externals');
import ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
import { NormalizedWebpackExecutorOptions } from '../executors/webpack/schema';
import { StatsJsonPlugin } from '../plugins/stats-json-plugin';
import { createCopyPlugin } from './create-copy-plugin';
import { GeneratePackageJsonWebpackPlugin } from '../plugins/generate-package-json-webpack-plugin';
import { getOutputHashFormat } from './hash-format';
const IGNORED_WEBPACK_WARNINGS = [
/The comment file/i,
/could not find any license/i,
];
const extensions = ['.ts', '.tsx', '.mjs', '.js', '.jsx'];
const mainFields = ['main', 'module'];
export function withNx(opts?: { skipTypeChecking?: boolean }) {
return function configure(
config: Configuration,
{
options,
context,
}: {
options: NormalizedWebpackExecutorOptions;
context: ExecutorContext;
}
): Configuration {
const plugins: WebpackPluginInstance[] = [];
if (!opts?.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 GeneratePackageJsonWebpackPlugin(context, options));
}
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';
return {
...config,
target: options.target,
node: false as const,
// 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.
mode:
process.env.NODE_ENV === 'development' ||
process.env.NODE_ENV === 'production'
? process.env.NODE_ENV
: 'none',
devtool:
options.sourceMap === 'hidden'
? 'hidden-source-map'
: options.sourceMap
? 'source-map'
: false,
entry,
output: {
...config.output,
libraryTarget: options.target === 'node' ? 'commonjs' : undefined,
path: options.outputPath,
filename,
chunkFilename,
hashFunction: 'xxhash64',
// Disabled for performance
pathinfo: false,
scriptType: 'module',
},
watch: options.watch,
watchOptions: {
poll: options.poll,
},
profile: options.statsJson,
resolve: {
...config.resolve,
extensions,
alias: options.fileReplacements.reduce(
(aliases, replacement) => ({
...aliases,
[replacement.replace]: replacement.with,
}),
{}
),
plugins: [
new TsconfigPathsPlugin({
configFile: options.tsConfig,
extensions,
mainFields,
}),
],
mainFields,
},
externals,
optimization: {
...config.optimization,
sideEffects: true,
minimize: !!options.optimization,
minimizer: [
options.compiler !== 'swc'
? new TerserPlugin({
parallel: true,
terserOptions: {
ecma: 2020,
safari10: true,
output: {
ascii_only: true,
comments: false,
webkit: true,
},
},
})
: new TerserPlugin({
minify: TerserPlugin.swcMinify,
// `terserOptions` options will be passed to `swc`
terserOptions: {
mangle: false,
},
}),
],
runtimeChunk: false,
concatenateModules: true,
},
performance: {
...config.performance,
hints: false,
},
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: [
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,
},
};
};
}
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('@nrwl/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);
return {
test: /\.([jt])sx?$/,
loader: path.join(__dirname, './web-babel-loader'),
exclude: /node_modules/,
options: {
rootMode: 'upward',
cwd: path.join(options.root, options.sourceRoot),
emitDecoratorMetadata: tsConfig.options.emitDecoratorMetadata,
isModern: true,
envName: process.env.NODE_ENV,
babelrc: true,
cacheDirectory: true,
cacheCompression: false,
},
};
default:
return null;
}
}

View File

@ -0,0 +1,477 @@
import * as webpack from 'webpack';
import {
Configuration,
ids,
RuleSetRule,
WebpackPluginInstance,
} from 'webpack';
import { SubresourceIntegrityPlugin } from 'webpack-subresource-integrity';
import * as path from 'path';
import { getOutputHashFormat } from '@nrwl/webpack/src/utils/hash-format';
import { PostcssCliResources } from '@nrwl/webpack/src/utils/webpack/plugins/postcss-cli-resources';
import { normalizeExtraEntryPoints } from '@nrwl/webpack/src/utils/webpack/normalize-entry';
import { NormalizedWebpackExecutorOptions } from '../executors/webpack/schema';
import { getClientEnvironment } from './get-client-environment';
import { RemoveHashPlugin } from './webpack/plugins/remove-hash-plugin';
import { ScriptsWebpackPlugin } from './webpack/plugins/scripts-webpack-plugin';
import { getCSSModuleLocalIdent } from './get-css-module-local-ident';
import CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
import MiniCssExtractPlugin = require('mini-css-extract-plugin');
import autoprefixer = require('autoprefixer');
import postcssImports = require('postcss-import');
import { Postcss } from 'postcss';
import { basename } from 'path';
interface PostcssOptions {
(loader: any): any;
config?: string;
}
export function withWeb() {
return function configure(
config: Configuration,
{ options }: { options: NormalizedWebpackExecutorOptions }
): Configuration {
const plugins = [];
const stylesOptimization =
typeof options.optimization === 'object'
? options.optimization.styles
: options.optimization;
if (Array.isArray(options.scripts)) {
plugins.push(...createScriptsPlugin(options));
}
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)$/,
})
);
}
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(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) {
const chunkNames: string[] = [];
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].push(resolvedPath);
} else {
entry[style.bundleName] = [resolvedPath];
}
// Add global css paths.
globalStylePaths.push(resolvedPath);
});
if (chunkNames.length > 0) {
// Add plugin to remove hashes from lazy styles.
plugins.push(new RemoveHashPlugin({ chunkNames, hashFormat }));
}
}
const cssModuleRules: RuleSetRule[] = [
{
test: /\.module\.css$/,
use: getCommonLoadersForCssModules(options, includePaths),
},
{
test: /\.module\.(scss|sass)$/,
use: [
...getCommonLoadersForCssModules(options, includePaths),
{
loader: require.resolve('sass-loader'),
options: {
implementation: require('sass'),
sassOptions: {
fiber: false,
precision: 8,
includePaths,
},
},
},
],
},
{
test: /\.module\.less$/,
use: [
...getCommonLoadersForCssModules(options, includePaths),
{
loader: require.resolve('less-loader'),
options: {
lessOptions: {
paths: includePaths,
},
},
},
],
},
{
test: /\.module\.styl$/,
use: [
...getCommonLoadersForCssModules(options, includePaths),
{
loader: require.resolve('stylus-loader'),
options: {
stylusOptions: {
include: includePaths,
},
},
},
],
},
];
const globalCssRules: RuleSetRule[] = [
{
test: /\.css$/,
use: getCommonLoadersForGlobalCss(options, includePaths),
},
{
test: /\.scss$|\.sass$/,
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$/,
use: [
...getCommonLoadersForGlobalCss(options, includePaths),
{
loader: require.resolve('less-loader'),
options: {
sourceMap: options.sourceMap,
lessOptions: {
javascriptEnabled: true,
...lessPathOptions,
},
},
},
],
},
{
test: /\.styl$/,
use: [
...getCommonLoadersForGlobalCss(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,
// load global css as css files
{
test: /\.css$|\.scss$|\.sass$|\.less$|\.styl$/,
include: globalStylePaths,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: { esModule: true },
},
{ loader: require.resolve('css-loader') },
{
loader: require.resolve('postcss-loader'),
options: {
implementation: require('postcss'),
postcssOptions: postcssOptionsCreator(options, includePaths),
},
},
],
},
],
},
];
plugins.push(
// extract global css from js files into own css file
new MiniCssExtractPlugin({
filename: `[name]${hashFormat.extract}.css`,
})
);
// context needs to be set for babel to pick up correct babelrc
config.context = path.join(options.root, options.projectRoot);
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,
emitOnErrors: false,
moduleIds: 'deterministic' as const,
runtimeChunk: options.runtimeChunk ? ('single' as const) : false,
splitChunks: {
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.plugins.push(...plugins);
config.module = {
...config.module,
rules: [
...config.module.rules,
...rules,
{
test: /\.(bmp|png|jpe?g|gif|webp|avif)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10_000, // 10 kB
},
},
},
{
test: /\.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)$/,
loader: require.resolve('file-loader'),
options: {
name: `[name]${hashFormat.file}.[ext]`,
},
},
],
};
return config;
};
}
function createScriptsPlugin(
options: NormalizedWebpackExecutorOptions
): 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: NormalizedWebpackExecutorOptions,
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),
},
},
];
}
function getCommonLoadersForGlobalCss(
options: NormalizedWebpackExecutorOptions,
includePaths: string[]
) {
return [
{
loader: options.extractCss
? MiniCssExtractPlugin.loader
: require.resolve('style-loader'),
},
{ loader: require.resolve('css-loader') },
{
loader: require.resolve('postcss-loader'),
options: {
implementation: require('postcss'),
postcssOptions: postcssOptionsCreator(options, includePaths),
},
},
];
}
function postcssOptionsCreator(
options: NormalizedWebpackExecutorOptions,
includePaths: string[]
) {
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),
}),
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;
}