@bundle-stats/utils 4.4.0-beta.0 → 4.4.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/lib/config/component-links.js +5 -1
  2. package/lib/config/component-links.js.map +1 -1
  3. package/lib/config/file-types.js +3 -3
  4. package/lib/config/file-types.js.map +1 -1
  5. package/lib/config/index.js +0 -1
  6. package/lib/config/index.js.map +1 -1
  7. package/lib/config/metrics.js.map +1 -1
  8. package/lib/constants.js +11 -5
  9. package/lib/constants.js.map +1 -1
  10. package/lib/i18n.js +5 -0
  11. package/lib/i18n.js.map +1 -1
  12. package/lib/utils/component-links.js +85 -9
  13. package/lib/utils/component-links.js.map +1 -1
  14. package/lib/utils/index.js +1 -0
  15. package/lib/utils/index.js.map +1 -1
  16. package/lib/utils/insights.js +54 -0
  17. package/lib/utils/insights.js.map +1 -0
  18. package/lib/webpack/extract/assets-size-total-insight.js +2 -2
  19. package/lib/webpack/extract/assets-size-total-insight.js.map +1 -1
  20. package/lib/webpack/extract/index.js +2 -0
  21. package/lib/webpack/extract/index.js.map +1 -1
  22. package/lib/webpack/extract/modules-packages-duplicate.js +28 -16
  23. package/lib/webpack/extract/modules-packages-duplicate.js.map +1 -1
  24. package/lib/webpack/extract/modules-packages-new-insight.js +57 -0
  25. package/lib/webpack/extract/modules-packages-new-insight.js.map +1 -0
  26. package/lib/webpack/extract/modules.js +45 -22
  27. package/lib/webpack/extract/modules.js.map +1 -1
  28. package/lib/webpack/metrics.js +5 -5
  29. package/lib/webpack/metrics.js.map +1 -1
  30. package/lib/webpack/types.js +1 -1
  31. package/lib/webpack/types.js.map +1 -1
  32. package/lib-esm/config/component-links.js +4 -0
  33. package/lib-esm/config/component-links.js.map +1 -1
  34. package/lib-esm/config/file-types.js +3 -3
  35. package/lib-esm/config/file-types.js.map +1 -1
  36. package/lib-esm/config/index.js +0 -1
  37. package/lib-esm/config/index.js.map +1 -1
  38. package/lib-esm/config/metrics.js +1 -1
  39. package/lib-esm/config/metrics.js.map +1 -1
  40. package/lib-esm/constants.js +6 -0
  41. package/lib-esm/constants.js.map +1 -1
  42. package/lib-esm/i18n.js +5 -0
  43. package/lib-esm/i18n.js.map +1 -1
  44. package/lib-esm/utils/component-links.js +79 -7
  45. package/lib-esm/utils/component-links.js.map +1 -1
  46. package/lib-esm/utils/index.js +1 -0
  47. package/lib-esm/utils/index.js.map +1 -1
  48. package/lib-esm/utils/insights.js +47 -0
  49. package/lib-esm/utils/insights.js.map +1 -0
  50. package/lib-esm/webpack/extract/assets-size-total-insight.js +1 -1
  51. package/lib-esm/webpack/extract/assets-size-total-insight.js.map +1 -1
  52. package/lib-esm/webpack/extract/index.js +2 -0
  53. package/lib-esm/webpack/extract/index.js.map +1 -1
  54. package/lib-esm/webpack/extract/modules-packages-duplicate.js +22 -10
  55. package/lib-esm/webpack/extract/modules-packages-duplicate.js.map +1 -1
  56. package/lib-esm/webpack/extract/modules-packages-new-insight.js +54 -0
  57. package/lib-esm/webpack/extract/modules-packages-new-insight.js.map +1 -0
  58. package/lib-esm/webpack/extract/modules.js +45 -23
  59. package/lib-esm/webpack/extract/modules.js.map +1 -1
  60. package/lib-esm/webpack/metrics.js +5 -5
  61. package/lib-esm/webpack/metrics.js.map +1 -1
  62. package/package.json +13 -13
  63. package/src/config/component-links.js +5 -0
  64. package/src/config/file-types.js +3 -3
  65. package/src/config/index.js +0 -1
  66. package/src/config/metrics.js +2 -6
  67. package/src/constants.ts +22 -10
  68. package/src/i18n.js +5 -0
  69. package/src/utils/component-links.ts +96 -10
  70. package/src/utils/index.js +1 -0
  71. package/src/utils/insights.ts +82 -0
  72. package/src/webpack/extract/assets-size-total-insight.js +1 -1
  73. package/src/webpack/extract/index.js +2 -0
  74. package/src/webpack/extract/modules-packages-duplicate.ts +32 -17
  75. package/src/webpack/extract/modules-packages-new-insight.ts +80 -0
  76. package/src/webpack/extract/modules.ts +60 -33
  77. package/src/webpack/metrics.js +5 -5
  78. package/src/webpack/types.ts +1 -2
  79. package/types/browsertime/metrics.d.ts +22 -22
  80. package/types/config/component-links.d.ts +24 -18
  81. package/types/config/file-types.d.ts +17 -17
  82. package/types/config/index.d.ts +0 -1
  83. package/types/config/metrics.d.ts +1 -30
  84. package/types/constants.d.ts +21 -9
  85. package/types/i18n.d.ts +17 -12
  86. package/types/lighthouse/metrics.d.ts +16 -16
  87. package/types/utils/component-links.d.ts +8 -1
  88. package/types/utils/index.d.ts +1 -0
  89. package/types/utils/insights.d.ts +9 -0
  90. package/types/webpack/constants.d.ts +1 -1
  91. package/types/webpack/extract/assets-size-total-insight.d.ts +2 -2
  92. package/types/webpack/extract/modules-packages-duplicate.d.ts +3 -3
  93. package/types/webpack/extract/modules-packages-new-insight.d.ts +8 -0
  94. package/types/webpack/metrics.d.ts +51 -51
  95. package/types/webpack/types.d.ts +1 -2
  96. package/lib/config/insights.js +0 -10
  97. package/lib/config/insights.js.map +0 -1
  98. package/lib-esm/config/insights.js +0 -7
  99. package/lib-esm/config/insights.js.map +0 -1
  100. package/src/config/insights.ts +0 -5
  101. package/tsconfig.lib.json +0 -7
  102. package/types/config/insights.d.ts +0 -5
