parent
0bc93ee83d
commit
6feb56e014
@ -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',
|
||||
|
||||
@ -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, {
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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(
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -30,6 +30,7 @@ export async function* devServerExecutor(
|
||||
const buildOptions = normalizeOptions(
|
||||
getBuildOptions(serveOptions, context),
|
||||
context.root,
|
||||
projectRoot,
|
||||
sourceRoot
|
||||
);
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -85,5 +85,6 @@ export interface NormalizedWebpackExecutorOptions
|
||||
extends WebpackExecutorOptions {
|
||||
assets?: AssetGlobPattern[];
|
||||
root?: string;
|
||||
projectRoot?: string;
|
||||
sourceRoot?: string;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
10
packages/webpack/src/plugins/stats-json-plugin.ts
Normal file
10
packages/webpack/src/plugins/stats-json-plugin.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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 we’re 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;
|
||||
}
|
||||
}
|
||||
|
||||
24
packages/webpack/src/utils/create-copy-plugin.ts
Normal file
24
packages/webpack/src/utils/create-copy-plugin.ts
Normal 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,
|
||||
},
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
30
packages/webpack/src/utils/get-client-environment.ts
Normal file
30
packages/webpack/src/utils/get-client-environment.ts
Normal 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 };
|
||||
}
|
||||
31
packages/webpack/src/utils/get-css-module-local-ident.ts
Normal file
31
packages/webpack/src/utils/get-css-module-local-ident.ts
Normal 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, '_');
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)];
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
@ -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}]]`;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
})();
|
||||
345
packages/webpack/src/utils/with-nx.ts
Normal file
345
packages/webpack/src/utils/with-nx.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
477
packages/webpack/src/utils/with-web.ts
Normal file
477
packages/webpack/src/utils/with-web.ts
Normal 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;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user