1import * as _ from 'lodash-es' 9import * as fs from 'fs' 10import path from 'path' 13export const extractNpmImports = (contents: string) => { 14 const npmImports = new Set<string>() 15 const importRegex = /(?:import|export).*from\s+['"]([^'"./][^'"]*)['"]/g 16 const dynamicRegex = /import\s*\(\s*['"]([^'"./][^'"]*)['"]\s*\)/g 17 const requireRegex = /require\s*\(\s*['"]([^'"./][^'"]*)['"]\s*\)/g 19 for (const regex of [importRegex, dynamicRegex, requireRegex]) { 21 while ((match = regex.exec(contents)) !== null) { 22 const pkgName = match[1].startsWith('@') 23 ? match[1].split('/').slice(0, 2).join('/') 24 : match[1].split('/')[0] 25 npmImports.add(pkgName) 31const getPkgSize = (pkgName: string) => { 34 const stat = fs.statSync(pkgPath) 35 if (!stat.isDirectory()) return 0 36 return getDirSize(pkgPath) 40const getDirSize = (dirPath: string): number => { 43 const entries = fs.readdirSync(dirPath, { withFileTypes: true }) 44 for (const entry of entries) { 45 const fullPath = path.join(dirPath, entry.name) 46 if (entry.isDirectory()) total += getDirSize(fullPath) 47 else if (entry.isFile()) total += fs.statSync(fullPath).size 53const requiredPkgs = new Set(['next', 'react', 'react-dom', '@chakra-ui/react', 'lodash-es', 'graphql']) 55const fmtSize = (bytes: number) => { 56 if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)}M` 57 if (bytes >= 1024) return `${(bytes / 1024).toFixed(0)}K` 61export const npmweight: PtAction = async () => { 64 const pkgToFiles: Record<string, Set<string>> = {} 65 for (const ptPath of pathsA) { 68 const npmImports = extractNpmImports(contents) 69 for (const pkg of npmImports) { 70 pkgToFiles[pkg] ||= new Set() 71 pkgToFiles[pkg].add(ptPath) 76 const pkgStats = _.map(pkgToFiles, (files, pkg) => ({ 78 fileCount: files.size, 79 size: getPkgSize(pkg), 82 const sorted = _.orderBy(pkgStats, ['size'], ['desc']) 83 const totalSize = _.sumBy(sorted, 'size') 85 console.log(`\n${'Package'.padEnd(35)} ${'Files'.padStart(6)} ${'Size'.padStart(8)}`) 86 console.log(`${'─'.repeat(35)} ${'─'.repeat(6)} ${'─'.repeat(8)}`) 88 for (const {pkg, fileCount, size} of sorted) { 89 if (size === 0) continue 90 const sizeStr = fmtSize(size) 91 const isHeavy = size > 50 * 1024 * 1024, isMedium = size > 10 * 1024 * 1024, isLight = size < 1024 * 1024 92 const colorFn = isHeavy ? chalkRed : isLight ? chalkGray : chalkYellow 93 const heavyPerFile = fileCount > 0 && (size / fileCount) > 5 * 1024 * 1024 94 const flag = ((isHeavy || isMedium) && heavyPerFile && !requiredPkgs.has(pkg)) ? chalkRed(' ⚠tree-shake?') : '' 95 console.log(`${pkg.padEnd(35)} ${String(fileCount).padStart(6)} ${colorFn(sizeStr.padStart(8))}${flag}`) 98 console.log(`${'─'.repeat(35)} ${'─'.repeat(6)} ${'─'.repeat(8)}`) 99 console.log(`${'Total'.padEnd(35)} ${String(sorted.length).padStart(6)} ${chalkGreen(fmtSize(totalSize).padStart(8))}`) 104npmweight.cliSchema = scopeCli 105npmweight.cliAdvanced = true 106npmweight.cliDescript = { 107 cliDefaultOpt: epScopes.dockerjs, 108 cliOptA: getEpScopeOptA, 109 cliSupportsEpScopes: true, 110 cliExplain: 'show npm package sizes imported by entrypoint'