diff --git a/packages/custom-webpack/README.md b/packages/custom-webpack/README.md index 0b4d357c8..7a24fbcc8 100644 --- a/packages/custom-webpack/README.md +++ b/packages/custom-webpack/README.md @@ -4,6 +4,28 @@ Allow customizing build configuration without ejecting webpack configuration (`ng eject`) +# Table of Contents + +- [Usage](#usage) + - [For Example](#for-example) +- [Builders](#builders) + - [Custom Webpack `browser`](#custom-webpack-browser) + - [Custom Webpack `dev-server`](#custom-webpack-dev-server) + - [Example](#example) + - [Custom Webpack `server`](#custom-webpack-server) + - [Custom Webpack `karma`](#custom-webpack-karma) + - [Custom Webpack `extract-i18n`](#custom-webpack-extract-i18n) + - [Example](#example-1) +- [Custom Webpack Config Object](#custom-webpack-config-object) + - [Merging Plugins Configuration:](#merging-plugins-configuration-) + - [Custom Webpack Promisified Config](#custom-webpack-promisified-config) + - [Custom Webpack Config Function](#custom-webpack-config-function) +- [Index Transform](#index-transform) + - [Example](#example-2) +- [ES Modules (ESM) Support](#es-modules-esm-support) +- [Verbose Logging](#verbose-logging) +- [Further Reading](#further-reading) + # This documentation is for the latest major version only ## Previous versions @@ -504,6 +526,39 @@ Custom Webpack builder fully supports ESM. - If you want to use TS config in ESM app, you must set the loader to `ts-node/esm` when running `ng build`. Also, in that case `tsconfig.json` for `ts-node` no longer defaults to `tsConfig` from the `browser` target - you have to specify it manually via environment variable. [Example](../../examples/custom-webpack/sanity-app-esm/package.json#L10). _Note that tsconfig paths are not supported in TS configs within ESM apps. That is because [tsconfig-paths](https://github.com/dividab/tsconfig-paths) do not support ESM._ +# Verbose Logging + +Custom Webpack allows enabling verbose logging for configuration properties. This can be achieved by providing the `verbose` object in builder options. Given the following example: + +```json +{ + "builder": "@angular-builders/custom-webpack:browser", + "options": { + "customWebpackConfig": { + "verbose": { + "properties": ["entry"] + } + } + } +} +``` + +`properties` is an array of strings that supports individual or deeply nested keys (`output.publicPath` and `plugins[0]` are valid keys). The number of times to recurse the object while formatting before it's logged is controlled by the `serializationDepth` property: + +```json +{ + "builder": "@angular-builders/custom-webpack:browser", + "options": { + "customWebpackConfig": { + "verbose": { + "properties": ["plugins[0]"], + "serializationDepth": 5 + } + } + } +} +``` + # Further Reading - [Customizing Angular CLI build - an alternative to ng eject](https://medium.com/angular-in-depth/customizing-angular-cli-build-an-alternative-to-ng-eject-v2-c655768b48cc) diff --git a/packages/custom-webpack/e2e/custom-webpack-config-schema.ts b/packages/custom-webpack/e2e/custom-webpack-config-schema.ts index f3ff004df..28f38cba0 100644 --- a/packages/custom-webpack/e2e/custom-webpack-config-schema.ts +++ b/packages/custom-webpack/e2e/custom-webpack-config-schema.ts @@ -18,6 +18,24 @@ export const customWebpackConfig = { type: 'boolean', description: 'Flag that indicates whether to replace duplicate webpack plugins or not', }, + verbose: { + type: 'object', + description: 'Determines whether to log configuration properties into a console', + properties: { + properties: { + description: + "A list of properties to log into a console, for instance, `['plugins', 'mode', 'entry']`", + type: 'array', + items: { + type: 'string', + }, + }, + serializationDepth: { + type: 'number', + description: 'The number of times to recurse the object while formatting', + }, + }, + }, }, }, { diff --git a/packages/custom-webpack/src/custom-webpack-builder-config.ts b/packages/custom-webpack/src/custom-webpack-builder-config.ts index bc7439961..24f84d0c2 100644 --- a/packages/custom-webpack/src/custom-webpack-builder-config.ts +++ b/packages/custom-webpack/src/custom-webpack-builder-config.ts @@ -6,4 +6,8 @@ export interface CustomWebpackBuilderConfig { path?: string; mergeRules?: MergeRules; replaceDuplicatePlugins?: boolean; + verbose?: { + properties?: string[]; + serializationDepth?: number; + }; } diff --git a/packages/custom-webpack/src/custom-webpack-builder.spec.ts b/packages/custom-webpack/src/custom-webpack-builder.spec.ts index 9c9d3d5ea..bf33fa8d4 100644 --- a/packages/custom-webpack/src/custom-webpack-builder.spec.ts +++ b/packages/custom-webpack/src/custom-webpack-builder.spec.ts @@ -1,4 +1,4 @@ -import { Path } from '@angular-devkit/core'; +import { logging, Path } from '@angular-devkit/core'; import { Configuration } from 'webpack'; import { CustomizeRule } from 'webpack-merge'; @@ -263,4 +263,95 @@ describe('CustomWebpackBuilder', () => { }, }); }); + + describe('verbose logging', () => { + let logger: logging.LoggerApi; + + beforeAll(() => { + logger = { info: jest.fn() } as unknown as logging.LoggerApi; + }); + + it('should serialize the object and log it', async () => { + const customWebpackConfig = { + entry: { + myModule: './my.module.js', + }, + }; + + createConfigFile(defaultWebpackConfigPath, customWebpackConfig); + + await CustomWebpackBuilder.buildWebpackConfig( + __dirname as Path, + { + verbose: { + properties: ['entry'], + }, + }, + baseWebpackConfig, + {}, + targetOptions, + logger + ); + + expect(logger.info).toHaveBeenCalledWith(`{ myModule: './my.module.js' }`); + }); + + it('should be able to provide deeply nested keys as properties', async () => { + const customWebpackConfig = { + output: { + enabledChunkLoadingTypes: ['jsonp'], + }, + }; + + createConfigFile(defaultWebpackConfigPath, customWebpackConfig); + + await CustomWebpackBuilder.buildWebpackConfig( + __dirname as Path, + { + verbose: { + properties: ['output.enabledChunkLoadingTypes[0]'], + }, + }, + baseWebpackConfig, + {}, + targetOptions, + logger + ); + + expect(logger.info).toHaveBeenCalledWith(`'jsonp'`); + }); + + it('should skip serializing deep objects (if serialize depth is not provided)', async () => { + const customWebpackConfig = { + plugins: [ + { + this: { + property: { + is: { + nested: true, + }, + }, + }, + }, + ], + }; + + createConfigFile(defaultWebpackConfigPath, customWebpackConfig); + + await CustomWebpackBuilder.buildWebpackConfig( + __dirname as Path, + { + verbose: { + properties: ['plugins[0]'], + }, + }, + baseWebpackConfig, + {}, + targetOptions, + logger + ); + + expect(logger.info).toHaveBeenCalledWith(`{ this: { property: { is: [Object] } } }`); + }); + }); }); diff --git a/packages/custom-webpack/src/custom-webpack-builder.ts b/packages/custom-webpack/src/custom-webpack-builder.ts index 5846e6b2a..ee1e9bd4b 100644 --- a/packages/custom-webpack/src/custom-webpack-builder.ts +++ b/packages/custom-webpack/src/custom-webpack-builder.ts @@ -1,4 +1,6 @@ +import { inspect } from 'util'; import { getSystemPath, logging, Path } from '@angular-devkit/core'; +import { get } from 'lodash'; import { Configuration } from 'webpack'; import { CustomWebpackBrowserSchema } from './browser'; import { CustomWebpackBuilderConfig } from './custom-webpack-builder-config'; @@ -25,7 +27,7 @@ type CustomWebpackConfig = export class CustomWebpackBuilder { static async buildWebpackConfig( root: Path, - config: CustomWebpackBuilderConfig, + config: CustomWebpackBuilderConfig | null, baseWebpackConfig: Configuration, buildOptions: any, targetOptions: TargetOptions, @@ -41,26 +43,33 @@ export class CustomWebpackBuilder { const configOrFactoryOrPromise = await resolveCustomWebpackConfig(path, tsConfig, logger); if (typeof configOrFactoryOrPromise === 'function') { - // That exported function can be synchronous either - // asynchronous. Given the following example: - // `module.exports = async (config) => { ... }` - return configOrFactoryOrPromise(baseWebpackConfig, buildOptions, targetOptions); + // The exported function can return a new configuration synchronously + // or return a promise that resolves to a new configuration. + const finalWebpackConfig = await configOrFactoryOrPromise( + baseWebpackConfig, + buildOptions, + targetOptions + ); + logConfigProperties(config, finalWebpackConfig, logger); + return finalWebpackConfig; } - // The user can also export a `Promise` that resolves `Configuration` - // object. Given the following example: + // The user can also export a promise that resolves to a `Configuration` object. + // Suppose the following example: // `module.exports = new Promise(resolve => resolve({ ... }))` - // If the user has exported a plain object, like: - // `module.exports = { ... }` - // then it will promisified and awaited + // This is valid both for promise and non-promise cases. If users export + // a plain object, for instance, `module.exports = { ... }`, then it will + // be wrapped into a promise and also `awaited`. const resolvedConfig = await configOrFactoryOrPromise; - return mergeConfigs( + const finalWebpackConfig = mergeConfigs( baseWebpackConfig, resolvedConfig, config.mergeRules, config.replaceDuplicatePlugins ); + logConfigProperties(config, finalWebpackConfig, logger); + return finalWebpackConfig; } } @@ -73,3 +82,23 @@ async function resolveCustomWebpackConfig( return loadModule(path); } + +function logConfigProperties( + config: CustomWebpackBuilderConfig, + webpackConfig: Configuration, + logger: logging.LoggerApi +): void { + // There's no reason to log the entire configuration object + // since Angular's Webpack configuration is huge by default + // and doesn't bring any meaningful context by being printed + // entirely. Users can provide a list of properties they want to be logged. + if (config.verbose?.properties) { + for (const property of config.verbose.properties) { + const value = get(webpackConfig, property); + if (value) { + const message = inspect(value, /* showHidden */ false, config.verbose.serializationDepth); + logger.info(message); + } + } + } +} diff --git a/packages/custom-webpack/src/schema.ext.json b/packages/custom-webpack/src/schema.ext.json index d375fd53a..f272638ac 100644 --- a/packages/custom-webpack/src/schema.ext.json +++ b/packages/custom-webpack/src/schema.ext.json @@ -18,6 +18,23 @@ "replaceDuplicatePlugins": { "type": "boolean", "description": "Flag that indicates whether to replace duplicate webpack plugins or not" + }, + "verbose": { + "type": "object", + "description": "Determines whether to log configuration properties into a console", + "properties": { + "properties": { + "description": "A list of properties to log into a console, for instance, `['plugins', 'mode', 'entry']`", + "type": "array", + "items": { + "type": "string" + } + }, + "serializationDepth": { + "type": "number", + "description": "The number of times to recurse the object while formatting" + } + } } } },