@bundle-stats/utils 4.2.2 → 4.2.3-beta.1

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 (188) hide show
  1. package/README.md +15 -10
  2. package/lib/browsertime/metrics.js +22 -22
  3. package/lib/browsertime/metrics.js.map +1 -1
  4. package/lib/config/component-links.js +7 -1
  5. package/lib/config/component-links.js.map +1 -1
  6. package/lib/config/file-types.js +3 -3
  7. package/lib/config/file-types.js.map +1 -1
  8. package/lib/config/index.js +1 -2
  9. package/lib/config/index.js.map +1 -1
  10. package/lib/config/metric-types.js +28 -0
  11. package/lib/config/metric-types.js.map +1 -0
  12. package/lib/constants.js +23 -22
  13. package/lib/constants.js.map +1 -1
  14. package/lib/i18n.js +15 -10
  15. package/lib/i18n.js.map +1 -1
  16. package/lib/jobs/create-job.js +17 -7
  17. package/lib/jobs/create-job.js.map +1 -1
  18. package/lib/jobs/create-jobs.js.map +1 -1
  19. package/lib/lighthouse/metrics.js +13 -13
  20. package/lib/lighthouse/metrics.js.map +1 -1
  21. package/lib/report/add-row-metric-data.js +4 -4
  22. package/lib/report/add-row-metric-data.js.map +1 -1
  23. package/lib/report/compare-metrics.js +4 -1
  24. package/lib/report/compare-metrics.js.map +1 -1
  25. package/lib/report/create.js +17 -7
  26. package/lib/report/create.js.map +1 -1
  27. package/lib/report/get-metric-changed.js.map +1 -1
  28. package/lib/report/index.js +1 -0
  29. package/lib/report/index.js.map +1 -1
  30. package/lib/report/merge-metrics-by-key.js +1 -1
  31. package/lib/report/merge-metrics-by-key.js.map +1 -1
  32. package/lib/utils/component-links.js +191 -27
  33. package/lib/utils/component-links.js.map +1 -1
  34. package/lib/utils/date.js.map +1 -1
  35. package/lib/utils/delta.js +35 -30
  36. package/lib/utils/delta.js.map +1 -1
  37. package/lib/utils/format.js +5 -6
  38. package/lib/utils/format.js.map +1 -1
  39. package/lib/utils/insights.js.map +1 -1
  40. package/lib/utils/metrics.js +49 -32
  41. package/lib/utils/metrics.js.map +1 -1
  42. package/lib/webpack/compare.js +16 -12
  43. package/lib/webpack/compare.js.map +1 -1
  44. package/lib/webpack/extract/assets.js +7 -1
  45. package/lib/webpack/extract/assets.js.map +1 -1
  46. package/lib/webpack/extract/meta.js.map +1 -1
  47. package/lib/webpack/extract/modules-packages-duplicate.js +13 -8
  48. package/lib/webpack/extract/modules-packages-duplicate.js.map +1 -1
  49. package/lib/webpack/extract/modules-packages-new-insight.js.map +1 -1
  50. package/lib/webpack/extract/modules-packages.js +8 -1
  51. package/lib/webpack/extract/modules-packages.js.map +1 -1
  52. package/lib/webpack/extract/modules.js +45 -20
  53. package/lib/webpack/extract/modules.js.map +1 -1
  54. package/lib/webpack/metrics.js +35 -34
  55. package/lib/webpack/metrics.js.map +1 -1
  56. package/lib/webpack/selectors.js +40 -3
  57. package/lib/webpack/selectors.js.map +1 -1
  58. package/lib/webpack/types.js +1 -1
  59. package/lib/webpack/types.js.map +1 -1
  60. package/lib/webpack/utils.js +37 -25
  61. package/lib/webpack/utils.js.map +1 -1
  62. package/lib-esm/browsertime/metrics.js +22 -22
  63. package/lib-esm/browsertime/metrics.js.map +1 -1
  64. package/lib-esm/config/component-links.js +6 -0
  65. package/lib-esm/config/component-links.js.map +1 -1
  66. package/lib-esm/config/file-types.js +3 -3
  67. package/lib-esm/config/file-types.js.map +1 -1
  68. package/lib-esm/config/index.js +1 -2
  69. package/lib-esm/config/index.js.map +1 -1
  70. package/lib-esm/config/metric-types.js +25 -0
  71. package/lib-esm/config/metric-types.js.map +1 -0
  72. package/lib-esm/constants.js +19 -18
  73. package/lib-esm/constants.js.map +1 -1
  74. package/lib-esm/i18n.js +15 -10
  75. package/lib-esm/i18n.js.map +1 -1
  76. package/lib-esm/jobs/create-job.js.map +1 -1
  77. package/lib-esm/jobs/create-jobs.js.map +1 -1
  78. package/lib-esm/lighthouse/metrics.js +13 -13
  79. package/lib-esm/lighthouse/metrics.js.map +1 -1
  80. package/lib-esm/report/add-row-metric-data.js +4 -4
  81. package/lib-esm/report/add-row-metric-data.js.map +1 -1
  82. package/lib-esm/report/compare-metrics.js +4 -1
  83. package/lib-esm/report/compare-metrics.js.map +1 -1
  84. package/lib-esm/report/create.js.map +1 -1
  85. package/lib-esm/report/get-metric-changed.js.map +1 -1
  86. package/lib-esm/report/index.js +1 -0
  87. package/lib-esm/report/index.js.map +1 -1
  88. package/lib-esm/report/merge-metrics-by-key.js +1 -1
  89. package/lib-esm/report/merge-metrics-by-key.js.map +1 -1
  90. package/lib-esm/utils/component-links.js +185 -27
  91. package/lib-esm/utils/component-links.js.map +1 -1
  92. package/lib-esm/utils/date.js.map +1 -1
  93. package/lib-esm/utils/delta.js +32 -26
  94. package/lib-esm/utils/delta.js.map +1 -1
  95. package/lib-esm/utils/format.js.map +1 -1
  96. package/lib-esm/utils/insights.js.map +1 -1
  97. package/lib-esm/utils/metrics.js +44 -27
  98. package/lib-esm/utils/metrics.js.map +1 -1
  99. package/lib-esm/webpack/compare.js +7 -5
  100. package/lib-esm/webpack/compare.js.map +1 -1
  101. package/lib-esm/webpack/extract/assets.js +7 -1
  102. package/lib-esm/webpack/extract/assets.js.map +1 -1
  103. package/lib-esm/webpack/extract/meta.js.map +1 -1
  104. package/lib-esm/webpack/extract/modules-packages-duplicate.js +13 -8
  105. package/lib-esm/webpack/extract/modules-packages-duplicate.js.map +1 -1
  106. package/lib-esm/webpack/extract/modules-packages-new-insight.js.map +1 -1
  107. package/lib-esm/webpack/extract/modules-packages.js +8 -1
  108. package/lib-esm/webpack/extract/modules-packages.js.map +1 -1
  109. package/lib-esm/webpack/extract/modules.js +45 -20
  110. package/lib-esm/webpack/extract/modules.js.map +1 -1
  111. package/lib-esm/webpack/metrics.js +35 -34
  112. package/lib-esm/webpack/metrics.js.map +1 -1
  113. package/lib-esm/webpack/selectors.js +35 -1
  114. package/lib-esm/webpack/selectors.js.map +1 -1
  115. package/lib-esm/webpack/utils.js +37 -25
  116. package/lib-esm/webpack/utils.js.map +1 -1
  117. package/package.json +20 -22
  118. package/src/browsertime/metrics.js +22 -22
  119. package/src/config/component-links.js +7 -0
  120. package/src/config/file-types.js +3 -3
  121. package/src/config/index.js +1 -2
  122. package/src/config/metric-types.ts +25 -0
  123. package/src/constants.ts +94 -34
  124. package/src/i18n.js +15 -10
  125. package/src/lighthouse/metrics.js +13 -15
  126. package/src/report/add-row-metric-data.ts +42 -38
  127. package/src/report/compare-metrics.ts +7 -4
  128. package/src/report/create.ts +1 -1
  129. package/src/report/index.js +1 -0
  130. package/src/report/merge-metrics-by-key.ts +7 -5
  131. package/src/report/types.ts +17 -0
  132. package/src/utils/component-links.ts +276 -35
  133. package/src/utils/delta.ts +98 -0
  134. package/src/utils/metrics.ts +88 -0
  135. package/src/webpack/compare.ts +62 -0
  136. package/src/webpack/extract/assets.ts +13 -7
  137. package/src/webpack/extract/modules-packages-duplicate.ts +23 -15
  138. package/src/webpack/extract/modules-packages.ts +74 -61
  139. package/src/webpack/extract/modules.ts +66 -33
  140. package/src/webpack/{metrics.js → metrics.ts} +41 -39
  141. package/src/webpack/selectors.js +82 -9
  142. package/src/webpack/types.ts +13 -4
  143. package/src/webpack/utils.ts +173 -0
  144. package/types/browsertime/compare.d.ts +1 -1
  145. package/types/browsertime/metrics.d.ts +63 -43
  146. package/types/config/component-links.d.ts +26 -18
  147. package/types/config/file-types.d.ts +17 -17
  148. package/types/config/index.d.ts +1 -2
  149. package/types/config/metric-types.d.ts +2 -0
  150. package/types/constants.d.ts +52 -29
  151. package/types/i18n.d.ts +19 -14
  152. package/types/lighthouse/compare.d.ts +1 -1
  153. package/types/lighthouse/filter.d.ts +1 -1
  154. package/types/lighthouse/metrics.d.ts +36 -28
  155. package/types/report/add-row-metric-data.d.ts +1 -16
  156. package/types/report/compare-metrics.d.ts +1 -1
  157. package/types/report/index.d.ts +1 -0
  158. package/types/report/merge-metrics-by-key.d.ts +1 -1
  159. package/types/report/types.d.ts +12 -0
  160. package/types/utils/component-links.d.ts +24 -8
  161. package/types/utils/delta.d.ts +4 -6
  162. package/types/utils/metrics.d.ts +4 -3
  163. package/types/webpack/compare.d.ts +11 -14
  164. package/types/webpack/constants.d.ts +1 -1
  165. package/types/webpack/extract/assets-size-total-insight.d.ts +2 -2
  166. package/types/webpack/extract/modules-packages-duplicate.d.ts +3 -3
  167. package/types/webpack/extract/modules.d.ts +1 -1
  168. package/types/webpack/metrics.d.ts +7 -153
  169. package/types/webpack/selectors.d.ts +21 -7
  170. package/types/webpack/types.d.ts +4 -4
  171. package/types/webpack/utils.d.ts +8 -8
  172. package/CHANGELOG.md +0 -333
  173. package/lib/config/delta.js +0 -13
  174. package/lib/config/delta.js.map +0 -1
  175. package/lib/config/metrics.js +0 -32
  176. package/lib/config/metrics.js.map +0 -1
  177. package/lib-esm/config/delta.js +0 -10
  178. package/lib-esm/config/delta.js.map +0 -1
  179. package/lib-esm/config/metrics.js +0 -29
  180. package/lib-esm/config/metrics.js.map +0 -1
  181. package/src/config/delta.js +0 -11
  182. package/src/config/metrics.js +0 -35
  183. package/src/utils/delta.js +0 -94
  184. package/src/utils/metrics.js +0 -92
  185. package/src/webpack/compare.js +0 -81
  186. package/src/webpack/utils.js +0 -155
  187. package/types/config/delta.d.ts +0 -9
  188. package/types/config/metrics.d.ts +0 -35
