Skip to content

Commit ea11c55

Browse files
alan-agius4dgp1130
authored andcommittedMar 23, 2020
feat(@angular-devkit/build-angular): show warnings when depending on CommonJS.
Depending on CommonJS modules is know to cause optimization bailouts. With this change when running a browser build and scripts optimization is enabled we display a warning. To suppress the warning for a particular package, users can use the `allowedCommonJsDepedencies` builder options. Example: ``` "build": { "builder": "@angular-devkit/build-angular:browser", "options": { ... "allowedCommonJsDepedencies": ["bootstrap"] }, } ``` Reference: TOOL-1328
1 parent ed90080 commit ea11c55

File tree

8 files changed

+202
-28
lines changed

8 files changed

+202
-28
lines changed
 

‎packages/angular/cli/lib/config/schema.json

+7
Original file line numberDiff line numberDiff line change
@@ -974,6 +974,13 @@
974974
"anonymous",
975975
"use-credentials"
976976
]
977+
},
978+
"allowedCommonJsDependencies": {
979+
"description": "A list of CommonJS packages that are allowed to be used without a built time warning.",
980+
"type": "array",
981+
"items": {
982+
"type": "string"
983+
}
977984
}
978985
},
979986
"additionalProperties": false,

‎packages/angular_devkit/build_angular/src/angular-cli-files/models/build-options.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,8 @@ export interface BuildOptions {
8787

8888
/* Append script target version to filename. */
8989
esVersionInFileName?: boolean;
90-
9190
experimentalRollupPass?: boolean;
91+
allowedCommonJsDependencies?: string[];
9292
}
9393

9494
export interface WebpackTestOptions extends BuildOptions {

‎packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/browser.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88
import { LicenseWebpackPlugin } from 'license-webpack-plugin';
99
import * as webpack from 'webpack';
10+
import { CommonJsUsageWarnPlugin } from '../../plugins/webpack';
1011
import { WebpackConfigOptions } from '../build-options';
1112
import { getSourceMapDevTool, isPolyfillsEntry, normalizeExtraEntryPoints } from './utils';
1213

@@ -23,12 +24,14 @@ export function getBrowserConfig(wco: WebpackConfigOptions): webpack.Configurati
2324
vendorChunk,
2425
commonChunk,
2526
styles,
27+
allowedCommonJsDependencies,
28+
optimization,
2629
} = buildOptions;
2730

2831
const extraPlugins = [];
2932

3033
let isEval = false;
31-
const { styles: stylesOptimization, scripts: scriptsOptimization } = buildOptions.optimization;
34+
const { styles: stylesOptimization, scripts: scriptsOptimization } = optimization;
3235
const {
3336
styles: stylesSourceMap,
3437
scripts: scriptsSourceMap,
@@ -123,7 +126,12 @@ export function getBrowserConfig(wco: WebpackConfigOptions): webpack.Configurati
123126
},
124127
},
125128
},
126-
plugins: extraPlugins,
129+
plugins: [
130+
new CommonJsUsageWarnPlugin({
131+
allowedDepedencies: allowedCommonJsDependencies,
132+
}),
133+
...extraPlugins,
134+
],
127135
node: false,
128136
};
129137
}

‎packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts

