🌳
pt0/deployF/ptDeployActions/npmweightActionAI.mts
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]) {
20 let match
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)
26 }
27 }
28 return npmImports
31const getPkgSize = (pkgName: string) => {
32 const pkgPath = pathDownJoin(ptDir, 'node_modules', pkgName)
33 try {
34 const stat = fs.statSync(pkgPath)
35 if (!stat.isDirectory()) return 0
36 return getDirSize(pkgPath)
37 } catch { return 0 }
40const getDirSize = (dirPath: string): number => {
41 let total = 0
42 try {
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
48 }
49 } catch {}
50 return total
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`
58 return `${bytes}B`
61export const npmweight: PtAction = async () => {
62 const {pathsA} = await getRakeEpsPaths({defaultEpScope: epScopes.dockerjs})
64 const pkgToFiles: Record<string, Set<string>> = {}
65 for (const ptPath of pathsA) {
66 try {
67 const contents = await read1File(pathDownJoin(ptDir, ptPath))
68 const npmImports = extractNpmImports(contents)
69 for (const pkg of npmImports) {
70 pkgToFiles[pkg] ||= new Set()
71 pkgToFiles[pkg].add(ptPath)
72 }
73 } catch {}
74 }
76 const pkgStats = _.map(pkgToFiles, (files, pkg) => ({
77 pkg,
78 fileCount: files.size,
79 size: getPkgSize(pkg),
80 }))
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}`)
96 }
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))}`)
100 console.log('')
101 printEpScopeHint({defaultEpScope: epScopes.dockerjs})
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'