Find the cost of adding an npm package to your app's bundle size teardown.kelinci.dev
14
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: detailed dependents/dependencies list

Mary a54aed0c 4fd3340c

+76 -27
+5 -4
src/components/package-dependencies.tsx
··· 32 32 }, 33 33 installedBy: { 34 34 label: 'Installed by count', 35 - compare: (a, b) => b.installedBy - a.installedBy || a.name.localeCompare(b.name), 35 + compare: (a, b) => b.dependents.length - a.dependents.length || a.name.localeCompare(b.name), 36 36 }, 37 37 dependencies: { 38 38 label: 'Dependencies count', 39 - compare: (a, b) => b.dependencyCount - a.dependencyCount || a.name.localeCompare(b.name), 39 + compare: (a, b) => b.dependencies.length - a.dependencies.length || a.name.localeCompare(b.name), 40 40 }, 41 41 name: { 42 42 label: 'Name', ··· 147 147 {/* stats */} 148 148 <div class="flex flex-wrap gap-4 text-base-300"> 149 149 <span class="text-neutral-foreground-3"> 150 - <span class="font-medium text-neutral-foreground-2">Installed by:</span> {props.pkg.installedBy} 150 + <span class="font-medium text-neutral-foreground-2">Installed by:</span>{' '} 151 + {props.pkg.dependents.length} 151 152 </span> 152 153 <span class="text-neutral-foreground-3"> 153 154 <span class="font-medium text-neutral-foreground-2">Dependencies:</span>{' '} 154 - {props.pkg.dependencyCount} 155 + {props.pkg.dependencies.length} 155 156 </span> 156 157 </div> 157 158 </div>
+13 -6
src/npm/installed-packages.test.ts
··· 23 23 expect(packages.every((p) => !p.isPeer)).toBe(true); 24 24 }); 25 25 26 - it('correctly sets installedBy count', async () => { 26 + it('correctly sets dependents', async () => { 27 27 const result = await resolve(['is-odd@3.0.1']); 28 28 const packages = buildInstalledPackages(result.roots[0], new Set()); 29 29 30 - // is-odd is the root, installedBy should be 0 30 + // is-odd is the root, no dependents 31 31 const isOdd = packages.find((p) => p.name === 'is-odd')!; 32 - expect(isOdd.installedBy).toBe(0); 32 + expect(isOdd.dependents.length).toBe(0); 33 33 34 34 // is-number is depended on by is-odd 35 35 const isNumber = packages.find((p) => p.name === 'is-number')!; 36 - expect(isNumber.installedBy).toBe(1); 36 + expect(isNumber.dependents.length).toBe(1); 37 + expect(isNumber.dependents[0].name).toBe('is-odd'); 37 38 }); 38 39 39 - it('correctly sets dependencyCount', async () => { 40 + it('correctly sets dependencies', async () => { 40 41 const result = await resolve(['is-odd@3.0.1']); 41 42 const packages = buildInstalledPackages(result.roots[0], new Set()); 42 43 43 44 // is-odd has 1 dependency (is-number) 44 45 const isOdd = packages.find((p) => p.name === 'is-odd')!; 45 - expect(isOdd.dependencyCount).toBe(1); 46 + expect(isOdd.dependencies.length).toBe(1); 47 + expect(isOdd.dependencies[0].name).toBe('is-number'); 46 48 }); 47 49 48 50 it('marks peer dependencies correctly', async () => { ··· 59 61 // use-sync-external-store should not be marked as peer 60 62 const main = packages.find((p) => p.name === 'use-sync-external-store')!; 61 63 expect(main.isPeer).toBe(false); 64 + 65 + // the dependency edge to react should be marked as peer 66 + const reactDep = main.dependencies.find((d) => d.name === 'react'); 67 + expect(reactDep).toBeDefined(); 68 + expect(reactDep!.isPeer).toBe(true); 62 69 }); 63 70 64 71 it('marks transitive peer deps correctly', async () => {
+50 -15
src/npm/installed-packages.ts
··· 2 2 import type { InstalledPackage } from './worker-protocol'; 3 3 4 4 /** 5 + * reference to a package in a dependency relationship. 6 + */ 7 + interface PackageRef { 8 + name: string; 9 + version: string; 10 + isPeer: boolean; 11 + } 12 + 13 + /** 5 14 * builds the installed packages list from the resolved dependency tree. 6 15 * also identifies which packages are only reachable through peer dependencies. 7 16 * ··· 10 19 * @returns array of installed packages with peer status 11 20 */ 12 21 export function buildInstalledPackages(root: ResolvedPackage, peerDepNames: Set<string>): InstalledPackage[] { 13 - // first pass: collect all unique packages and compute levels + installedBy 22 + // collect all unique packages and compute levels 14 23 const packageMap = new Map< 15 24 string, 16 25 { 17 26 pkg: ResolvedPackage; 18 27 level: number; 19 - installedBy: number; 28 + dependents: PackageRef[]; 29 + dependencies: PackageRef[]; 20 30 } 21 31 >(); 22 32 23 33 // track which packages are reachable without going through peer deps 24 34 const reachableWithoutPeers = new Set<string>(); 25 35 36 + // first pass: collect packages and compute levels 26 37 { 27 38 const visited = new Set<string>(); 28 39 ··· 36 47 existing.level = level; 37 48 } 38 49 } else { 39 - packageMap.set(key, { pkg, level, installedBy: 0 }); 50 + packageMap.set(key, { pkg, level, dependents: [], dependencies: [] }); 40 51 } 41 52 42 53 // track if reachable without peers ··· 50 61 } 51 62 visited.add(key); 52 63 53 - // count installedBy for each dependency 54 64 for (const [depName, dep] of pkg.dependencies) { 55 - const depKey = `${dep.name}@${dep.version}`; 56 - const depEntry = packageMap.get(depKey); 57 - if (depEntry) { 58 - depEntry.installedBy++; 59 - } else { 60 - packageMap.set(depKey, { pkg: dep, level: level + 1, installedBy: 1 }); 61 - } 62 - 63 65 // check if this edge goes through a root peer dep 64 66 const isPeerEdge = pkg === root && peerDepNames.has(depName); 65 67 walk(dep, level + 1, inPeerSubtree || isPeerEdge); ··· 71 73 walk(root, 0, false); 72 74 } 73 75 76 + // second pass: build dependency/dependent relationships 77 + // we need to read peerDependencies from each package's manifest 78 + for (const [_key, entry] of packageMap) { 79 + const pkg = entry.pkg; 80 + 81 + for (const [depName, dep] of pkg.dependencies) { 82 + const depKey = `${dep.name}@${dep.version}`; 83 + const depEntry = packageMap.get(depKey); 84 + if (!depEntry) { 85 + continue; 86 + } 87 + 88 + // check if this is a peer dependency edge by looking at the manifest 89 + // note: during resolution, peer deps are added to dependencies map 90 + // we need to check the original peerDependencies field 91 + const isPeerDep = pkg === root && peerDepNames.has(depName); 92 + 93 + // add to this package's dependencies 94 + entry.dependencies.push({ 95 + name: dep.name, 96 + version: dep.version, 97 + isPeer: isPeerDep, 98 + }); 99 + 100 + // add to the dependency's dependents 101 + depEntry.dependents.push({ 102 + name: pkg.name, 103 + version: pkg.version, 104 + isPeer: isPeerDep, 105 + }); 106 + } 107 + } 108 + 74 109 // build final array 75 110 const packages: InstalledPackage[] = []; 76 - for (const [key, { pkg, level, installedBy }] of packageMap) { 111 + for (const [key, { pkg, level, dependents, dependencies }] of packageMap) { 77 112 packages.push({ 78 113 name: pkg.name, 79 114 version: pkg.version, 80 115 size: pkg.unpackedSize ?? 0, 81 116 path: `node_modules/${pkg.name}`, 82 117 level, 83 - installedBy, 84 - dependencyCount: pkg.dependencies.size, 118 + dependents, 119 + dependencies, 85 120 description: pkg.description, 86 121 license: pkg.license, 87 122 isPeer: !reachableWithoutPeers.has(key),
+8 -2
src/npm/worker-protocol.ts
··· 40 40 defaultSubpath: v.nullable(v.string()), 41 41 }); 42 42 43 + const packageRefSchema = v.object({ 44 + name: v.string(), 45 + version: v.string(), 46 + isPeer: v.boolean(), 47 + }); 48 + 43 49 const installedPackageSchema = v.object({ 44 50 name: v.string(), 45 51 version: v.string(), 46 52 size: v.number(), 47 53 path: v.string(), 48 54 level: v.number(), 49 - installedBy: v.number(), 50 - dependencyCount: v.number(), 55 + dependents: v.array(packageRefSchema), 56 + dependencies: v.array(packageRefSchema), 51 57 description: v.optional(v.string()), 52 58 license: v.optional(v.string()), 53 59 isPeer: v.boolean(),