@@ -0,0 +1,98 @@
1
+ import round from 'lodash/round';
2
+
3
+ import type {
4
+ MetricRun,
5
+ MetricRunDelta,
6
+ MetricRunInfoDeltaType,
7
+ MetricTypeConfig,
8
+ } from '../constants';
9
+
10
+ export function getDelta(baseline?: MetricRun | null, current?: MetricRun | null): MetricRunDelta {
11
+ const baselineValue = baseline?.value || 0;
12
+ const currentValue = current?.value || 0;
13
+
14
+ if (baselineValue === currentValue) {
15
+ return {
16
+ delta: 0,
17
+ deltaPercentage: 0,
18
+ };
19
+ }
20
+
21
+ if (baselineValue === 0) {
22
+ return {
23
+ delta: currentValue,
24
+ deltaPercentage: 100,
25
+ };
26
+ }
27
+
28
+ return {
29
+ delta: currentValue - baselineValue,
30
+ deltaPercentage: round((currentValue / baselineValue) * 100 - 100, 8),
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Format delta output
36
+ */
37
+ export function formatDelta(value: number, formatter: MetricTypeConfig['formatter']): string {
38
+ let sign = '';
39
+
40
+ if (value > 0) {
41
+ sign = '+';
42
+ } else if (value < 0) {
43
+ sign = '-';
44
+ }
45
+
46
+ let absValue = Math.abs(value);
47
+
48
+ if (absValue > 0 && absValue < 0.01) {
49
+ sign = `~${sign}`;
50
+ absValue = 0.01;
51
+ }
52
+
53
+ if (absValue > 0.01) {
54
+ absValue = round(absValue, 2);
55
+ }
56
+
57
+ return `${sign}${formatter(absValue)}`;
58
+ }
59
+
60
+ /**
61
+ * Get delta type
62
+ */
63
+ export function getDeltaType(
64
+ deltaPercentageValue: number,
65
+ biggerIsBetter?: MetricTypeConfig['biggerIsBetter'],
66
+ ): MetricRunInfoDeltaType {
67
+ // No change
68
+ if (deltaPercentageValue === 0) {
69
+ return 'NO_CHANGE';
70
+ }
71
+
72
+ // Change + no regression/improvement
73
+ if (biggerIsBetter === null) {
74
+ return 'CHANGE';
75
+ }
76
+
77
+ if (deltaPercentageValue < -50) {
78
+ return biggerIsBetter ? 'HIGH_NEGATIVE' : 'HIGH_POSITIVE';
79
+ }
80
+
81
+ if (deltaPercentageValue < -5) {
82
+ return biggerIsBetter ? 'NEGATIVE' : 'POSITIVE';
83
+ }
84
+
85
+ if (deltaPercentageValue < 0) {
86
+ return biggerIsBetter ? 'LOW_NEGATIVE' : 'LOW_POSITIVE';
87
+ }
88
+
89
+ if (deltaPercentageValue <= 5) {
90
+ return biggerIsBetter ? 'LOW_POSITIVE' : 'LOW_NEGATIVE';
91
+ }
92
+
93
+ if (deltaPercentageValue <= 50) {
94
+ return biggerIsBetter ? 'POSITIVE' : 'NEGATIVE';
95
+ }
96
+
97
+ return biggerIsBetter ? 'HIGH_POSITIVE' : 'HIGH_NEGATIVE';
98
+ }
@@ -0,0 +1,88 @@
1
+ import get from 'lodash/get';
2
+
3
+ import {
4
+ MetricRunInfo,
5
+ MetricRunInfoBaseline,
6
+ Metric,
7
+ MetricTypes,
8
+ MetricConfig,
9
+ MetricTypeConfig,
10
+ } from '../constants';
11
+ import { METRIC_TYPE_CONFIGS } from '../config';
12
+ import { metrics as webpackMetricTypes } from '../webpack/metrics';
13
+ import { metrics as lighthouseMetricTypes } from '../lighthouse/metrics';
14
+ import { metrics as browsertimeMeticTypes } from '../browsertime/metrics';
15
+ import { formatPercentage } from './format';
16
+ import { formatDelta, getDelta, getDeltaType } from './delta';
17
+
18
+ export function createGetMetricType(metrics: Record<string, unknown>) {
19
+ return function getMetricType(key: string, type?: MetricTypes): Metric {
20
+ const metric = get(metrics, key) as MetricConfig;
21
+
22
+ if (metric?.type) {
23
+ return {
24
+ ...METRIC_TYPE_CONFIGS[metric.type],
25
+ ...metric,
26
+ };
27
+ }
28
+
29
+ const resolvedType = type || MetricTypes.Numeric;
30
+
31
+ return {
32
+ ...METRIC_TYPE_CONFIGS[resolvedType],
33
+ type: resolvedType,
34
+ label: key,
35
+ };
36
+ };
37
+ }
38
+
39
+ export function getGlobalMetricType(key: string, type?: MetricTypes): Metric {
40
+ const getMetricType = createGetMetricType({
41
+ webpack: webpackMetricTypes,
42
+ lighthouse: lighthouseMetricTypes,
43
+ browsertime: browsertimeMeticTypes,
44
+ });
45
+
46
+ return getMetricType(key, type);
47
+ }
48
+
49
+ export function getMetricRunInfo(
50
+ metricType: MetricTypeConfig,
51
+ current: number,
52
+ baseline?: number,
53
+ ): MetricRunInfo | MetricRunInfoBaseline {
54
+ const { formatter, biggerIsBetter } = metricType;
55
+
56
+ const runInfo = {
57
+ value: current,
58
+ displayValue: formatter(current),
59
+ };
60
+
61
+ // Skip computing delta fields when the baseline is missing
62
+ if (typeof baseline === 'undefined') {
63
+ return runInfo;
64
+ }
65
+
66
+ const { delta, deltaPercentage } = getDelta({ value: baseline }, { value: current });
67
+
68
+ // Resolve regression/improvement flag
69
+ let regression;
70
+
71
+ if (biggerIsBetter === null) {
72
+ regression = null;
73
+ } else if ((biggerIsBetter === true && delta > 0) || (biggerIsBetter === false && delta < 0)) {
74
+ regression = false;
75
+ } else if ((biggerIsBetter === false && delta > 0) || (biggerIsBetter === true && delta < 0)) {
76
+ regression = true;
77
+ }
78
+
79
+ return {
80
+ ...runInfo,
81
+ delta,
82
+ deltaPercentage,
83
+ displayDelta: formatDelta(delta, formatter),
84
+ displayDeltaPercentage: formatDelta(deltaPercentage, formatPercentage),
85
+ deltaType: getDeltaType(deltaPercentage, biggerIsBetter),
86
+ ...(typeof regression !== 'undefined' && { regression }),
87
+ };
88
+ }
@@ -0,0 +1,62 @@
1
+ import { MetricTypes } from '../constants';
2
+ import { compareMetrics } from '../report/compare-metrics';
3
+ import { MetricReportRowTransformFn } from '../report/types';
4
+ import {
5
+ SECTION_WEBPACK_STATS,
6
+ SECTION_WEBPACK_SIZES,
7
+ SECTION_WEBPACK_ASSETS,
8
+ SECTION_WEBPACK_MODULES,
9
+ SECTION_WEBPACK_PACKAGES,
10
+ SECTIONS,
11
+ } from './constants';
12
+ import { selectors, getModulesDuplicateSizeMetrics, getModulesTotalSizeMetrics } from './selectors';
13
+
14
+ const compareStats = (jobs: Array<unknown>, rowTransformers?: Array<MetricReportRowTransformFn>) =>
15
+ compareMetrics(jobs, selectors.stats, undefined, rowTransformers);
16
+
17
+ const compareSizes = (jobs: Array<unknown>, rowTransformers?: Array<MetricReportRowTransformFn>) =>
18
+ compareMetrics(jobs, selectors.sizes, undefined, rowTransformers);
19
+
20
+ const compareAssets = (jobs: Array<unknown>, rowTransformers?: Array<MetricReportRowTransformFn>) =>
21
+ compareMetrics(jobs, selectors.assets, MetricTypes.FileSize, rowTransformers);
22
+
23
+ const compareModules = (
24
+ jobs: Array<unknown>,
25
+ rowTransformers?: Array<MetricReportRowTransformFn>,
26
+ ) => compareMetrics(jobs, selectors.modules, MetricTypes.FileSize, rowTransformers);
27
+
28
+ export const compareModuleDuplicateSize = (
29
+ jobs: Array<unknown>,
30
+ rowTransformers?: Array<MetricReportRowTransformFn>,
31
+ ) => compareMetrics(jobs, getModulesDuplicateSizeMetrics, MetricTypes.FileSize, rowTransformers);
32
+
33
+ export const compareModuleTotalSize = (
34
+ jobs: Array<unknown>,
35
+ rowTransformers?: Array<MetricReportRowTransformFn>,
36
+ ) => compareMetrics(jobs, getModulesTotalSizeMetrics, MetricTypes.FileSize, rowTransformers);
37
+
38
+ const comparePackages = (
39
+ jobs: Array<unknown>,
40
+ rowTransformers?: Array<MetricReportRowTransformFn>,
41
+ ) => compareMetrics(jobs, selectors.packages, MetricTypes.FileSize, rowTransformers);
42
+
43
+ export const compareBySection = {
44
+ [SECTION_WEBPACK_STATS]: compareStats,
45
+ [SECTION_WEBPACK_SIZES]: compareSizes,
46
+ [SECTION_WEBPACK_ASSETS]: compareAssets,
47
+ [SECTION_WEBPACK_MODULES]: compareModules,
48
+ [SECTION_WEBPACK_PACKAGES]: comparePackages,
49
+ };
50
+
51
+ /* eslint-disable prettier/prettier */
52
+ /**
53
+ * Compare webpack sections
54
+ */
55
+ export const compare = (jobs: Array<unknown>) => SECTIONS.reduce(
56
+ (agg, sectionId) => ({
57
+ ...agg,
58
+ [sectionId]: compareBySection[sectionId as keyof typeof compareBySection](jobs),
59
+ }),
60
+ {},
61
+ );
62
+ /* eslint-enable prettier/prettier */
@@ -43,9 +43,7 @@ export const extractAssets = (webpackStats: WebpackStatsFiltered): MetricsAssets
43
43
  const normalizedName = getAssetName(baseName);
44
44
  const { size, name } = asset;
45
45
 
46
- // Reassign for perf
47
- // eslint-disable-next-line no-param-reassign
48
- agg[normalizedName] = {
46
+ const entry = {
49
47
  name: baseName,
50
48
  value: size || 0,
51
49
  isEntry: entryAssetNames.includes(name),
@@ -54,6 +52,14 @@ export const extractAssets = (webpackStats: WebpackStatsFiltered): MetricsAssets
54
52
  ...(assetChunk && { chunkId: assetChunk.id }),
55
53
  };
56
54
 
55
+ if (agg[normalizedName]) {
56
+ // eslint-disable-next-line no-param-reassign
57
+ agg[baseName] = entry;
58
+ } else {
59
+ // eslint-disable-next-line no-param-reassign
60
+ agg[normalizedName] = entry;
61
+ }
62
+
57
63
  return agg;
58
64
  }, {} as Assets) || {};
59
65
 
@@ -63,10 +69,10 @@ export const extractAssets = (webpackStats: WebpackStatsFiltered): MetricsAssets
63
69
  },