@@ -16,6 +16,7 @@ import {
16
16
  MODULE_FILE_TYPE,
17
17
  MODULE_FILTERS,
18
18
  MODULE_SOURCE_TYPE,
19
+ MODULE_SOURCE_TYPE_LABELS,
19
20
  PACKAGE_FILTERS,
20
21
  SECTIONS,
21
22
  } from '../config/component-links';
@@ -25,6 +26,7 @@ export type ComponentLinkFilters = Record<string, boolean>;
25
26
 
26
27
  export interface ComponentLinkParams {
27
28
  search?: string;
29
+ entryId?: string;
28
30
  filters: ComponentLinkFilters;
29
31
  }
30
32
 
@@ -159,6 +161,51 @@ export const BUNDLE_ASSETS_CHUNK_COUNT: ComponentLink = {
159
161
  },
160
162
  };
161
163
 
164
+ export const getBundleAsset = (entryId: string): ComponentLink => ({
165
+ section: SECTIONS.ASSETS,
166
+ title: 'View asset details' as any,
167
+ params: {
168
+ [COMPONENT.BUNDLE_ASSETS]: {
169
+ entryId,
170
+ filters: {},
171
+ },
172
+ },
173
+ });
174
+
175
+ export const getBundleAssetsByEntryType = (
176
+ assetType: 'entry' | 'initial' | 'chunk',
177
+ ): ComponentLink => ({
178
+ section: SECTIONS.ASSETS,
179
+ title: I18N.COMPONENT_LINK_BUNDLE_ASSETS_BY_ENTRY_TYPE(assetType),
180
+ params: {
181
+ [COMPONENT.BUNDLE_ASSETS]: {
182
+ filters: {
183
+ [ASSET_FILTERS.CHANGED]: false,
184
+ ...getAssetEntryTypeFilters(false),
185
+ [`${ASSET_ENTRY_TYPE}.${ASSET_FILTERS.ENTRY}`]: assetType === 'entry',
186
+ [`${ASSET_ENTRY_TYPE}.${ASSET_FILTERS.INITIAL}`]: assetType === 'initial',
187
+ [`${ASSET_ENTRY_TYPE}.${ASSET_FILTERS.CHUNK}`]: assetType === 'chunk',
188
+ },
189
+ },
190
+ },
191
+ });
192
+
193
+ export const getBundleAssetsFileTypeComponentLink = (
194
+ fileType: string,
195
+ label: string = fileType,
196
+ ): ComponentLink => ({
197
+ section: SECTIONS.ASSETS,
198
+ title: I18N.COMPONENT_LINK_BUNDLE_ASSETS_BY_FILE_TYPE({ label }),
199
+ params: {
200
+ [COMPONENT.BUNDLE_ASSETS]: {
201
+ filters: {
202
+ ...getAssetFileTypeFilters(false),
203
+ [`${ASSET_FILE_TYPE}.${fileType}`]: true,
204
+ },
205
+ },
206
+ },
207
+ });
208
+
162
209
  export const BUNDLE_MODULES: ComponentLink = {
163
210
  section: SECTIONS.MODULES,
164
211
  title: I18N.COMPONENT_LINK_MODULES,
@@ -221,6 +268,35 @@ export const getBundleModulesByChunk = (
221
268
  },
222
269
  });
