diff --git a/packages/ui/src/components/bundle-modules/bundle-modules.jsx b/packages/ui/src/components/bundle-modules/bundle-modules.jsx index 25c028b6f8..44a7f8c8fd 100644 --- a/packages/ui/src/components/bundle-modules/bundle-modules.jsx +++ b/packages/ui/src/components/bundle-modules/bundle-modules.jsx @@ -10,8 +10,7 @@ import { MODULE_CHUNK, MODULE_FILTERS, MODULE_FILE_TYPE, - SECTIONS, - COMPONENT, + getBundleModulesEntry, } from '@bundle-stats/utils'; import config from '../../config.json'; @@ -31,87 +30,60 @@ import { ModuleInfo } from '../module-info'; import css from './bundle-modules.module.css'; const getFilters = ({ filters, compareMode, chunks }) => ({ - [MODULE_FILTERS.CHANGED]: { - label: 'Changed', - defaultValue: filters.changed, - disabled: !compareMode, - }, - [MODULE_FILTERS.DUPLICATED]: { - label: 'Duplicate', - defaultValue: filters[MODULE_FILTERS.DUPLICATED], - }, - - // When chunks data available, list available chunks as filters - ...(!isEmpty(chunks) && { - [MODULE_CHUNK]: { - label: 'Chunk', - ...chunks.reduce( - (chunkFilters, { id, name }) => ({ - ...chunkFilters, - [id]: { - label: name, - defaultValue: get(filters, `${MODULE_CHUNK}.${id}`, true), - }, - }), - {}, - ), - }, - }), - - [MODULE_SOURCE_TYPE]: { - label: 'Source', - [MODULE_FILTERS.FIRST_PARTY]: { - label: 'First party', - defaultValue: get(filters, `${MODULE_SOURCE_TYPE}.${MODULE_FILTERS.FIRST_PARTY}`, true), - }, - [MODULE_FILTERS.THIRD_PARTY]: { - label: 'Third party', - defaultValue: get(filters, `${MODULE_SOURCE_TYPE}.${MODULE_FILTERS.THIRD_PARTY}`, true), - }, - }, + [MODULE_FILTERS.CHANGED]: { + label: 'Changed', + defaultValue: filters.changed, + disabled: !compareMode, + }, + [MODULE_FILTERS.DUPLICATED]: { + label: 'Duplicate', + defaultValue: filters[MODULE_FILTERS.DUPLICATED], + }, - // Module source types - [MODULE_FILE_TYPE]: { - label: 'File type', - ...MODULE_SOURCE_FILE_TYPES.reduce( - (agg, fileType) => ({ - ...agg, - [fileType]: { - label: FILE_TYPE_LABELS[fileType], - defaultValue: get(filters, `${MODULE_FILE_TYPE}.${fileType}`, true), + // When chunks data available, list available chunks as filters + ...(!isEmpty(chunks) && { + [MODULE_CHUNK]: { + label: 'Chunk', + ...chunks.reduce( + (chunkFilters, { id, name }) => ({ + ...chunkFilters, + [id]: { + label: name, + defaultValue: get(filters, `${MODULE_CHUNK}.${id}`, true), }, }), {}, ), }, -}) + }), -const RowHeader = ({ row, filters, search, customComponentLink: CustomComponentLink }) => ( - - {row.duplicated && ( - - )} - - -); - -RowHeader.propTypes = { - row: PropTypes.shape({ - label: PropTypes.string, - duplicated: PropTypes.bool, - }).isRequired, - search: PropTypes.string, - filters: PropTypes.object, - customComponentLink: PropTypes.elementType.isRequired, -}; + [MODULE_SOURCE_TYPE]: { + label: 'Source', + [MODULE_FILTERS.FIRST_PARTY]: { + label: 'First party', + defaultValue: get(filters, `${MODULE_SOURCE_TYPE}.${MODULE_FILTERS.FIRST_PARTY}`, true), + }, + [MODULE_FILTERS.THIRD_PARTY]: { + label: 'Third party', + defaultValue: get(filters, `${MODULE_SOURCE_TYPE}.${MODULE_FILTERS.THIRD_PARTY}`, true), + }, + }, -RowHeader.defaultProps = { - chunks: [], -}; + // Module source types + [MODULE_FILE_TYPE]: { + label: 'File type', + ...MODULE_SOURCE_FILE_TYPES.reduce( + (agg, fileType) => ({ + ...agg, + [fileType]: { + label: FILE_TYPE_LABELS[fileType], + defaultValue: get(filters, `${MODULE_FILE_TYPE}.${fileType}`, true), + }, + }), + {}, + ), + }, +}); export const BundleModules = ({ className, @@ -139,45 +111,56 @@ export const BundleModules = ({ const dropdownFilters = useMemo( () => getFilters({ filters, chunks, compareMode: jobs.length > 1 }), - [jobs, filters, chunks] + [jobs, filters, chunks], + ); + + const metricsTableTitle = useMemo( + () => ( + + ), + [items, totalRowCount], ); - const metricsTableTitle = useMemo(() => ( - - ), [items, totalRowCount]); + const getEntryComponentLinkProps = useCallback( + (moduleEntryId) => getBundleModulesEntry(moduleEntryId, search, filters), + [filters, search], + ); const renderRowHeader = useCallback( (row) => ( - + + {row.duplicated && ( + + )} + + ), - [jobs, chunks, CustomComponentLink, filters, search], + [jobs, chunks, CustomComponentLink, getEntryComponentLinkProps], ); - const emptyMessage = useMemo(() => ( - - ), [totalRowCount, resetFilters, resetAllFilters]); + const emptyMessage = useMemo( + () => ( + + ), + [totalRowCount, resetFilters, resetAllFilters], + ); const entryItem = useMemo(() => { if (!entryId) { return null; } - return allItems.find(({ key }) => key === entryId) + return allItems.find(({ key }) => key === entryId); }, [allItems, entryId]); return ( @@ -230,6 +213,7 @@ export const BundleModules = ({ chunkIds={chunks?.map(({ id }) => id)} labels={jobLabels} customComponentLink={CustomComponentLink} + getEntryComponentLinkProps={getEntryComponentLinkProps} onClose={hideEntryInfo} /> )} @@ -240,8 +224,10 @@ export const BundleModules = ({ BundleModules.defaultProps = { className: '', items: [], + allItems: [], jobs: [], totalRowCount: 0, + entryId: '', hasActiveFilters: false, customComponentLink: ComponentLink, }; @@ -253,6 +239,9 @@ BundleModules.propTypes = { /** Rows data */ items: PropTypes.array, // eslint-disable-line react/forbid-prop-types + /** All rows data */ + allItems: PropTypes.array, // eslint-disable-line react/forbid-prop-types + /** Jobs data */ jobs: PropTypes.array, // eslint-disable-line react/forbid-prop-types @@ -278,6 +267,7 @@ BundleModules.propTypes = { filters: PropTypes.shape({ changed: PropTypes.bool, }).isRequired, + entryId: PropTypes.string, hasActiveFilters: PropTypes.bool, diff --git a/packages/ui/src/components/module-info/module-info.module.css b/packages/ui/src/components/module-info/module-info.module.css index e370226bfe..71a16c3396 100644 --- a/packages/ui/src/components/module-info/module-info.module.css +++ b/packages/ui/src/components/module-info/module-info.module.css @@ -1,3 +1,10 @@ .chunksItems { display: inline; } + +.reasons { + display: inline-block; + padding: 0; + margin: 0; + list-style-position: inside; +} diff --git a/packages/ui/src/components/module-info/module-info.tsx b/packages/ui/src/components/module-info/module-info.tsx index 2f914cd313..382a0dd9bd 100644 --- a/packages/ui/src/components/module-info/module-info.tsx +++ b/packages/ui/src/components/module-info/module-info.tsx @@ -1,7 +1,6 @@ import React, { useMemo } from 'react'; import cx from 'classnames'; import isEmpty from 'lodash/isEmpty'; -import noop from 'lodash/noop'; import { BUNDLE_MODULES_DUPLICATE, FILE_TYPE_LABELS, @@ -14,6 +13,7 @@ import { import { Module, MetaChunk } from '@bundle-stats/utils/types/webpack'; import { Stack } from '../../layout/stack'; +import { FileName } from '../../ui/file-name'; import { Tag } from '../../ui/tag'; import { ComponentLink } from '../component-link'; import { EntryInfo, EntryInfoMetaLink } from '../entry-info'; @@ -32,6 +32,7 @@ interface ModuleInfoProps { chunkIds?: Array; labels: Array; customComponentLink?: React.ElementType; + getEntryComponentLinkProps: (entryId: string) => Record; onClose: () => void; } @@ -43,7 +44,7 @@ export const ModuleInfo = (props: ModuleInfoProps & React.ComponentProps<'div'>) chunks = [], chunkIds = [], customComponentLink: CustomComponentLink = ComponentLink, - onClick = noop, + getEntryComponentLinkProps, onClose, } = props; @@ -110,11 +111,28 @@ export const ModuleInfo = (props: ModuleInfoProps & React.ComponentProps<'div'>) {sourceTypeLabel} + + {item?.runs?.[0].reasons && ( + +
    + {item.runs[0].reasons.map((reason) => ( +
  • + + + +
  • + ))} +
+
+ )} ); diff --git a/packages/utils/src/i18n.js b/packages/utils/src/i18n.js index b6aa1002eb..c75bc2f8dd 100644 --- a/packages/utils/src/i18n.js +++ b/packages/utils/src/i18n.js @@ -6,6 +6,7 @@ export default { COMPONENT_LINK_BUNDLE_ASSETS_COUNT: 'View all assets', COMPONENT_LINK_BUNDLE_ASSETS_CHUNK_COUNT: 'View all chunks', COMPONENT_LINK_MODULES: 'View modules', + COMPONENT_LINK_MODULE: 'View module information', COMPONENT_LINK_MODULES_DUPLICATE: 'View duplicate modules', COMPONENT_LINK_MODULES_BY_FILE_TYPE: (fileType) => `View all ${fileType} modules`, COMPONENT_LINK_MODULES_BY_SOURCE: (source) => `View all ${source} modules`, diff --git a/packages/utils/src/utils/component-links.ts b/packages/utils/src/utils/component-links.ts index 6a9cd4ac48..396ccad9b8 100644 --- a/packages/utils/src/utils/component-links.ts +++ b/packages/utils/src/utils/component-links.ts @@ -246,6 +246,22 @@ export const getBundleModulesBySearch = (search: string): ComponentLink => ({ }, }); +export const getBundleModulesEntry = ( + entryId: string, + search = '', + filters: ComponentLinkFilters = {}, +): ComponentLink => ({ + section: SECTIONS.MODULES, + title: I18N.COMPONENT_LINK_MODULE, + params: { + [COMPONENT.BUNDLE_MODULES]: { + search, + filters, + entryId, + }, + }, +}); + export const getBundleModulesByChunk = ( chunkIds: Array, chunkId: string, diff --git a/packages/utils/src/webpack/extract/modules.ts b/packages/utils/src/webpack/extract/modules.ts index 22f71a3f4d..424f0dd297 100644 --- a/packages/utils/src/webpack/extract/modules.ts +++ b/packages/utils/src/webpack/extract/modules.ts @@ -84,11 +84,10 @@ export const extractModules = (webpackStats?: WebpackStatsFiltered): MetricsModu modulesByName.forEach((moduleEntry, normalizedName) => { const { name, size = 0, chunks } = moduleEntry; - const normalizedName = getModuleName(name); // skip modules that are orphane(do not belong to any chunk) if (!chunks || chunks?.length === 0) { - return agg; + return; } const instances = chunks.length; @@ -98,12 +97,13 @@ export const extractModules = (webpackStats?: WebpackStatsFiltered): MetricsModu moduleCount += instances; totalCodeSize += instances * size; - const reasons = moduleEntry.reasons?.map((reason) => getModuleName(reason.module)); if (duplicated) { duplicateModulesCount += duplicateInstances; duplicateCodeSize += duplicateInstances * size; } + const reasons = moduleEntry.reasons?.map((reason) => getModuleName(reason.module)); + modules[normalizedName] = { name, value: size,