mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-19 04:26:01 +08:00
Current we are always sending `0` as the number of components in JIT mode because ` Component({` doesn't match the component source
292 lines
9.9 KiB
TypeScript
292 lines
9.9 KiB
TypeScript
/**
|
|
* @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
|
|
*/
|
|
import { analytics } from '@angular-devkit/core';
|
|
import {
|
|
Compiler,
|
|
Module,
|
|
Stats,
|
|
compilation,
|
|
} from 'webpack';
|
|
import { Source } from 'webpack-sources';
|
|
|
|
const NormalModule = require('webpack/lib/NormalModule');
|
|
|
|
interface NormalModule extends Module {
|
|
_source?: Source | null;
|
|
resource?: string;
|
|
}
|
|
|
|
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
|
|
*/
|
|
export function countOccurrences(source: string, match: string, wordBreak = false): number {
|
|
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;
|
|
}
|
|
|
|
|
|
/**
|
|
* Holder of statistics related to the build.
|
|
*/
|
|
class AnalyticsBuildStats {
|
|
public errors: string[] = [];
|
|
public numberOfNgOnInit = 0;
|
|
public numberOfComponents = 0;
|
|
public initialChunkSize = 0;
|
|
public totalChunkCount = 0;
|
|
public totalChunkSize = 0;
|
|
public lazyChunkCount = 0;
|
|
public lazyChunkSize = 0;
|
|
public assetCount = 0;
|
|
public assetSize = 0;
|
|
public polyfillSize = 0;
|
|
public cssSize = 0;
|
|
}
|
|
|
|
|
|
/**
|
|
* Analytics plugin that reports the analytics we want from the CLI.
|
|
*/
|
|
export class NgBuildAnalyticsPlugin {
|
|
protected _built = false;
|
|
protected _stats = new AnalyticsBuildStats();
|
|
|
|
constructor(
|
|
protected _projectRoot: string,
|
|
protected _analytics: analytics.Analytics,
|
|
protected _category: string,
|
|
private _isIvy: boolean,
|
|
) {
|
|
}
|
|
|
|
protected _reset() {
|
|
this._stats = new AnalyticsBuildStats();
|
|
}
|
|
|
|
protected _getMetrics(stats: Stats) {
|
|
const startTime = +(stats.startTime || 0);
|
|
const endTime = +(stats.endTime || 0);
|
|
const metrics: (string | number)[] = [];
|
|
metrics[analytics.NgCliAnalyticsMetrics.BuildTime] = (endTime - startTime);
|
|
metrics[analytics.NgCliAnalyticsMetrics.NgOnInitCount] = this._stats.numberOfNgOnInit;
|
|
metrics[analytics.NgCliAnalyticsMetrics.NgComponentCount] = this._stats.numberOfComponents;
|
|
metrics[analytics.NgCliAnalyticsMetrics.InitialChunkSize] = this._stats.initialChunkSize;
|
|
metrics[analytics.NgCliAnalyticsMetrics.TotalChunkCount] = this._stats.totalChunkCount;
|
|
metrics[analytics.NgCliAnalyticsMetrics.TotalChunkSize] = this._stats.totalChunkSize;
|
|
metrics[analytics.NgCliAnalyticsMetrics.LazyChunkCount] = this._stats.lazyChunkCount;
|
|
metrics[analytics.NgCliAnalyticsMetrics.LazyChunkSize] = this._stats.lazyChunkSize;
|
|
metrics[analytics.NgCliAnalyticsMetrics.AssetCount] = this._stats.assetCount;
|
|
metrics[analytics.NgCliAnalyticsMetrics.AssetSize] = this._stats.assetSize;
|
|
metrics[analytics.NgCliAnalyticsMetrics.PolyfillSize] = this._stats.polyfillSize;
|
|
metrics[analytics.NgCliAnalyticsMetrics.CssSize] = this._stats.cssSize;
|
|
|
|
return metrics;
|
|
}
|
|
protected _getDimensions(stats: Stats) {
|
|
const dimensions: (string | number | boolean)[] = [];
|
|
|
|
if (this._stats.errors.length) {
|
|
// Adding commas before and after so the regex are easier to define filters.
|
|
dimensions[analytics.NgCliAnalyticsDimensions.BuildErrors] = `,${this._stats.errors.join()},`;
|
|
}
|
|
|
|
dimensions[analytics.NgCliAnalyticsDimensions.NgIvyEnabled] = this._isIvy;
|
|
|
|
return dimensions;
|
|
}
|
|
|
|
protected _reportBuildMetrics(stats: Stats) {
|
|
const dimensions = this._getDimensions(stats);
|
|
const metrics = this._getMetrics(stats);
|
|
this._analytics.event(this._category, 'build', { dimensions, metrics });
|
|
}
|
|
|
|
protected _reportRebuildMetrics(stats: Stats) {
|
|
const dimensions = this._getDimensions(stats);
|
|
const metrics = this._getMetrics(stats);
|
|
this._analytics.event(this._category, 'rebuild', { dimensions, metrics });
|
|
}
|
|
|
|
protected _checkTsNormalModule(module: NormalModule) {
|
|
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);
|
|
}
|
|
}
|
|
|
|
protected _checkNgFactoryNormalModule(module: NormalModule) {
|
|
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(');
|
|
}
|
|
}
|
|
|
|
protected _collectErrors(stats: 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
|
|
protected _collectBundleStats(json: any) {
|
|
json.chunks
|
|
.filter((chunk: { rendered?: boolean }) => chunk.rendered)
|
|
.forEach((chunk: { files: string[], initial?: boolean, entry?: boolean }) => {
|
|
const asset = json.assets.find((x: { name: string }) => 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: { name: string }) => {
|
|
return json.chunks.every((chunk: { files: string[] }) => chunk.files[0] != a.name);
|
|
})
|
|
.forEach((a: { size?: number }) => {
|
|
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
|
|
*/
|
|
protected _succeedModule(mod: Module) {
|
|
// Only report NormalModule instances.
|
|
if (mod.constructor !== NormalModule) {
|
|
return;
|
|
}
|
|
const module = mod as {} as NormalModule;
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
protected _compilation(compiler: Compiler, compilation: compilation.Compilation) {
|
|
this._reset();
|
|
compilation.hooks.succeedModule.tap('NgBuildAnalyticsPlugin', this._succeedModule.bind(this));
|
|
}
|
|
|
|
protected _done(stats: Stats) {
|
|
this._collectErrors(stats);
|
|
this._collectBundleStats(stats.toJson());
|
|
if (this._built) {
|
|
this._reportRebuildMetrics(stats);
|
|
} else {
|
|
this._reportBuildMetrics(stats);
|
|
this._built = true;
|
|
}
|
|
}
|
|
|
|
apply(compiler: Compiler): void {
|
|
compiler.hooks.compilation.tap(
|
|
'NgBuildAnalyticsPlugin',
|
|
this._compilation.bind(this, compiler),
|
|
);
|
|
compiler.hooks.done.tap('NgBuildAnalyticsPlugin', this._done.bind(this));
|
|
}
|
|
}
|