Skip to content

Commit

Permalink
feat(custom-webpack): provide verbose option (#1307)
Browse files Browse the repository at this point in the history
* feat(custom-webpack): provide `verbose` option

Co-authored-by: JeB <9823087+just-jeb@users.noreply.github.com>
  • Loading branch information
arturovt and just-jeb authored Nov 13, 2022
1 parent acc0e55 commit fb5c228
Show file tree
Hide file tree
Showing 6 changed files with 226 additions and 12 deletions.
55 changes: 55 additions & 0 deletions packages/custom-webpack/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
18 changes: 18 additions & 0 deletions packages/custom-webpack/e2e/custom-webpack-config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
},
},
},
},
{
Expand Down
4 changes: 4 additions & 0 deletions packages/custom-webpack/src/custom-webpack-builder-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@ export interface CustomWebpackBuilderConfig {
path?: string;
mergeRules?: MergeRules;
replaceDuplicatePlugins?: boolean;
verbose?: {
properties?: string[];
serializationDepth?: number;
};
}
93 changes: 92 additions & 1 deletion packages/custom-webpack/src/custom-webpack-builder.spec.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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] } } }`);
});
});
});
51 changes: 40 additions & 11 deletions packages/custom-webpack/src/custom-webpack-builder.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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,
Expand All @@ -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;
}
}

Expand All @@ -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);
}
}
}
}
17 changes: 17 additions & 0 deletions packages/custom-webpack/src/schema.ext.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
}
},
Expand Down

0 comments on commit fb5c228

Please sign in to comment.