223
270
 
271
+ export const getBundleModulesByFileTpe = (
272
+ fileType: string,
273
+ fileTypeLabel?: string,
274
+ ): ComponentLink => ({
275
+ section: SECTIONS.MODULES,
276
+ title: I18N.COMPONENT_LINK_MODULES_BY_FILE_TYPE(fileTypeLabel || fileType),
277
+ params: {
278
+ [COMPONENT.BUNDLE_MODULES]: {
279
+ filters: {
280
+ ...getModuleFileTypeFilters(false),
281
+ [`${MODULE_FILE_TYPE}.${fileType}`]: true,
282
+ },
283
+ },
284
+ },
285
+ });
286
+
287
+ export const getBundleModulesBySource = (thirdParty: boolean, label = ''): ComponentLink => ({
288
+ section: SECTIONS.MODULES,
289
+ title: I18N.COMPONENT_LINK_MODULES_BY_SOURCE(label),
290
+ params: {
291
+ [COMPONENT.BUNDLE_MODULES]: {
292
+ filters: {
293
+ [`${MODULE_SOURCE_TYPE}.${MODULE_FILTERS.THIRD_PARTY}`]: thirdParty,
294
+ [`${MODULE_SOURCE_TYPE}.${MODULE_FILTERS.FIRST_PARTY}`]: !thirdParty,
295
+ },
296
+ },
297
+ },
298
+ });
299
+
224
300
  export const BUNDLE_PACKAGES_COUNT: ComponentLink = {
225
301
  section: SECTIONS.PACKAGES,
226
302
  title: I18N.COMPONENT_LINK_PACKAGES_COUNT,
@@ -234,6 +310,19 @@ export const BUNDLE_PACKAGES_COUNT: ComponentLink = {
234
310
  },
235
311
  };
236
312
 
313
+ export const BUNDLE_PACKAGES_CHANGED: ComponentLink = {
314
+ section: SECTIONS.PACKAGES,
315
+ title: I18N.COMPONENT_LINK_PACKAGES_CHANGED,
316
+ params: {
317
+ [COMPONENT.BUNDLE_PACKAGES]: {
318
+ filters: {
319
+ // Keep a filter to allow the merge and skip default
320
+ [PACKAGE_FILTERS.CHANGED]: true,
321
+ },
322
+ },
323
+ },
324
+ };
325
+
237
326
  export const BUNDLE_PACKAGES_DUPLICATE: ComponentLink = {
238
327
  section: SECTIONS.PACKAGES,
239
328
  title: I18N.COMPONENT_LINK_PACKAGES_DUPLICATE,
@@ -246,21 +335,18 @@ export const BUNDLE_PACKAGES_DUPLICATE: ComponentLink = {
246
335
  },
247
336
  };
248
337
 