64
70
  ...(!isEmpty(normalizedChunks)
65
71
  ? {
66
- meta: {
67
- chunks: normalizedChunks?.map(({ id, name }) => ({ id, name })) || [],
68
- },
69
- }
72
+ meta: {
73
+ chunks: normalizedChunks?.map(({ id, name }) => ({ id, name })) || [],
74
+ },
75
+ }
70
76
  : {}),
71
77
  };
72
78
  };
@@ -26,9 +26,9 @@ interface DuplicatePackageGroup {
26
26
  }
27
27
 
28
28
  interface ModulesPackagesDuplicateData {
29
- insights: {
29
+ insights?: {
30
30
  duplicatePackages?: JobInsights['webpack']['duplicatePackages'];
31
- duplicatePackagesV3: JobInsights['webpack']['duplicatePackagesV3'];
31
+ duplicatePackagesV3?: JobInsights['webpack']['duplicatePackagesV3'];
32
32
  };
33
33
  metrics: {
34
34
  duplicatePackagesCount: MetricRun;
@@ -38,7 +38,7 @@ interface ModulesPackagesDuplicateData {
38
38
  export const getDuplicatePackagesInsight = (
39
39
  duplicatePackagesMap: Record<string, Array<string>>,
40
40
  baselineDuplicatePackagesMap?: Record<string, Array<string>>,
41
- ): JobInsight<JobInsightDuplicatePackagesV3Data> => {
41
+ ): JobInsight<JobInsightDuplicatePackagesV3Data> | null => {
42
42
  const duplicateInstances: Array<string> = [];
43
43
  const baselineDuplicateInstances: Array<string> = [];
44
44
  const newDuplicateInstances: Array<string> = [];
@@ -82,8 +82,8 @@ export const getDuplicatePackagesInsight = (
82
82
  const newDuplicateInstancesCount = newDuplicateInstances.length;
83
83
  const removedDuplicateInstancesCount = removedDuplicateInstances.length;
84
84
 
85
- let text: string;
86
- let insightType: InsightType;
85
+ let text = '';
86
+ let insightType: InsightType | null = null;
87
87
 
88
88
  if (newDuplicateInstancesCount > 0 && removedDuplicateInstancesCount > 0) {
89
89
  // New duplicate packages and removed duplicates
@@ -122,9 +122,11 @@ export const getDuplicatePackagesInsight = (
122
122
  text = `Bundle contains ${duplicateInstancesCount} duplicate ${item}`;
123
123
  insightType = InsightType.WARNING;
124
124
  } else {
125
- // No change
126
- text = 'Bundle does not contain duplicate packages';
127
- insightType = InsightType.INFO;
125
+ // No change - noop
126
+ }
127
+
128
+ if (!text || !insightType) {
129
+ return null;
128
130
  }
129
131
 
130
132
  return {
@@ -187,11 +189,15 @@ export const extractModulesPackagesDuplicate = (
187
189
  baselineJob?.insights?.webpack?.duplicatePackages?.data ||
188
190
  {};
189
191
 
192
+ const noDuplicatesDuplicatePackagesV3 = getDuplicatePackagesInsight({}, baselineDuplicatePackages);
193
+
190
194
  if (!count) {
191
195
  return {
192
- insights: {
193
- duplicatePackagesV3: getDuplicatePackagesInsight({}, baselineDuplicatePackages),
194
- },
196
+ ...(noDuplicatesDuplicatePackagesV3 && {
197
+ insights: {
198
+ duplicatePackagesV3: noDuplicatesDuplicatePackagesV3,
199
+ },
200
+ }),
195
201
  metrics: {
196
202
  duplicatePackagesCount: {
197
203
  value: count,
@@ -228,16 +234,18 @@ export const extractModulesPackagesDuplicate = (
228
234
  duplicatePackagesData[packageName] = packageData.children.map(({ id }) => id);
229
235
  });
230
236
 
237
+ const duplicatePackagesV3 = getDuplicatePackagesInsight(
238
+ duplicatePackagesData,
239
+ baselineDuplicatePackages,
240
+ );
241
+
231
242
  return {
232
243
  insights: {
233
244
  duplicatePackages: {
234
245
  type: InsightType.WARNING,
235
246
  data: duplicatePackagesData,
236
247
  },
237
- duplicatePackagesV3: getDuplicatePackagesInsight(
238
- duplicatePackagesData,
239
- baselineDuplicatePackages,
240
- ),
248
+ ...(duplicatePackagesV3 && { duplicatePackagesV3 }),
241
249
  },
242
250
  metrics: {
243
251
  duplicatePackagesCount: {
@@ -22,6 +22,11 @@ const uniqLast = (data: Array<unknown>) => {
22
22
  * Heuristics to extract package id, name, and path from a module path
23
23
  */
24
24
  export const getPackageMetaFromModulePath = (modulePath: string) => {
25
+ // Skip module federation entries
26
+ if (modulePath.match(/^(provide|consume) shared module/)) {
27
+ return null;
28
+ }
29
+
25
30
  const paths = modulePath.match(MODULE_PATH_PACKAGES);
26
31
 
27
32
  // No package paths found, skip
@@ -41,7 +46,11 @@ export const getPackageMetaFromModulePath = (modulePath: string) => {
41
46
  return [];
42
47
  }
43
48
 
44
- return [...found].flat().slice(1).filter(Boolean).map((name) => name.replace(/\+/g, '/'));
49
+ return [...found]
50
+ .flat()
51
+ .slice(1)
52
+ .filter(Boolean)
53
+ .map((name) => name.replace(/\+/g, '/'));
45
54
  })
46
55
  .flat(),
47
56
  );
@@ -71,74 +80,78 @@ export const extractModulesPackages = (
71
80
  ): MetricsPackages => {
72
81
  const modules = Object.entries(currentExtractedData?.metrics?.modules || {});
73
82
 
74
- const packages = modules.reduce((agg, [modulePath, { value }]) => {
75
- const packageMeta = getPackageMetaFromModulePath(modulePath);
76
-
77
- if (!packageMeta) {
78
- return agg;
79
- }
80
-
81
- const existingPackageData = agg[packageMeta.id];
83
+ const packages = modules.reduce(
84
+ (agg, [modulePath, { value }]) => {
85
+ const packageMeta = getPackageMetaFromModulePath(modulePath);
86
+
87
+ if (!packageMeta) {
88
+ return agg;
89
+ }
90
+
91
+ const existingPackageData = agg[packageMeta.id];
92
+
93
+ // New package data
94
+ if (!existingPackageData) {
95
+ return {
96
+ ...agg,
97
+ [packageMeta.id]: {
98
+ name: packageMeta.name,
99
+ path: packageMeta.path,
100
+ value,
101
+ },
102
+ };
103
+ }
104
+
105
+ // Existing package info
106
+ if (existingPackageData.path === packageMeta.path) {
107
+ return {
108
+ ...agg,
109
+ [packageMeta.id]: {
110
+ ...existingPackageData,
111
+ value: existingPackageData.value + value,
112
+ },
113
+ };
114
+ }
115
+
116
+ // Same package name, but different paths (eg: symlinks)
117
+ const existingPackageWithEqualPath = Object.entries(agg).find(
118
+ ([__, packageData]) => packageData.path === packageMeta.path,
119
+ );
120
+
121
+ if (existingPackageWithEqualPath) {
122
+ const [name, data] = existingPackageWithEqualPath;
123
+
124
+ return {
125
+ ...agg,
126
+ [name]: {
127
+ ...data,
128
+ value: data.value + value,
129
+ },
130
+ };
131
+ }
132
+
133
+ // New package name & data
134
+ const lastIndex =
135
+ max(
136
+ Object.keys(agg)
137
+ .map((id) => id.split('~'))
138
+ .filter(([id]) => id === packageMeta.id)
139
+ .map(([__, index]) => parseInt(index, 10)),
140
+ ) || 0;
141
+
142
+ const packageName = [packageMeta.id, lastIndex + 1].join(PACKAGE_ID_SEPARATOR);
82
143
 
83
- // New package data
84
- if (!existingPackageData) {
85
144
  return {
86
145
  ...agg,
87
- [packageMeta.id]: {
146
+ [packageName]: {
88
147
  name: packageMeta.name,
89
148
  path: packageMeta.path,
90
149
  value,
91
150
  },
92
151
  };
93
- }
94
-
95
- // Existing package info
96
- if (existingPackageData.path === packageMeta.path) {
97
- return {
98
- ...agg,
99
- [packageMeta.id]: {
100
- ...existingPackageData,
101
- value: existingPackageData.value + value,
102
- },
103
- };
104
- }
105
-
106
- // Same package name, but different paths (eg: symlinks)
107
- const existingPackageWithEqualPath = Object.entries(agg).find(
108
- ([__, packageData]) => packageData.path === packageMeta.path,
109
- );
110
-
111
- if (existingPackageWithEqualPath) {
112
- const [name, data] = existingPackageWithEqualPath;
113
-
114
- return {
115
- ...agg,
116
- [name]: {
117
- ...data,
118
- value: data.value + value,
119
- },
120
- };
121
- }
122
-
123
- // New package name & data
124
- const lastIndex = max(
125
- Object.keys(agg)
126
- .map((id) => id.split('~'))
127
- .filter(([id]) => id === packageMeta.id)
128
- .map(([__, index]) => parseInt(index, 10)),
129
- ) || 0;
130
-
131
- const packageName = [packageMeta.id, lastIndex + 1].join(PACKAGE_ID_SEPARATOR);
132
-
133
- return {
134
- ...agg,
135
- [packageName]: {
136
- name: packageMeta.name,
137
- path: packageMeta.path,
138
- value,
139
- },
140
- };
141
- }, {} as Record<string, Package>);
152
+ },
153
+ {} as Record<string, Package>,
154
+ );
142
155
 
143
156
  return { metrics: { packages } };
144
157
  };
@@ -1,11 +1,15 @@
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 {
4
+ WebpackStatsFiltered,
5
+ WebpackStatsFilteredModule,
6
+ } from '@bundle-stats/plugin-webpack-filter';
3
7
 
4
- import { Module, MetricsModules } from '../types';
8
+ import type { MetricsModules, Modules } from '../types';
5
9
  import { getModuleName, normalizeChunkId } from '../utils';
6
10
 
7
11
  /*
8
- * Extract webpack modules array to an object with metrics
12
+ * Extract webpack modules metrics and stats
9
13
  */
10
14
  export const extractModules = (webpackStats?: WebpackStatsFiltered): MetricsModules => {
11
15
  const modulesSource = webpackStats?.modules;
@@ -22,58 +26,87 @@ export const extractModules = (webpackStats?: WebpackStatsFiltered): MetricsModu
22
26
  }
23
27
 
24
28
  // Flatten concatenated modules
25
- const allModules = modulesSource.reduce((agg, moduleEntry) => {
26
- if (!moduleEntry.modules) {
27
- agg.push(moduleEntry);
29
+ const modulesByName: Map<string, WebpackStatsFilteredModule> = new Map();
28
30
 
29
- return agg;
31
+ modulesSource.forEach((rootModule) => {
32
+ // skip modules that do not belong to any chunk
33
+ if (!rootModule.chunks || rootModule.chunks.length === 0) {
34
+ return;
35
+ }
36
+
37
+ // flatten concatenated modules
38
+ if (rootModule.modules) {
39
+ rootModule.modules.forEach((concatenatedModule) => {
40
+ const concatenatedModuleName = getModuleName(concatenatedModule.name);
41
+ const existingModule = modulesByName.get(concatenatedModuleName);
42
+
43
+ // Merge if the module is already available
44
+ // webpack adds a module entry for every concatenated module
45
+ if (existingModule) {
46
+ modulesByName.set(concatenatedModuleName, {
47
+ ...existingModule,
48
+ chunks: union(existingModule.chunks, rootModule.chunks),
49
+ });
50
+ } else {
51
+ modulesByName.set(concatenatedModuleName, {
52
+ ...concatenatedModule,
53
+ chunks: rootModule.chunks,
54
+ });
55
+ }
56
+ });
57
+
58
+ return;
59
+ }
60
+
61
+ const normalizedName = getModuleName(rootModule.name);
62
+ const savedRootModule = modulesByName.get(normalizedName);
63
+
64
+ // Merge module data
65
+ // @NOTE some webpack plugins (mini-css-extract-plugin) might add a root module entry
66
+ // even if thee module was already concantenated in another module
67
+ if (savedRootModule) {
68
+ modulesByName.set(normalizedName, {
69
+ ...savedRootModule,
70
+ chunks: union(savedRootModule.chunks, rootModule.chunks),
71
+ size: rootModule.size,
72
+ });
73
+
74
+ return;
30
75
  }
31
76
 
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
- );
77
+ modulesByName.set(normalizedName, rootModule);
78
+ });
40
79
 
41
- return agg;
42
- }, [] as Array<WebpackStatsFilteredRootModule>);
80
+ // Normalize module entries and extract metrics
81
+ const modules: Modules = {};
43
82
 
44
83
  let moduleCount = 0;
45
84
  let totalCodeSize = 0;
46
85
  let duplicateCodeSize = 0;
47
86
  let duplicateModulesCount = 0;
48
87
 
49
- // Normalize module entries
50
- const modules = allModules.reduce((agg, moduleEntry) => {
88
+ modulesByName.forEach((moduleEntry, normalizedName) => {
51
89
  const { name, size = 0, chunks } = moduleEntry;
52
- const normalizedName = getModuleName(name);
53
-
54
- // skip modules that do not belong to any chunk
55
- if (!chunks || chunks?.length === 0) {
56
- return agg;
57
- }
58
90
 
59
91
  const instances = chunks.length;
60
92
  const duplicateInstances = instances - 1;
93
+ const duplicated = duplicateInstances > 0;
61
94
 
62
95
  moduleCount += instances;
63
- duplicateModulesCount += duplicateInstances;
64
- duplicateCodeSize += duplicateInstances * size;
65
96
  totalCodeSize += instances * size;
66
97
 
67
- // eslint-disable-next-line no-param-reassign
68
- agg[normalizedName] = {
98
+ if (duplicated) {
99
+ duplicateModulesCount += duplicateInstances;
100
+ duplicateCodeSize += duplicateInstances * size;
101
+ }
102
+
103
+ modules[normalizedName] = {
69
104
  name,
70
105
  value: size,
71
106
  chunkIds: chunks.map(normalizeChunkId),
72
- duplicated: Boolean(duplicateInstances),
107
+ ...(duplicated && { duplicated }),
73
108
  };
74
-
75
- return agg;
76
- }, {} as Record<string, Module>);
109
+ }, {} as Modules);
77
110
 
78
111
  const duplicateCode = totalCodeSize ? round((duplicateCodeSize / totalCodeSize) * 100, 2) : 0;
79
112