@angular-devkit/build-angular
Version:
Angular Webpack Build Facade
255 lines (254 loc) • 10.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.NgBuildAnalyticsPlugin = exports.countOccurrences = void 0;
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
const core_1 = require("@angular-devkit/core");
const NormalModule = require('webpack/lib/NormalModule');
const webpackAllErrorMessageRe = /^([^(]+)\(\d+,\d\): (.*)$/gm;
const webpackTsErrorMessageRe = /^[^(]+\(\d+,\d\): error (TS\d+):/;
/**
* Faster than using a RegExp, so we use this to count occurences in source code.
* @param source The source to look into.
* @param match The match string to look for.
* @param wordBreak Whether to check for word break before and after a match was found.
* @return The number of matches found.
* @private
*/
function countOccurrences(source, match, wordBreak = false) {
if (match.length == 0) {
return source.length + 1;
}
let count = 0;
// We condition here so branch prediction happens out of the loop, not in it.
if (wordBreak) {
const re = /\w/;
for (let pos = source.lastIndexOf(match); pos >= 0; pos = source.lastIndexOf(match, pos)) {
if (!(re.test(source[pos - 1] || '') || re.test(source[pos + match.length] || ''))) {
count++; // 1 match, AH! AH! AH! 2 matches, AH! AH! AH!
}
pos -= match.length;
if (pos < 0) {
break;
}
}
}
else {
for (let pos = source.lastIndexOf(match); pos >= 0; pos = source.lastIndexOf(match, pos)) {
count++; // 1 match, AH! AH! AH! 2 matches, AH! AH! AH!
pos -= match.length;
if (pos < 0) {
break;
}
}
}
return count;
}
exports.countOccurrences = countOccurrences;
/**
* Holder of statistics related to the build.
*/
class AnalyticsBuildStats {
constructor() {
this.errors = [];
this.numberOfNgOnInit = 0;
this.numberOfComponents = 0;
this.initialChunkSize = 0;
this.totalChunkCount = 0;
this.totalChunkSize = 0;
this.lazyChunkCount = 0;
this.lazyChunkSize = 0;
this.assetCount = 0;
this.assetSize = 0;
this.polyfillSize = 0;
this.cssSize = 0;
}
}
/**
* Analytics plugin that reports the analytics we want from the CLI.
*/
class NgBuildAnalyticsPlugin {
constructor(_projectRoot, _analytics, _category, _isIvy) {
this._projectRoot = _projectRoot;
this._analytics = _analytics;
this._category = _category;
this._isIvy = _isIvy;
this._built = false;
this._stats = new AnalyticsBuildStats();
}
_reset() {
this._stats = new AnalyticsBuildStats();
}
_getMetrics(stats) {
const startTime = +(stats.startTime || 0);
const endTime = +(stats.endTime || 0);
const metrics = [];
metrics[core_1.analytics.NgCliAnalyticsMetrics.BuildTime] = (endTime - startTime);
metrics[core_1.analytics.NgCliAnalyticsMetrics.NgOnInitCount] = this._stats.numberOfNgOnInit;
metrics[core_1.analytics.NgCliAnalyticsMetrics.NgComponentCount] = this._stats.numberOfComponents;
metrics[core_1.analytics.NgCliAnalyticsMetrics.InitialChunkSize] = this._stats.initialChunkSize;
metrics[core_1.analytics.NgCliAnalyticsMetrics.TotalChunkCount] = this._stats.totalChunkCount;
metrics[core_1.analytics.NgCliAnalyticsMetrics.TotalChunkSize] = this._stats.totalChunkSize;
metrics[core_1.analytics.NgCliAnalyticsMetrics.LazyChunkCount] = this._stats.lazyChunkCount;
metrics[core_1.analytics.NgCliAnalyticsMetrics.LazyChunkSize] = this._stats.lazyChunkSize;
metrics[core_1.analytics.NgCliAnalyticsMetrics.AssetCount] = this._stats.assetCount;
metrics[core_1.analytics.NgCliAnalyticsMetrics.AssetSize] = this._stats.assetSize;
metrics[core_1.analytics.NgCliAnalyticsMetrics.PolyfillSize] = this._stats.polyfillSize;
metrics[core_1.analytics.NgCliAnalyticsMetrics.CssSize] = this._stats.cssSize;
return metrics;
}
_getDimensions() {
const dimensions = [];
if (this._stats.errors.length) {
// Adding commas before and after so the regex are easier to define filters.
dimensions[core_1.analytics.NgCliAnalyticsDimensions.BuildErrors] = `,${this._stats.errors.join()},`;
}
dimensions[core_1.analytics.NgCliAnalyticsDimensions.NgIvyEnabled] = this._isIvy;
return dimensions;
}
_reportBuildMetrics(stats) {
const dimensions = this._getDimensions();
const metrics = this._getMetrics(stats);
this._analytics.event(this._category, 'build', { dimensions, metrics });
}
_reportRebuildMetrics(stats) {
const dimensions = this._getDimensions();
const metrics = this._getMetrics(stats);
this._analytics.event(this._category, 'rebuild', { dimensions, metrics });
}
_checkTsNormalModule(module) {
if (module._source) {
// PLEASE REMEMBER:
// We're dealing with ES5 _or_ ES2015 JavaScript at this point (we don't know for sure).
// Just count the ngOnInit occurences. Comments/Strings/calls occurences should be sparse
// so we just consider them within the margin of error. We do break on word break though.
this._stats.numberOfNgOnInit += countOccurrences(module._source.source(), 'ngOnInit', true);
// Count the number of `Component({` strings (case sensitive), which happens in __decorate().
// This does not include View Engine AOT compilation, we use the ngfactory for it.
this._stats.numberOfComponents += countOccurrences(module._source.source(), 'Component({');
// For Ivy we just count ɵcmp.
this._stats.numberOfComponents += countOccurrences(module._source.source(), '.ɵcmp', true);
// for ascii_only true
this._stats.numberOfComponents += countOccurrences(module._source.source(), '.\u0275cmp', true);
}
}
_checkNgFactoryNormalModule(module) {
if (module._source) {
// PLEASE REMEMBER:
// We're dealing with ES5 _or_ ES2015 JavaScript at this point (we don't know for sure).
// Count the number of `.ɵccf(` strings (case sensitive). They're calls to components
// factories.
this._stats.numberOfComponents += countOccurrences(module._source.source(), '.ɵccf(');
// for ascii_only true
this._stats.numberOfComponents += countOccurrences(module._source.source(), '.\u0275ccf(');
}
}
_collectErrors(stats) {
if (stats.hasErrors()) {
for (const errObject of stats.compilation.errors) {
if (errObject instanceof Error) {
const allErrors = errObject.message.match(webpackAllErrorMessageRe);
for (const err of [...allErrors || []].slice(1)) {
const message = (err.match(webpackTsErrorMessageRe) || [])[1];
if (message) {
// At this point this should be a TS1234.
this._stats.errors.push(message);
}
}
}
}
}
}
// We can safely disable no any here since we know the format of the JSON output from webpack.
// tslint:disable-next-line:no-any
_collectBundleStats(json) {
json.chunks
.filter((chunk) => chunk.rendered)
.forEach((chunk) => {
const asset = json.assets.find((x) => x.name == chunk.files[0]);
const size = asset ? asset.size : 0;
if (chunk.entry || chunk.initial) {
this._stats.initialChunkSize += size;
}
else {
this._stats.lazyChunkCount++;
this._stats.lazyChunkSize += size;
}
this._stats.totalChunkCount++;
this._stats.totalChunkSize += size;
});
json.assets
// Filter out chunks. We only count assets that are not JS.
.filter((a) => {
return json.chunks.every((chunk) => chunk.files[0] != a.name);
})
.forEach((a) => {
this._stats.assetSize += (a.size || 0);
this._stats.assetCount++;
});
for (const asset of json.assets) {
if (asset.name == 'polyfill') {
this._stats.polyfillSize += asset.size || 0;
}
}
for (const chunk of json.chunks) {
if (chunk.files[0] && chunk.files[0].endsWith('.css')) {
this._stats.cssSize += chunk.size || 0;
}
}
}
/************************************************************************************************
* The next section is all the different Webpack hooks for this plugin.
*/
/**
* Reports a succeed module.
* @private
*/
_succeedModule(mod) {
// Only report NormalModule instances.
if (mod.constructor !== NormalModule) {
return;
}
const module = mod;
// Only reports modules that are part of the user's project. We also don't do node_modules.
// There is a chance that someone name a file path `hello_node_modules` or something and we
// will ignore that file for the purpose of gathering, but we're willing to take the risk.
if (!module.resource
|| !module.resource.startsWith(this._projectRoot)
|| module.resource.indexOf('node_modules') >= 0) {
return;
}
// Check that it's a source file from the project.
if (module.resource.endsWith('.ts')) {
this._checkTsNormalModule(module);
}
else if (module.resource.endsWith('.ngfactory.js')) {
this._checkNgFactoryNormalModule(module);
}
}
_compilation(compiler, compilation) {
this._reset();
compilation.hooks.succeedModule.tap('NgBuildAnalyticsPlugin', this._succeedModule.bind(this));
}
_done(stats) {
this._collectErrors(stats);
this._collectBundleStats(stats.toJson());
if (this._built) {
this._reportRebuildMetrics(stats);
}
else {
this._reportBuildMetrics(stats);
this._built = true;
}
}
apply(compiler) {
compiler.hooks.compilation.tap('NgBuildAnalyticsPlugin', this._compilation.bind(this, compiler));
compiler.hooks.done.tap('NgBuildAnalyticsPlugin', this._done.bind(this));
}
}
exports.NgBuildAnalyticsPlugin = NgBuildAnalyticsPlugin;