diff --git a/.changeset/fluffy-islands-unite.md b/.changeset/fluffy-islands-unite.md new file mode 100644 index 00000000..2f68aef9 --- /dev/null +++ b/.changeset/fluffy-islands-unite.md @@ -0,0 +1,6 @@ +--- +'@rsdoctor/components': patch +'@rsdoctor/client': patch +--- + +feat(platform): report platform add bundle size page diff --git a/examples/modern-minimal/modern.config.ts b/examples/modern-minimal/modern.config.ts new file mode 100644 index 00000000..31869622 --- /dev/null +++ b/examples/modern-minimal/modern.config.ts @@ -0,0 +1,27 @@ +import appTools, { defineConfig } from '@modern-js/app-tools'; +import { RsdoctorWebpackPlugin } from '@rsdoctor/webpack-plugin'; + +const pluginName = 'Web Doctor'; + +export default defineConfig({ + source: { + entries: { + main: './src/index.ts', + }, + }, + plugins: [appTools()], + builderPlugins: [ + { + name: pluginName, + setup(builder) { + builder.modifyWebpackChain((chain) => { + chain.plugin(pluginName).use(RsdoctorWebpackPlugin, [ + { + disableClientServer: !process.env.ENABLE_CLIENT_SERVER, + }, + ]); + }); + }, + }, + ], +}); diff --git a/examples/modern-minimal/package.json b/examples/modern-minimal/package.json new file mode 100644 index 00000000..84e22c5f --- /dev/null +++ b/examples/modern-minimal/package.json @@ -0,0 +1,31 @@ +{ + "name": "@example/doctor-modern-minimal", + "version": "0.0.1", + "description": "", + "files": [ + "dist", + "src", + "package.json" + ], + "scripts": { + "start:analysis": "ENABLE_CLIENT_SERVER=true modern start", + "build:analysis": "ENABLE_CLIENT_SERVER=true modern build", + "build": "modern build" + }, + "author": "", + "license": "MIT", + "dependencies": { + "@babel/highlight": "7.18.6", + "chalk": "4.1.2", + "htmlparser2": "7.2.0" + }, + "devDependencies": { + "@rsdoctor/webpack-plugin": "workspace:*", + "@modern-js/app-tools": "2.41.0", + "@types/node": "14.18.26", + "eslint": "8.22.0", + "ts-loader": "9.4.2", + "tslib": "2.4.1", + "typescript": "4.9.4" + } +} diff --git a/examples/modern-minimal/src/html.ts b/examples/modern-minimal/src/html.ts new file mode 100644 index 00000000..ab1954d0 --- /dev/null +++ b/examples/modern-minimal/src/html.ts @@ -0,0 +1,15 @@ +import Parser from 'htmlparser2'; + +export function getHtmlText(code: string) { + let context = ''; + + const parser = new Parser.Parser({ + ontext(data) { + context += data; + }, + }); + + parser.write(code); + + return context; +} diff --git a/examples/modern-minimal/src/index.ts b/examples/modern-minimal/src/index.ts new file mode 100644 index 00000000..2d5caaf8 --- /dev/null +++ b/examples/modern-minimal/src/index.ts @@ -0,0 +1,8 @@ +import { Instance } from 'chalk'; +import { highlight } from './utils'; +import { getHtmlText } from './html'; + +const print = new Instance(); + +print(getHtmlText('
Test Text
')); +print(highlight('const abc = 123;')); diff --git a/examples/modern-minimal/src/types.ts b/examples/modern-minimal/src/types.ts new file mode 100644 index 00000000..8f4fb3ec --- /dev/null +++ b/examples/modern-minimal/src/types.ts @@ -0,0 +1 @@ +declare module '@babel/highlight'; diff --git a/examples/modern-minimal/src/utils.ts b/examples/modern-minimal/src/utils.ts new file mode 100644 index 00000000..a9ba4c1e --- /dev/null +++ b/examples/modern-minimal/src/utils.ts @@ -0,0 +1,7 @@ +import highlight from '@babel/highlight'; + +export { highlight }; +export const key1 = '123'; +export const key2 = '123'; + +console.log(key2); diff --git a/examples/modern-minimal/tsconfig.json b/examples/modern-minimal/tsconfig.json new file mode 100644 index 00000000..d6a1f33f --- /dev/null +++ b/examples/modern-minimal/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@rsdoctor/tsconfig/base", + "include": ["src", "webpack.config.ts"], + "compilerOptions": { + "outDir": "dist", + "baseUrl": ".", + "module": "ESNext" + } +} diff --git a/examples/webpack-minimal/src/index.ts b/examples/webpack-minimal/src/index.ts index ee34841d..0d23e630 100644 --- a/examples/webpack-minimal/src/index.ts +++ b/examples/webpack-minimal/src/index.ts @@ -6,5 +6,5 @@ import { key6 } from './utils2'; const print = new Instance(); print(key6); -print(getHtmlText('
测试文本
')); +print(getHtmlText('
Test Text
')); print(highlight?.('const abc = 123;')); diff --git a/nx.json b/nx.json index adb76fb0..2d4053d9 100644 --- a/nx.json +++ b/nx.json @@ -2,7 +2,7 @@ "$schema": "./node_modules/nx/schemas/nx-schema.json", "targetDefaults": { "build": { - "cache": false, + "cache": true, "dependsOn": ["^build"], "inputs": [ "{projectRoot}/src/**/*", diff --git a/packages/client/modern.config.ts b/packages/client/modern.config.ts index 7a2d7703..9219ee60 100644 --- a/packages/client/modern.config.ts +++ b/packages/client/modern.config.ts @@ -48,10 +48,7 @@ export default defineConfig<'webpack'>((env) => { media: 'resource/media', }, assetPrefix: IS_PRODUCTION - ? // 此处不要修改!这里 production 的 publicPath 会和 sdk serve 的路径 以及 轻服务 部署的路径联动。 - // OFFICAL_PREVIEW_PUBLIC_PATH 是提供给 轻服务 部署后域名关系 - // "/" 是提供给 sdk 使用的 - OFFICAL_PREVIEW_PUBLIC_PATH?.replace(/\/resource$/, '') || '/' + ? OFFICAL_PREVIEW_PUBLIC_PATH?.replace(/\/resource$/, '') || '/' : '/', cleanDistPath: IS_PRODUCTION, disableTsChecker: !IS_PRODUCTION, @@ -62,11 +59,6 @@ export default defineConfig<'webpack'>((env) => { strategy: 'custom', splitChunks: { cacheGroups: { - shadow: { - test: /node_modules\/@byted-shadow\/*/, - name: 'shadow', - chunks: 'all', - }, react: { test: /node_modules\/react-*/, name: 'react', diff --git a/packages/client/src/router.tsx b/packages/client/src/router.tsx index 93270552..92914b80 100644 --- a/packages/client/src/router.tsx +++ b/packages/client/src/router.tsx @@ -1,15 +1,24 @@ import React from 'react'; import { Route, Routes } from 'react-router-dom'; - -import { OverallPage } from '@rsdoctor/components/pages'; +import { Overall, BundleSize } from '@rsdoctor/components/pages'; export default function Router(): React.ReactElement { + const routes = [ + /** bundle routes */ + { + path: BundleSize.route, + element: , + }, + ].filter((e) => Boolean(e)) as { path: string; element: JSX.Element }[]; return ( - } /> - } /> + } /> + } /> + {routes.map((e) => ( + + ))} ); } diff --git a/packages/components/src/components/Form/keyword.tsx b/packages/components/src/components/Form/keyword.tsx new file mode 100644 index 00000000..f1c36f85 --- /dev/null +++ b/packages/components/src/components/Form/keyword.tsx @@ -0,0 +1,70 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { Button, Input, InputRef, Typography } from 'antd'; + +interface KeywordProps { + style?: React.CSSProperties; + labelStyle?: React.CSSProperties; + icon?: React.ReactNode; + label?: string; + placeholder?: string; + delay?: number; + className?: string; + width?: number; + onChange: (keyword: string) => void; +} + +export const KeywordInput: React.FC = ({ + icon: Icon, + label, + labelStyle: ls = {}, + placeholder, + onChange, + style, + className, + width, + delay = 300, +}) => { + const labelWidth = 120; + const [filename, setFilename] = useState(''); + const labelStyle: React.CSSProperties = { width: labelWidth, ...ls }; + + const ref = useRef(null); + + let timer: NodeJS.Timeout; + + useEffect(() => { + onChange(filename); + }, [filename]); + + return ( + + {label || Icon ? ( + + ) : null} + { + clearTimeout(timer); + const v = e.target.value.trim(); + setTimeout(() => { + setFilename(v); + }, delay); + }} + /> + + ); +}; diff --git a/packages/components/src/components/Keyword/index.tsx b/packages/components/src/components/Keyword/index.tsx new file mode 100644 index 00000000..be626fff --- /dev/null +++ b/packages/components/src/components/Keyword/index.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { Typography } from 'antd'; +import { TextProps } from 'antd/es/typography/Text'; + +export const Keyword: React.FC = ({ text, keyword, ...rest }) => { + if (!keyword) { + return {text}; + } + + const idx = text.indexOf(keyword); + if (idx === -1) { + return {text}; + } + + const els: (string | React.ReactNode)[] = []; + + let str = text; + + while (str.length > 0) { + const idx = str.indexOf(keyword); + if (idx > -1) { + if (idx !== 0) { + els.push( + + {str.slice(0, idx)} + , + ); + } + els.push( + + {keyword} + , + ); + str = str.slice(idx + keyword.length); + } else { + els.push( + + {str} + , + ); + break; + } + } + + return {els}; +}; diff --git a/packages/components/src/components/Layout/header.tsx b/packages/components/src/components/Layout/header.tsx index 6ee8c4e9..3f1ccc0e 100644 --- a/packages/components/src/components/Layout/header.tsx +++ b/packages/components/src/components/Layout/header.tsx @@ -1,10 +1,8 @@ import { TranslationOutlined, UserOutlined } from '@ant-design/icons'; -import { Avatar, Button, Col, Dropdown, Input, Layout, Row, Select, Switch, Typography } from 'antd'; +import { Avatar, Col, Dropdown, Layout, Row, Switch, Typography } from 'antd'; import React from 'react'; -import { APILoaderMode4Dev, Language, Size, Theme } from '../../constants'; +import { Language, Size, Theme } from '../../constants'; import { - getAPILoaderModeFromStorage, - setAPILoaderModeToStorage, useI18n, useTheme } from '../../utils'; @@ -63,28 +61,6 @@ export const Header: React.FC = () => { wrap={false} gutter={[Size.BasePadding / 3, 0]} > - {process.env.NODE_ENV === 'development' ? ( - - - - - - - ) : null} diff --git a/packages/components/src/components/Layout/menus.tsx b/packages/components/src/components/Layout/menus.tsx index db86a7f9..be3c35c6 100644 --- a/packages/components/src/components/Layout/menus.tsx +++ b/packages/components/src/components/Layout/menus.tsx @@ -1,5 +1,6 @@ import { BarChartOutlined, + FolderViewOutlined, MenuOutlined } from '@ant-design/icons'; import { Manifest, SDK } from '@rsdoctor/types'; @@ -9,8 +10,9 @@ import React from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { Size } from '../../constants'; import * as OverallConstants from '../../pages/Overall/constants'; -import { useI18n } from '../../utils'; +import { useI18n, hasBundle } from '../../utils'; import { withServerAPI } from '../Manifest'; +import { BundleSize } from 'src/pages'; const BuilderSwitchName = 'builder-switcher'; @@ -21,9 +23,14 @@ const MenusBase: React.FC<{ style?: React.CSSProperties; routes: Manifest.Doctor const { pathname } = useLocation(); const navigate = useNavigate(); const { routes: enableRoutes } = props; + const iconStyle: React.CSSProperties = { fontSize: 16, }; + const customIconStyle: React.CSSProperties = { + ...iconStyle, + transform: 'translateY(-2px)', + }; const items: MenuProps['items'] = []; @@ -43,6 +50,27 @@ const MenusBase: React.FC<{ style?: React.CSSProperties; routes: Manifest.Doctor }); } + if (hasBundle(enableRoutes)) { + items.push({ + label: t(BundleSize.name), + key: BundleSize.name, + icon: 📦, + children: [ + includes(enableRoutes, Manifest.DoctorManifestClientRoutes.BundleSize) && { + label: t(BundleSize.name), + key: BundleSize.route, + icon: , + }, + // TODO: Tree shaking menu + // includes(enableRoutes, Manifest.DoctorManifestClientRoutes.TreeShaking) && { + // label: t(TreeShakingConstants.name), + // key: TreeShakingConstants.route, + // icon: , + // }, + ].filter((e) => Boolean(e)) as MenuProps['items'], + }); + } + const MenuComponent = ( = ({ data }) => { + const [tab, setTab] = useState('parsedSource'); + const { t } = useI18n(); + + const TAB_LAB_MAP: Record = { + source: 'source code', + transformed: `transformed(${t('After Compile')})`, + parsedSource: `parsedSource(${t('After Bundle')})`, + }; + if (!data) return null; + + const { path } = data; + + return ( + , + type: 'default', + }} + buttonStyle={{ padding: `0 4px` }} + drawerProps={{ + destroyOnClose: true, + title: `Code of "${path}"`, + }} + > + ({ + ...e, + tab: TAB_LAB_MAP[e.tab], + key: e.tab, + }))} + activeTabKey={tab} + onTabChange={(v) => setTab(v)} + tabBarExtraContent={ + Explain} + content={ + <> +
+
+ source: + {TAB_MAP.source} +
+
+ transformed: + {TAB_MAP.transformed} +
+
+ parsedSource: + {TAB_MAP.parsedSource} +
+
+ {'More'} + {t('CodeModeExplain')} +
+ + } + trigger={'hover'} + > + Explain +
+ } + > + + {(source) => { + return ( + + ); + }} + +
+
+ ); +}; + +// export const ModuleGraphViewer: React.FC<{ +// id: number | string; +// show: boolean; +// setShow: (_show: boolean) => void; +// cwd: string; +// }> = ({ id, show, setShow, cwd }) => { +// if (!id) return null; + +// return ( +// setShow(false)}> +// +// {(modules) => } +// +// +// ); +// }; + +const inlinedResourcePathKey = '__RESOURCEPATH__'; + +export function getChildrenModule(node: DataNode) { + const mods: string[] = []; + + node.children && + node.children.forEach((n: DataNode) => { + if (n.isLeaf) { + mods.push(n[inlinedResourcePathKey]); + } else { + getChildrenModule(n); + } + }); + + return mods; +} + +export const ModulesStatistics: React.FC<{ + modules: SDK.ModuleData[]; + chunks: SDK.ChunkData[]; + filteredModules: SDK.ModuleData[]; +}> = ({ modules, chunks, filteredModules }) => { + const { sourceSize, parsedSize, filteredParsedSize, filteredSourceSize } = useMemo(() => { + return { + sourceSize: sumBy(modules, (e) => e.size.sourceSize), + parsedSize: sumBy(modules, (e) => e.size.parsedSize), + filteredSourceSize: sumBy(filteredModules, (e) => e.size.sourceSize), + filteredParsedSize: sumBy(filteredModules, (e) => e.size.parsedSize), + }; + }, [modules, filteredModules]); + + return ( + + + + + Modules: {filteredModules.length} / {modules.length} + + + + + + + + Total modules parsed size: {formatSize(parsedSize)} + + + Total modules source size: {formatSize(sourceSize)} + + + Filtered modules parsed size: {formatSize(filteredParsedSize)} + + + Filtered modules source size: {formatSize(filteredSourceSize)} + + + } + > + + + Modules Size:{' '} + {filteredParsedSize === parsedSize + ? formatSize(parsedSize) + : `${formatSize(filteredParsedSize)} / ${formatSize(parsedSize)}`} + + + + + + + this asset includes {chunks.length} chunks: + {chunks.map((e) => ( + + ))} + + } + > + + + Chunks: {chunks.length} + + + + + + ); +}; + +export const AssetDetail: React.FC<{ + asset: SDK.AssetData; + chunks: SDK.ChunkData[]; + modules: SDK.ModuleData[]; + moduleSizeLimit?: number; + height?: number; + root: string; +}> = ({ asset, chunks: includeChunks, modules: includeModules, moduleSizeLimit, height }) => { + // const navigate = useNavigate(); + const [moduleKeyword, setModuleKeyword] = useState(''); + const [defaultExpandAll, setDefaultExpandAll] = useState(false); + const [moduleJumpList, setModuleJumpList] = useState([] as number[]); + const [_show, setShow] = useState(false); + + const filteredModules = useMemo(() => { + let res = includeModules.slice(); + if (moduleKeyword) { + const regexp = new RegExp(moduleKeyword, 'i'); + res = res.filter((e) => regexp.test(e.path)); + } + + if (moduleSizeLimit) { + res = res.filter((e) => e.size.parsedSize >= moduleSizeLimit); + } + + return res; + }, [includeModules, moduleKeyword, moduleSizeLimit]); + + const avgSize = sumBy(includeModules, (e) => e.size.parsedSize || 0) / includeModules.length; + + const fileStructures = useMemo(() => { + const res = createFileStructures({ + files: filteredModules.map((e) => e.path).filter(Boolean), + inlinedResourcePathKey, + fileTitle(file, basename) { + const mod = filteredModules.find((e) => e.path === file)!; + + if (!mod) return basename; + + const { parsedSize = 0, sourceSize = 0 } = mod.size; + const isConcatenation = mod.kind === SDK.ModuleKind.Concatenation; + + const containedOtherModules = + !isConcatenation && + parsedSize === 0 && + includeModules.filter((e) => e !== mod && e.modules && e.modules.indexOf(mod.id) > -1); + + return ( + + + {parsedSize !== 0 ? ( + = avgSize ? 'error' : 'default'} + tooltip={ + + + + + } + /> + ) : sourceSize !== 0 ? ( + // fallback to display tag for source size + + ) : null} + {isConcatenation ? ( + + + this is a concatenated module, it contains {mod.modules?.length} modules + + + } + > + concatenated + + ) : null} + {containedOtherModules && containedOtherModules.length ? ( + + + this is a concatenated module, it is be contained in these modules below: + + {containedOtherModules.map(({ id, path }) => { + if (isJsDataUrl(path)) { + return ( + + {path} + + ); + } + + const p = relative(dirname(mod.path), path); + + if (p.startsWith('javascript;charset=utf-8;base64,')) { + return ( + + {p[0] === '.' ? p : `./${p}`} + + ); + } + + return ( + + {p[0] === '.' ? p : `./${p}`} + + ); + })} + + } + > + concatenated + + ) : null} + + + + ); + }, + dirTitle(dir, defaultTitle) { + const paths = getChildrenModule(dir); + if (paths.length) { + const mods = paths.map((e) => includeModules.find((m) => m.path === e)!); + const parsedSize = sumBy(mods, (e) => e.size?.parsedSize || 0); + return ( + + {defaultTitle} + {parsedSize > 0 ? ( + = avgSize ? 'error' : 'default'} + /> + ) : null} + + ); + } + + return defaultTitle; + }, + }); + return res; + }, [filteredModules]); + + const onSearch = (value: string) => setModuleKeyword(value); + + useEffect(() => { + setModuleKeyword(''); + setDefaultExpandAll(false); + }, [asset]); + + useEffect(() => { + setDefaultExpandAll(false); + }, [moduleKeyword]); + + return ( + + + {includeModules.length ? ( + + + + + + + + + + + + {filteredModules.length ? ( + { + expanedModulesKeys = expandedKeys; + }} + treeData={fileStructures} + autoExpandParent + defaultExpandParent + defaultExpandedKeys={ + expanedModulesKeys?.length + ? expanedModulesKeys + : fileStructures.length === 1 + ? [fileStructures[0].key] + : [] + } + key={`tree_${moduleKeyword}_${defaultExpandAll}_${asset.path}`} + defaultExpandAll={defaultExpandAll || filteredModules.length <= 20} + /> + ) : ( + {`"${moduleKeyword}" can't match any modules`}} + /> + )} + + + ) : ( + {`"${asset.path}" don't has any modules`}} /> + )} + + {/* */} + + + ); +}; diff --git a/packages/components/src/pages/BundleSize/components/cards.tsx b/packages/components/src/pages/BundleSize/components/cards.tsx new file mode 100644 index 00000000..dcdefbec --- /dev/null +++ b/packages/components/src/pages/BundleSize/components/cards.tsx @@ -0,0 +1,143 @@ +/* eslint-disable react/jsx-key */ +import React, { useState } from 'react'; +import { Col, Row, Segmented } from 'antd'; +import { Client, SDK } from '@rsdoctor/types'; +import { useDuplicatePackagesByErrors } from '../../../utils'; +import { Size } from '../../../constants'; +import { StatisticCard } from '../../../components/Card/statistic'; +import { DuplicatePackageDrawerWithServer } from '../../../components/TextDrawer'; +import { SizeCard } from '../../../components/Card/size'; + +const height = 100; + +interface CardProps { + showProgress?: boolean; + data: Client.DoctorClientAssetsSummary['all']['total']; + total: number; +} + +const AssetCard: React.FC = ({ showProgress = false, data, total }) => { + return ; +}; + +const AssetCardContainer: React.FC<{ titles: string[]; datas: CardProps[] }> = ({ titles, datas }) => { + const [idx, setIdx] = useState(0); + + return ( + 1 ? ( + { + setIdx(titles.indexOf(e as string)); + }} + size="small" + style={{ transition: 'transform 0.3s ease' }} + value={titles[idx] || titles[0]} + /> + ) : ( + titles[idx] + ) + } + value={datas.map((e, i) => )[idx]} + /> + ); +}; + +export const BundleCards: React.FC<{ + cwd: string; + errors: SDK.ErrorsData; + summary: SDK.ServerAPI.InferResponseType; +}> = ({ cwd, errors, summary }) => { + const duplicatePackages = useDuplicatePackagesByErrors(errors); + + const arr = [ + , + , + , + , + , + + } + />, + ]; + + return ( + + {arr.map((e, i) => ( + + {e} + + ))} + + ); +}; diff --git a/packages/components/src/pages/BundleSize/components/editor.tsx b/packages/components/src/pages/BundleSize/components/editor.tsx new file mode 100644 index 00000000..0db77e58 --- /dev/null +++ b/packages/components/src/pages/BundleSize/components/editor.tsx @@ -0,0 +1,44 @@ +import { LoadingOutlined } from '@ant-design/icons'; +import Editor from '@monaco-editor/react'; +import { Card } from 'antd'; +import { getOriginalLanguage, getShortPath } from '../../../utils'; + + +export interface CodeEditorProps { + content?: string; + path: string; +} + +export function CodeEditor(props: CodeEditorProps) { + const { content, path } = props; + + if (!content) { + return
No Code~
; + } + + return ( + + } + options={{ + readOnly: true, + domReadOnly: true, + fontSize: 12, + renderValidationDecorations: 'off', + hideCursorInOverviewRuler: true, + smoothScrolling: true, + wordWrap: 'bounded', + colorDecorators: true, + codeLens: false, + cursorWidth: 0, + minimap: { + enabled: false, + }, + }} + /> + + ); +} diff --git a/packages/components/src/pages/BundleSize/components/index.scss b/packages/components/src/pages/BundleSize/components/index.scss new file mode 100644 index 00000000..3e574a7f --- /dev/null +++ b/packages/components/src/pages/BundleSize/components/index.scss @@ -0,0 +1,26 @@ +$prefixCls: 'bundle-size'; + +.monaco-hover .markdown-hover.hover-row hr { + margin: 2px; + background-color: gray; +} + +.#{$prefixCls}-editor { + margin-left: 40px; + flex-grow: 1; + height: 700px; + + .ant-card-body { + width: auto; + height: calc(100% - 48px); + padding: 0; + } +} + +.#{$prefixCls}-module-bunton { + margin-left: 4em; +} + +.ant-tabs-nav { + width: 100%; +} diff --git a/packages/components/src/pages/BundleSize/components/index.tsx b/packages/components/src/pages/BundleSize/components/index.tsx new file mode 100644 index 00000000..f0eeb880 --- /dev/null +++ b/packages/components/src/pages/BundleSize/components/index.tsx @@ -0,0 +1,390 @@ +import { ColumnHeightOutlined, InfoCircleOutlined, VerticalAlignMiddleOutlined } from '@ant-design/icons'; +import { Client, SDK } from '@rsdoctor/types'; +import { + Button, + Card, + Col, + Divider, + Empty, + InputNumber, + Radio, + Row, + Select, + Space, + Tag, + Tooltip, + Typography, +} from 'antd'; +import { debounce, includes, sumBy } from 'lodash-es'; +import React, { useCallback, useMemo, useState } from 'react'; +import { ServerAPIProvider, withServerAPI } from '../../../components/Manifest'; +import { Badge as Bdg } from '../../../components/Badge'; +import { FileTree } from '../../../components/FileTree'; +import { KeywordInput } from '../../../components/Form/keyword'; +import { Keyword } from '../../../components/Keyword'; +import { Title } from '../../../components/Title'; +import { Size } from '../../../constants'; +import { createFileStructures, formatSize, useI18n } from '../../../utils'; +import { BundleCards } from './cards'; +import { CodeViewerWithDrawer } from '../../../components/CodeViewer'; + +import { AssetDetail } from './asset'; +import './index.sass'; +import { GraphType } from '../constants'; + +const { Option } = Select; + +const cardBodyHeight = 410; + +const largeCardBodyHeight = 800; + +interface WebpackModulesOverallProps { + cwd: string; + errors: SDK.ErrorsData; + summary: Client.DoctorClientAssetsSummary; + entryPoints: SDK.ServerAPI.InferResponseType; +} + +export const WebpackModulesOverallBase: React.FC = ({ + errors, + cwd, + summary, + entryPoints, +}) => { + const [selectedEntryPoints, setEntryPoints] = useState([]); + const [inputModule, setModuleValue] = useState(0); + const [inputAssetName, setAssetName] = useState(''); + const [inputAssetSize, setAssetSize] = useState(0); + const [defaultExpandAll, setDefaultExpandAll] = useState(false); + const [inputModuleUnit, setModuleUnit] = useState(''); + const [inputChunkUnit, setChunkUnit] = useState(''); + const [assetPath, setAssetPath] = useState(null); + const [fold, setFold] = useState(false as Boolean); + const [graphType, setGraphType] = useState('tree' as GraphType); + + const { t } = useI18n(); + + const assets = summary.all.total.files; + const avgAssetSize = summary.all.total.size / assets.length; + + const handleChange = useCallback( + (type: string) => (value: string) => { + if (type === 'module') { + setModuleUnit(value); + } else if (type === 'chunk') { + setChunkUnit(value); + } + }, + [], + ); + + const selectAfter = (type: string) => ( + + ); + const onChangeModule = useCallback( + debounce((newValue: number) => { + const count = inputModuleUnit === 'mb' ? newValue * 1024 * 1024 : newValue * 1024; + setModuleValue(count); + }, 300), + [], + ); + + const onChangeAsset = useCallback( + debounce((newValue: number) => { + const count = inputChunkUnit === 'mb' ? newValue * 1024 * 1024 : newValue * 1024; + setAssetSize(count); + }, 300), + [], + ); + + const filteredAssets = useMemo(() => { + let res = assets.slice(); + + if (inputAssetName) { + res = res.filter((e) => e.path.indexOf(inputAssetName) > -1); + } + + if (inputAssetSize > 0) { + res = res.filter((e) => e.size >= inputAssetSize); + } + + if (selectedEntryPoints.length) { + res = res.filter((e) => { + if (selectedEntryPoints.some((ep) => includes(ep.assets, e.path))) { + return true; + } + return false; + }); + } + + return res.sort((a, b) => { + const _a = a.path.indexOf('/') > -1 ? 1 : 0; + const _b = b.path.indexOf('/') > -1 ? 1 : 0; + // return _a - _b; + return _b - _a; + }); + }, [assets, selectedEntryPoints, inputAssetName, inputAssetSize]); + + const assetsStructures = useMemo(() => { + const res = createFileStructures({ + files: filteredAssets.map((e) => e.path).filter(Boolean), + fileTitle(file, basename) { + const target = filteredAssets.find((e) => e.path === file)!; + const { size, initial, path, content } = target; + + return ( + { + setAssetPath(path); + }} + > + + = avgAssetSize ? 'error' : 'default'} /> + {initial ? ( + + initial + + ) : null} + + + ); + }, + }); + return res; + }, [filteredAssets]); + + const onSearch = (value: string) => { + setAssetName(value); + setDefaultExpandAll(false); + }; + + return ( + + + + setGraphType(e.target.value)} + style={{ marginBottom: Size.BasePadding }} + buttonStyle="solid" + optionType="button" + > + Tree Graph + {/* Bundle Analyzer Graph */} + + {/* TODO: tile graph */} + {/*