+26-24
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,13 @@ import {
3636
cachingDisabled,
3737
shouldBeautify,
3838
} from '../../../utils/environment-options';
39-
import { BundleBudgetPlugin } from '../../plugins/bundle-budget';
40-
import { NamedLazyChunksPlugin } from '../../plugins/named-chunks-plugin';
41-
import { OptimizeCssWebpackPlugin } from '../../plugins/optimize-css-webpack-plugin';
42-
import { ScriptsWebpackPlugin } from '../../plugins/scripts-webpack-plugin';
43-
import { WebpackRollupLoader } from '../../plugins/webpack';
39+
import {
40+
BundleBudgetPlugin,
41+
NamedLazyChunksPlugin,
42+
OptimizeCssWebpackPlugin,
43+
ScriptsWebpackPlugin,
44+
WebpackRollupLoader,
45+
} from '../../plugins/webpack';
4446
import { findAllNodeModules } from '../../utilities/find-up';
4547
import { WebpackConfigOptions } from '../build-options';
4648
import { getEsVersionForFileName, getOutputHashFormat, normalizeExtraEntryPoints } from './utils';
@@ -149,7 +151,7 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
149151
// tslint:disable-next-line: no-any
150152
(compilation.mainTemplate.hooks as any).assetPath.tap(
151153
'build-angular',
152-
(filename: string | ((data: ChunkData) => string), data: ChunkData) => {
154+
(filename: string | ((data: ChunkData) => string), data: ChunkData) => {
153155
const assetName = typeof filename === 'function' ? filename(data) : filename;
154156
const isMap = assetName && assetName.endsWith('.map');
155157

@@ -313,6 +315,12 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
313315
extraPlugins.push(new NamedLazyChunksPlugin());
314316
}
315317

318+
if (!differentialLoadingMode) {
319+
// Budgets are computed after differential builds, not via a plugin.
320+
// https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/build_angular/src/browser/index.ts
321+
extraPlugins.push(new BundleBudgetPlugin({ budgets: buildOptions.budgets }));
322+
}
323+
316324
let sourceMapUseRule;
317325
if ((scriptsSourceMap || stylesSourceMap) && vendorSourceMap) {
318326
sourceMapUseRule = {
@@ -411,18 +419,18 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
411419
allowMinify &&
412420
(buildOptions.platform == 'server'
413421
? {
414-
ecma: terserEcma,
415-
global_defs: angularGlobalDefinitions,
416-
keep_fnames: true,
417-
}
422+
ecma: terserEcma,
423+
global_defs: angularGlobalDefinitions,
424+
keep_fnames: true,
425+
}
418426
: {
419-
ecma: terserEcma,
420-
pure_getters: buildOptions.buildOptimizer,
421-
// PURE comments work best with 3 passes.
422-
// See https://github.com/webpack/webpack/issues/2899#issuecomment-317425926.
423-
passes: buildOptions.buildOptimizer ? 3 : 1,
424-
global_defs: angularGlobalDefinitions,
425-
}),
427+
ecma: terserEcma,
428+
pure_getters: buildOptions.buildOptimizer,
429+
// PURE comments work best with 3 passes.
430+
// See https://github.com/webpack/webpack/issues/2899#issuecomment-317425926.
431+
passes: buildOptions.buildOptimizer ? 3 : 1,
432+
global_defs: angularGlobalDefinitions,
433+
}),
426434
// We also want to avoid mangling on server.
427435
// Name mangling is handled within the browser builder
428436
mangle: allowMangle && buildOptions.platform !== 'server' && !differentialLoadingMode,
@@ -543,13 +551,7 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
543551
minimizer: [
544552
new HashedModuleIdsPlugin(),
545553
...extraMinimizers,
546-
].concat(differentialLoadingMode ? [
547-
// Budgets are computed after differential builds, not via a plugin.
548-
// https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/build_angular/src/browser/index.ts
549-
] : [
550-
// Non differential builds should be computed here, as a plugin.
551-
new BundleBudgetPlugin({ budgets: buildOptions.budgets }),
552-
]),
554+
],
553555
},
554556
plugins: [
555557
// Always replace the context for the System.import in angular/core to prevent warnings.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
2+
/**
3+
* @license
4+
* Copyright Google Inc. All Rights Reserved.
5+
*
6+
* Use of this source code is governed by an MIT-style license that can be
7+
* found in the LICENSE file at https://angular.io/license
8+
*/
9+
10+
import { isAbsolute } from 'path';
11+
import { Compiler, compilation } from 'webpack';
12+
13+
// Webpack doesn't export these so the deep imports can potentially break.
14+
const CommonJsRequireDependency = require('webpack/lib/dependencies/CommonJsRequireDependency');
15+
const AMDDefineDependency = require('webpack/lib/dependencies/AMDDefineDependency');
16+
17+
// The below is extended because there are not in the typings
18+
interface WebpackModule extends compilation.Module {
19+
name?: string;
20+
rawRequest?: string;
21+
dependencies: unknown[];
22+
issuer: WebpackModule | null;
23+
userRequest?: string;
24+
}
25+
26+
export interface CommonJsUsageWarnPluginOptions {
27+
/** A list of CommonJS packages that are allowed to be used without a warning. */
28+
allowedDepedencies?: string[];
29+
}
30+
31+
export class CommonJsUsageWarnPlugin {
32+
private shownWarnings = new Set<string>();
33+
34+
constructor(private options: CommonJsUsageWarnPluginOptions = {}) {
35+
36+
}
37+
38+
apply(compiler: Compiler) {
39+
compiler.hooks.compilation.tap('CommonJsUsageWarnPlugin', compilation => {
40+
compilation.hooks.finishModules.tap('CommonJsUsageWarnPlugin', modules => {
41+
for (const { dependencies, rawRequest, issuer } of modules as unknown as WebpackModule[]) {
42+
if (
43+
!rawRequest ||
44+
rawRequest.startsWith('.') ||
45+
isAbsolute(rawRequest)
46+
) {
47+
// Skip if module is absolute or relative.
48+
continue;
49+
}
50+
51+
if (this.options.allowedDepedencies?.includes(rawRequest)) {
52+
// Skip as this module is allowed even if it's a CommonJS.
53+
continue;
54+
}
55+
56+
if (this.hasCommonJsDependencies(dependencies)) {
57+
// Dependency is CommonsJS or AMD.
58+
59+
// Check if it's parent issuer is also a CommonJS dependency.
60+
// In case it is skip as an warning will be show for the parent CommonJS dependency.
61+
if (this.hasCommonJsDependencies(issuer?.issuer?.dependencies ?? [])) {
62+
continue;
63+
}
64+
65+
// Find the main issuer (entry-point).
66+
let mainIssuer = issuer;
67+
while (mainIssuer?.issuer) {
68+
mainIssuer = mainIssuer.issuer;
69+
}
70+
71+
// Only show warnings for modules from main entrypoint.
72+
if (mainIssuer?.name === 'main') {
73+
const warning = `${issuer?.userRequest} depends on ${rawRequest}. CommonJS or AMD dependencies can cause optimization bailouts.`;
74+
75+
// Avoid showing the same warning multiple times when in 'watch' mode.
76+
if (!this.shownWarnings.has(warning)) {
77+
compilation.warnings.push(warning);
78+
this.shownWarnings.add(warning);
79+
}
80+
}
81+
}
82+
}
83+
});
84+
});
85+
}
86+
87+
private hasCommonJsDependencies(dependencies: unknown[]): boolean {
88+
return dependencies.some(d => d instanceof CommonJsRequireDependency || d instanceof AMDDefineDependency);
89+
}
90+
91+
}

‎packages/angular_devkit/build_angular/src/angular-cli-files/plugins/webpack.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ export { BundleBudgetPlugin, BundleBudgetPluginOptions } from './bundle-budget';
1313
export { ScriptsWebpackPlugin, ScriptsWebpackPluginOptions } from './scripts-webpack-plugin';
1414
export { SuppressExtractedTextChunksWebpackPlugin } from './suppress-entry-chunks-webpack-plugin';
1515
export { RemoveHashPlugin, RemoveHashPluginOptions } from './remove-hash-plugin';
16-
export { NamedLazyChunksPlugin as NamedChunksPlugin } from './named-chunks-plugin';
16+
export { NamedLazyChunksPlugin } from './named-chunks-plugin';
17+
export { CommonJsUsageWarnPlugin } from './common-js-usage-warn-plugin';
1718
export {
1819
default as PostcssCliResources,
1920
PostcssCliResourcesOptions,

‎packages/angular_devkit/build_angular/src/browser/schema.json

+8
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,14 @@
379379
"type": "boolean",
380380
"description": "Concatenate modules with Rollup before bundling them with Webpack.",
381381
"default": false
382+
},
383+
"allowedCommonJsDependencies": {
384+
"description": "A list of CommonJS packages that are allowed to be used without a built time warning.",
385+
"type": "array",
386+
"items": {
387+
"type": "string"
388+
},
389+
"default": []
382390
}
383391
},
384392
"additionalProperties": false,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import { Architect } from '@angular-devkit/architect';
9+
import { logging } from '@angular-devkit/core';
10+
import { createArchitect, host } from '../utils';
11+
12+
describe('Browser Builder commonjs warning', () => {
13+
const targetSpec = { project: 'app', target: 'build' };
14+
15+
let architect: Architect;
16+
let logger: logging.Logger;
17+
let logs: string[];
18+
19+
beforeEach(async () => {
20+
await host.initialize().toPromise();
21+
architect = (await createArchitect(host.root())).architect;
22+
23+
// Add a Common JS dependency
24+
host.appendToFile('src/app/app.component.ts', `import 'bootstrap';`);
25+
26+
// Create logger
27+
logger = new logging.Logger('');
28+
logs = [];
29+
logger.subscribe(e => logs.push(e.message));
30+
});
31+
32+
afterEach(async () => host.restore().toPromise());
33+
34+
it('should show warning when depending on a Common JS bundle', async () => {
35+
const run = await architect.scheduleTarget(targetSpec, undefined, { logger });
36+
const output = await run.result;
37+
expect(output.success).toBe(true);
38+
const logMsg = logs.join();
39+
expect(logMsg).toMatch(/WARNING in.+app\.component\.ts depends on bootstrap\. CommonJS or AMD dependencies/);
40+
expect(logMsg).not.toContain('jquery', 'Should not warn on transitive CommonJS packages which parent is also CommonJS.');
41+
await run.stop();
42+
});
43+
44+
it('should not show warning when depending on a Common JS bundle which is allowed', async () => {
45+
const overrides = {
46+
allowedCommonJsDependencies: [
47+
'bootstrap',
48+
],
49+
};
50+
51+
const run = await architect.scheduleTarget(targetSpec, overrides, { logger });
52+
const output = await run.result;
53+
expect(output.success).toBe(true);
54+
expect(logs.join()).not.toContain('WARNING');
55+
await run.stop();
56+
});
57+
});

0 commit comments

Comments
 (0)
Please sign in to comment.