249
- export const getBundleAssetsFileTypeComponentLink = (
250
- fileType: string,
251
- label: string,
252
- ): ComponentLink => ({
253
- section: SECTIONS.ASSETS,
254
- title: I18N.COMPONENT_LINK_BUNDLE_ASSETS_BY_FILE_TYPE({ label }),
338
+ export const BUNDLE_PACKAGES_DUPLICATE_CHANGED: ComponentLink = {
339
+ section: SECTIONS.PACKAGES,
340
+ title: I18N.COMPONENT_LINK_PACKAGES_DUPLICATE_CHANGED,
255
341
  params: {
256
- [COMPONENT.BUNDLE_ASSETS]: {
342
+ [COMPONENT.BUNDLE_PACKAGES]: {
257
343
  filters: {
258
- ...getAssetFileTypeFilters(false),
259
- [`${ASSET_FILE_TYPE}.${fileType}`]: true,
344
+ [PACKAGE_FILTERS.CHANGED]: true,
345
+ [PACKAGE_FILTERS.DUPLICATE]: true,
260
346
  },
261
347
  },
262
348
  },
263
- });
349
+ };
264
350
 
265
351
  export const getBundlePackagesByNameComponentLink = (search: string): ComponentLink => ({
266
352
  section: SECTIONS.PACKAGES,
@@ -1,6 +1,7 @@
1
1
  export * from './component-links';
2
2
  export * from './delta';
3
3
  export * from './date';
4
+ export * from './insights';
4
5
  export * from './file-types';
5
6
  export * from './format';
6
7
  export * from './metrics';
@@ -0,0 +1,82 @@
1
+ import isEmpty from 'lodash/isEmpty';
2
+
3
+ import { InsightType, JobInsight, JobInsightsInfo } from '../constants';
4
+ import {
5
+ BUNDLE_PACKAGES_DUPLICATE,
6
+ BUNDLE_PACKAGES_CHANGED,
7
+ BUNDLE_PACKAGES_DUPLICATE_CHANGED,
8
+ } from './component-links';
9
+
10
+ interface InsightListItem {
11
+ /**
12
+ * Inight name
13
+ */
14
+ name: string;
15
+ /**
16
+ * Insight data
17
+ */
18
+ insight: JobInsight;
19
+ /**
20
+ * Custom link props
21
+ */
22
+ link: any;
23
+ }
24
+
25
+ /**
26
+ * Get the insigh list ordered by level type
27
+ */
28
+ export const getInsightList = (insights: JobInsightsInfo): Array<InsightListItem> => {
29
+ const { duplicatePackages, newPackages } = insights;
30
+
31
+ const insightsByLevel: Record<InsightType, Array<InsightListItem>> = {
32
+ [InsightType.ERROR]: [],
33
+ [InsightType.WARNING]: [],
34
+ [InsightType.INFO]: [],
35
+ };
36
+
37
+ if (duplicatePackages) {
38
+ insightsByLevel[duplicatePackages.type].push({
39
+ name: 'duplicatePackages',
40
+ insight: duplicatePackages,
41
+ link:
42
+ duplicatePackages.type === InsightType.WARNING
43
+ ? BUNDLE_PACKAGES_DUPLICATE
44
+ : BUNDLE_PACKAGES_DUPLICATE_CHANGED,
45
+ });
46
+ }
47
+
48
+ if (newPackages) {
49
+ insightsByLevel[newPackages.type].push({
50
+ name: 'newPackages',
51
+ insight: newPackages,
52
+ link: BUNDLE_PACKAGES_CHANGED,
53
+ });
54
+ }
55
+
56
+ return [
57
+ ...(insightsByLevel[InsightType.ERROR] || []),
58
+ ...(insightsByLevel[InsightType.WARNING] || []),
59
+ ...(insightsByLevel[InsightType.INFO] || []),
60
+ ];
61
+ };
62
+
63
+ /**
64
+ * Show only the insights that are introduced by a change
65
+ */
66
+ export const getChangedInsights = (normalizedInsights: JobInsightsInfo): JobInsightsInfo | null => {
67
+ const res: JobInsightsInfo = {};
68
+
69
+ if (normalizedInsights.duplicatePackages?.type === InsightType.ERROR) {
70
+ res.duplicatePackages = normalizedInsights.duplicatePackages;
71
+ }
72
+
73
+ if (normalizedInsights.newPackages) {
74
+ res.newPackages = normalizedInsights.newPackages;
75
+ }
76
+
77
+ if (isEmpty(res)) {
78
+ return null;
79
+ }
80
+
81
+ return res;
82
+ };
@@ -1,4 +1,4 @@
1
- import { InsightType } from '../../config/insights';
1
+ import { InsightType } from '../../constants';
2
2
  import { getMetricRunInfo } from '../../utils/metrics';
3
3
  import { getMetricType } from '../utils';
4
4
  import { Metric } from '../types';
@@ -11,6 +11,7 @@ import { extractModules } from './modules';
11
11
  import { extractModulesPackages } from './modules-packages';
12
12
  import { extractModulesPackagesCount } from './modules-packages-count';
13
13
  import { extractModulesPackagesDuplicate } from './modules-packages-duplicate';
14
+ import { extractModulesPackagesNewInsight } from './modules-packages-new-insight';
14
15
 
15
16
  const extractFns = [
16
17
  extractAssets,
@@ -24,6 +25,7 @@ const extractFns = [
24
25
  extractModulesPackages,
25
26
  extractModulesPackagesCount,
26
27
  extractModulesPackagesDuplicate,
28
+ extractModulesPackagesNewInsight,
27
29
  ];
28
30
 
29
31
  export const extract = (webpackStats, baseline) =>
@@ -1,9 +1,10 @@
1
+ import isEmpty from 'lodash/isEmpty';
1
2
  import map from 'lodash/map';
2
3
  import orderBy from 'lodash/orderBy';
3
4
  import sum from 'lodash/sum';
4
5
 
5
- import { InsightType } from '../../config';
6
6
  import {
7
+ InsightType,
7
8
  Job,
8
9
  JobInsight,
9
10
  JobInsightDuplicatePackagesData,
@@ -25,9 +26,9 @@ interface DuplicatePackageGroup {
25
26
  }
26
27
 
27
28
  interface ModulesPackagesDuplicateData {
28
- insights: {
29
+ insights?: {
29
30
  duplicatePackages?: JobInsights['webpack']['duplicatePackages'];
30
- duplicatePackagesV3: JobInsights['webpack']['duplicatePackagesV3'];
31
+ duplicatePackagesV3?: JobInsights['webpack']['duplicatePackagesV3'];
31
32
  };
32
33
  metrics: {
33
34
  duplicatePackagesCount: MetricRun;
@@ -37,7 +38,7 @@ interface ModulesPackagesDuplicateData {
37
38
  export const getDuplicatePackagesInsight = (
38
39
  duplicatePackagesMap: Record<string, Array<string>>,
39
40
  baselineDuplicatePackagesMap?: Record<string, Array<string>>,
40
- ): JobInsight<JobInsightDuplicatePackagesV3Data> => {
41
+ ): JobInsight<JobInsightDuplicatePackagesV3Data> | null => {
41
42
  const duplicateInstances: Array<string> = [];
42
43
  const baselineDuplicateInstances: Array<string> = [];
43
44
  const newDuplicateInstances: Array<string> = [];
@@ -81,8 +82,8 @@ export const getDuplicatePackagesInsight = (
81
82
  const newDuplicateInstancesCount = newDuplicateInstances.length;
82
83
  const removedDuplicateInstancesCount = removedDuplicateInstances.length;
83
84
 
84
- let text: string;
85
- let insightType: InsightType;
85
+ let text = '';
86
+ let insightType: InsightType | null = null;
86
87
 
87
88
  if (newDuplicateInstancesCount > 0 && removedDuplicateInstancesCount > 0) {
88
89
  // New duplicate packages and removed duplicates
@@ -90,7 +91,7 @@ export const getDuplicatePackagesInsight = (
90
91
  newDuplicateInstancesCount > 1 || removedDuplicateInstancesCount > 1 ? 'packages' : 'package';
91
92
  text = `Bundle introduced ${newDuplicateInstancesCount} and removed ${removedDuplicateInstancesCount} duplicate ${item}`;
92
93
  insightType = InsightType.ERROR;
93
- } else if (newDuplicateInstancesCount > 0 && baselineDuplicatePackagesMap) {
94
+ } else if (newDuplicateInstancesCount > 0 && !isEmpty(baselineDuplicatePackagesMap)) {
94
95
  // New duplicate packages and baseline comparison
95
96
  const item = newDuplicateInstancesCount > 1 ? 'packages' : 'package';
96
97
  text = `Bundle introduced ${newDuplicateInstancesCount} duplicate ${item}`;
@@ -100,6 +101,12 @@ export const getDuplicatePackagesInsight = (
100
101
  const item = newDuplicateInstancesCount > 1 ? 'packages' : 'package';
101
102
  text = `Bundle contains ${newDuplicateInstancesCount} duplicate ${item}`;
102
103
  insightType = InsightType.WARNING;
104
+ } else if (removedDuplicateInstancesCount > 0 && duplicateInstancesCount > 0) {
105
+ // Removed duplicate packages, but there are duplicate packages remaining
106
+ const itemRemoved = removedDuplicateInstancesCount > 1 ? 'packages' : 'package';
107
+ const itemRemaining = duplicateInstancesCount > 1 ? 'packages' : 'package';
108
+ text = `Bundle removed ${removedDuplicateInstancesCount} duplicate ${itemRemoved}, ${duplicateInstancesCount} duplicate ${itemRemaining} remaining`;
109
+ insightType = InsightType.WARNING;
103
110
  } else if (removedDuplicateInstancesCount > 0) {
104
111
  // Removed duplicate packages
105
112
  const item = removedDuplicateInstancesCount > 1 ? 'packages' : 'package';
@@ -115,9 +122,11 @@ export const getDuplicatePackagesInsight = (
115
122
  text = `Bundle contains ${duplicateInstancesCount} duplicate ${item}`;
116
123
  insightType = InsightType.WARNING;
117
124
  } else {
118
- // No change
119
- text = 'Bundle does not contain duplicate packages';
120
- insightType = InsightType.INFO;
125
+ // No change - noop
126
+ }
127
+
128
+ if (!text || !insightType) {
129
+ return null;
121
130
  }
122
131
 
123
132
  return {
@@ -180,11 +189,15 @@ export const extractModulesPackagesDuplicate = (
180
189
  baselineJob?.insights?.webpack?.duplicatePackages?.data ||
181
190
  {};
182
191
 
192
+ const noDuplicatesDuplicatePackagesV3 = getDuplicatePackagesInsight({}, baselineDuplicatePackages);
193
+
183
194
  if (!count) {
184
195
  return {
185
- insights: {
186
- duplicatePackagesV3: getDuplicatePackagesInsight({}, baselineDuplicatePackages),
187
- },
196
+ ...(noDuplicatesDuplicatePackagesV3 && {
197
+ insights: {
198
+ duplicatePackagesV3: noDuplicatesDuplicatePackagesV3,
199
+ },
200
+ }),
188
201
  metrics: {
189
202
  duplicatePackagesCount: {
190
203
  value: count,
@@ -221,16 +234,18 @@ export const extractModulesPackagesDuplicate = (
221
234
  duplicatePackagesData[packageName] = packageData.children.map(({ id }) => id);
222
235
  });
223
236
 
237
+ const duplicatePackagesV3 = getDuplicatePackagesInsight(
238
+ duplicatePackagesData,
239
+ baselineDuplicatePackages,
240
+ );
241
+
224
242
  return {
225
243
  insights: {
226
244
  duplicatePackages: {
227
245
  type: InsightType.WARNING,
228
246
  data: duplicatePackagesData,
229
247
  },
230
- duplicatePackagesV3: getDuplicatePackagesInsight(
231
- duplicatePackagesData,
232
- baselineDuplicatePackages,
233
- ),
248
+ ...(duplicatePackagesV3 && { duplicatePackagesV3 }),
234
249
  },
235
250
  metrics: {
236
251
  duplicatePackagesCount: {
@@ -0,0 +1,80 @@
1
+ import { InsightType, Job, JobInsights } from '../../constants';
2
+ import { Packages } from '../types';
3
+
4
+ interface ModulesPackagesNewData {
5
+ insights?: {
6
+ newPackages: JobInsights['webpack']['newPackages'];
7
+ };
8
+ }
9
+
10
+ const getPackageNames = (packages: Packages): Set<string> => {
11
+ const result: Set<string> = new Set();
12
+
13
+ Object.values(packages).forEach((metricPackage) => {
14
+ result.add(metricPackage.name);
15
+ });
16
+
17
+ return result;
18
+ };
19
+
20
+ const getText = (newPackages: Array<string>) => {
21
+ if (newPackages.length === 1) {
22
+ return `Bundle introduced one new package: ${newPackages[0]}`;
23
+ }
24
+
25
+ const firstNewPackages = newPackages.slice(0, 3);
26
+ const otherPackages = newPackages.slice(3);
27
+
28
+ const text = `Bundle introduced ${newPackages.length} new packages: ${firstNewPackages.join(', ')}`;
29
+
30
+ if (otherPackages.length === 0) {
31
+ return text;
32
+ }
33
+
34
+ if (otherPackages.length === 1) {
35
+ return `${text} and one more`;
36
+ }
37
+
38
+ return `${text} and ${otherPackages.length} more`;
39
+ };
40
+
41
+ export const extractModulesPackagesNewInsight = (
42
+ _: any,
43
+ currentExtractedData: any,
44
+ baselineJob?: Job,
45
+ ): ModulesPackagesNewData | null => {
46
+ const currentMetricPackages = currentExtractedData?.metrics?.packages as Packages;
47
+ const baselineMetricPackages = baselineJob?.metrics?.webpack?.packages as Packages;
48
+
49
+ // Skip insight when the baseline data is missing
50
+ if (!baselineMetricPackages) {
51
+ return null;
52
+ }
53
+
54
+ const currentPackages = getPackageNames(currentMetricPackages);
55
+ const baselinePackages = getPackageNames(baselineMetricPackages);
56
+
57
+ const newPackages: Array<string> = [];
58
+
59
+ currentPackages.forEach((packageName) => {
60
+ if (!baselinePackages.has(packageName)) {
61
+ newPackages.push(packageName);
62
+ }
63
+ });
64
+
65
+ if (newPackages.length === 0) {
66
+ return null;
67
+ }
68
+
69
+ return {
70
+ insights: {
71
+ newPackages: {
72
+ type: InsightType.WARNING,
73
+ data: {
74
+ text: getText(newPackages),
75
+ packages: newPackages,
76
+ },
77
+ },
78
+ },
79
+ };
80
+ };
@@ -1,11 +1,12 @@
1
1
  import round from 'lodash/round';
2
- import { WebpackStatsFiltered, WebpackStatsFilteredRootModule } from '@bundle-stats/plugin-webpack-filter';
2
+ import union from 'lodash/union';
3
+ import { WebpackStatsFiltered, WebpackStatsFilteredModule } from '@bundle-stats/plugin-webpack-filter';
3
4
 
4
5
  import { Module, MetricsModules } from '../types';
5
6
  import { getModuleName, normalizeChunkId } from '../utils';
6
7
 
7
8
  /*
8
- * Extract webpack modules array to an object with metrics
9
+ * Extract webpack modules metrics and stats
9
10
  */
10
11
  export const extractModules = (webpackStats?: WebpackStatsFiltered): MetricsModules => {
11
12
  const modulesSource = webpackStats?.modules;
@@ -22,60 +23,86 @@ export const extractModules = (webpackStats?: WebpackStatsFiltered): MetricsModu
22
23
  }
23
24
 
24
25
  // Flatten concatenated modules
25
- const allModules = modulesSource.reduce((agg, moduleEntry) => {
26
- if (!moduleEntry.modules) {
27
- agg.push(moduleEntry);
26
+ const modulesByName: Map<string, WebpackStatsFilteredModule> = new Map();
28
27
 
29
- return agg;
28
+ modulesSource.forEach((rootModule) => {
29
+ // skip modules that do not belong to any chunk
30
+ if (!rootModule.chunks || rootModule.chunks.length === 0) {
31
+ return;
30
32
  }
31
33
 
32
- // eslint-disable-next-line no-param-reassign
33
- agg = agg.concat(
34
- moduleEntry.modules.map((concatenatedModule) => ({
35
- ...concatenatedModule,
36
- // Add parent chunks
37
- chunks: moduleEntry.chunks,
38
- })),
39
- );
34
+ // flatten concatenated modules
35
+ if (rootModule.modules) {
36
+ rootModule.modules.forEach((concatenatedModule) => {
37
+ const concatenatedModuleName = getModuleName(concatenatedModule.name);
38
+ const existingModule = modulesByName.get(concatenatedModuleName);
39
+
40
+ // Merge if the module is already available
41
+ // webpack adds a module entry for every concatenated module
42
+ if (existingModule) {
43
+ modulesByName.set(concatenatedModuleName, {
44
+ ...existingModule,
45
+ chunks: union(existingModule.chunks, rootModule.chunks),
46
+ });
47
+ } else {
48
+ modulesByName.set(concatenatedModuleName, {
49
+ ...concatenatedModule,
50
+ chunks: rootModule.chunks,
51
+ });
52
+ }
53
+ });
54
+
55
+ return;
56
+ }
57
+
58
+ const normalizedName = getModuleName(rootModule.name);
59
+ const savedRootModule = modulesByName.get(normalizedName);
60
+
61
+ // Merge module data
62
+ // @NOTE some webpack plugins (mini-css-extract-plugin) might add a root module entry
63
+ // even if thee module was already concantenated in another module
64
+ if (savedRootModule) {
65
+ modulesByName.set(normalizedName, {
66
+ ...savedRootModule,
67
+ chunks: union(savedRootModule.chunks, rootModule.chunks),
68
+ size: rootModule.size,
69
+ });
70
+
71
+ return;
72
+ }
40
73
 
41
- return agg;
42
- }, [] as Array<WebpackStatsFilteredRootModule>);
74
+ modulesByName.set(normalizedName, rootModule);
75
+ });
76
+
77
+ // Normalize module entries and extract metrics
78
+ const modules: Record<string, Module> = {};
43
79
 
44
80
  let moduleCount = 0;
45
81
  let totalCodeSize = 0;
46
82
  let duplicateCodeSize = 0;
47
83
  let duplicateModulesCount = 0;
48
84
 
49
- // Normalize module entries
50
- const modules = allModules.reduce((agg, moduleEntry) => {
85
+ modulesByName.forEach((moduleEntry, normalizedName) => {
51
86
  const { name, size = 0, chunks } = moduleEntry;
52
- const normalizedName = getModuleName(name);
53
-
54
- // skip modules that are orphane(do not belong to any chunk)
55
- if (!chunks || chunks?.length === 0) {
56
- return agg;
57
- }
58
87
 
59
88
  const instances = chunks.length;
60
89
  const duplicateInstances = instances - 1;
90
+ const duplicated = duplicateInstances > 0;
61
91
 
62
92
  moduleCount += instances;
63
- duplicateModulesCount += duplicateInstances;
64
- duplicateCodeSize += duplicateInstances * size;
65
93
  totalCodeSize += instances * size;
66
94
 
67
- const reasons = moduleEntry.reasons?.map((reason) => getModuleName(reason.module));
95
+ if (duplicated) {
96
+ duplicateModulesCount += duplicateInstances;
97
+ duplicateCodeSize += duplicateInstances * size;
98
+ }
68
99
 
69
- // eslint-disable-next-line no-param-reassign
70
- agg[normalizedName] = {
100
+ modules[normalizedName] = {
71
101
  name,
72
102
  value: size,
73
103
  chunkIds: chunks.map(normalizeChunkId),
74
- duplicated: Boolean(duplicateInstances),
75
- ...(reasons && { reasons }),
104
+ ...(duplicated && { duplicated }),
76
105
  };
77
-
78
- return agg;
79
106
  }, {} as Record<string, Module>);
80
107
 
81
108
  const duplicateCode = totalCodeSize ? round((duplicateCodeSize / totalCodeSize) * 100, 2) : 0;
@@ -9,7 +9,7 @@ import { Metric } from './types';
9
9
  export const metrics = {
10
10
  [Metric.BUNDLE_SIZE]: {
11
11
  label: 'Bundle Size',
12
- description: 'The total size of the assets produced by webpack',
12
+ description: 'The total size of the assets processed by the bundler',
13
13
  url: 'https://relative-ci.com/documentation/metrics-and-data#assets',
14
14
  type: METRIC_TYPE_FILE_SIZE,
15
15
  },
@@ -48,14 +48,14 @@ export const metrics = {
48
48
  },
49
49
  [Metric.MODULE_COUNT]: {
50
50
  label: 'Modules',
51
- description: 'The total number of modules bundled by webpack',
51
+ description: 'The total number of bundled modules',
52
52
  url: 'https://relative-ci.com/documentation/metrics-and-data#modules-1',
53
53
  type: METRIC_TYPE_NUMERIC,
54
54
  biggerIsBetter: false,
55
55
  },
56
56
  [Metric.DUPLICATE_MODULES_COUNT]: {
57
57
  label: 'Duplicate Modules',
58
- description: 'The total number of modules that belong to more than one chunk',
58
+ description: 'The total number of module instances bundled into multiple chunks',
59
59
  url: 'https://relative-ci.com/documentation/metrics-and-data#modules-1',
60
60
  type: METRIC_TYPE_NUMERIC,
61
61
  biggerIsBetter: false,
@@ -69,14 +69,14 @@ export const metrics = {
69
69
  },
70
70
  [Metric.PACKAGE_COUNT]: {
71
71
  label: 'Packages',
72
- description: 'The total number of packages bundled by webpack',
72
+ description: 'The total number of bundled packages',
73
73
  url: 'https://relative-ci.com/documentation/metrics-and-data#packages',
74
74
  type: METRIC_TYPE_NUMERIC,
75
75
  biggerIsBetter: false,
76
76
  },
77
77
  [Metric.DUPLICATE_PACKAGES_COUNT]: {
78
78
  label: 'Duplicate Packages',
79
- description: 'The total number of duplicate packages',
79
+ description: 'The total number of bundled duplicate package instances',
80
80
  url: 'https://relative-ci.com/documentation/metrics-and-data#packages',
81
81
  type: METRIC_TYPE_NUMERIC,
82
82
  biggerIsBetter: false,
@@ -51,8 +51,7 @@ export interface MetricsAssets {
51
51
  export interface Module extends MetricRun {
52
52
  name: string;
53
53
  chunkIds: Array<string>;
54
- duplicated: boolean;
55
- reasons?: Array<string>;
54
+ duplicated?: boolean;
56
55
  }
57
56
 
58
57
  export type Modules = Record<string, Module>;