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: clean things up

Mary b9f10703 bbb6eb74

+56 -130
+5 -14
src/components/package-bundle.tsx
··· 1 + import { dequal } from 'dequal'; 1 2 import { createMemo, createSignal, For, Match, onCleanup, Show, Switch } from 'solid-js'; 2 3 3 4 import { LucideCheck, LucideChevronDown, LucideCircleAlert, LucideInfo, LucideLoader } from '../icons/lucide'; ··· 19 20 function serializeCacheKey(subpath: string, exports: string[] | null, excludePeers: boolean): string { 20 21 const base = exports === null ? subpath : `${subpath}\0${exports.join('\0')}`; 21 22 return excludePeers ? `${base}\0peers` : base; 22 - } 23 - 24 - function arraysEqual(a: string[], b: string[]): boolean { 25 - if (a.length !== b.length) { 26 - return false; 27 - } 28 - for (let i = 0; i < a.length; i++) { 29 - if (a[i] !== b[i]) { 30 - return false; 31 - } 32 - } 33 - return true; 34 23 } 35 24 36 25 /** sorts output: entry chunk first, then chunks alphabetically, then assets alphabetically */ ··· 143 132 144 133 const exports = selectedExports(); 145 134 // if selection equals all exports, pass null to reuse LRU cache 146 - const exportsParam = arraysEqual(exports, $initialBundle.exports) ? null : exports; 135 + const exportsParam = dequal(exports, $initialBundle.exports) ? null : exports; 147 136 148 137 return { subpath: $subpath, exports: exportsParam, excludePeers: props.excludePeers }; 149 138 }, ··· 179 168 180 169 const selectNone = () => setSelectedExports([]); 181 170 171 + const selectedSet = createMemo(() => new Set(selectedExports())); 172 + 182 173 const isExportSelected = (exportName: string) => { 183 - return selectedExports().includes(exportName); 174 + return selectedSet().has(exportName); 184 175 }; 185 176 186 177 return (
+1 -3
src/components/package-dependencies.tsx
··· 310 310 311 311 if (filterText) { 312 312 result = result.filter((pkg) => pkg.name.toLowerCase().includes(filterText)); 313 - } else { 314 - result = [...result]; 315 313 } 316 314 317 - return [...result].toSorted(sortConfig.compare); 315 + return result.toSorted(sortConfig.compare); 318 316 }); 319 317 320 318 return (
+2 -9
src/components/package-search-input.tsx
··· 4 4 import { modality } from '../lib/modality'; 5 5 import { formatPackageSpecifier, parsePackageSpecifier, type Registry } from '../lib/package-name'; 6 6 import { createQuery } from '../lib/query'; 7 + import { scrollIntoContainerView } from '../lib/scroll'; 7 8 import { createTrailingThrottle, makeAbortable } from '../lib/signals'; 8 9 import { normalizeWhitespace } from '../lib/strings'; 9 10 import Input from '../primitives/input'; ··· 261 262 ref={(el) => { 262 263 createEffect(() => { 263 264 if (activeIndex() === index() && modality() === 'keyboard' && listboxRef) { 264 - const padding = 4; // matches p-1 265 - const listboxRect = listboxRef.getBoundingClientRect(); 266 - const optionRect = el.getBoundingClientRect(); 267 - 268 - if (optionRect.top < listboxRect.top + padding) { 269 - listboxRef.scrollTop -= listboxRect.top + padding - optionRect.top; 270 - } else if (optionRect.bottom > listboxRect.bottom - padding) { 271 - listboxRef.scrollTop += optionRect.bottom - (listboxRect.bottom - padding); 272 - } 265 + scrollIntoContainerView(listboxRef, el); 273 266 } 274 267 }); 275 268 }}
+1 -1
src/lib/emitter.ts
··· 81 81 }, 82 82 emit(...args) { 83 83 if (listener === undefined) { 84 - return false; 84 + return; 85 85 } 86 86 if (typeof listener === 'function') { 87 87 listener.apply(this, args);
+18
src/lib/scroll.ts
··· 1 + /** 2 + * scrolls a container so that the given item is visible, with padding. 3 + * adjusts `scrollTop` minimally — only scrolls if the item is partially or fully outside view. 4 + * 5 + * @param container the scrollable container element 6 + * @param item the item element to scroll into view 7 + * @param padding pixels of padding inside the container edges 8 + */ 9 + export function scrollIntoContainerView(container: HTMLElement, item: HTMLElement, padding = 4): void { 10 + const containerRect = container.getBoundingClientRect(); 11 + const itemRect = item.getBoundingClientRect(); 12 + 13 + if (itemRect.top < containerRect.top + padding) { 14 + container.scrollTop -= containerRect.top + padding - itemRect.top; 15 + } else if (itemRect.bottom > containerRect.bottom - padding) { 16 + container.scrollTop += itemRect.bottom - (containerRect.bottom - padding); 17 + } 18 + }
+6 -36
src/npm/lib/bundler.ts
··· 1 - import { encodeUtf8, getUtf8Length } from '@atcute/uint8array'; 1 + import { encodeUtf8 } from '@atcute/uint8array'; 2 2 import { rolldown } from '@rolldown/browser'; 3 3 import { memfs } from '@rolldown/browser/experimental'; 4 4 ··· 44 44 } 45 45 46 46 /** 47 - * get compressed size of a string using a compression stream. 48 - */ 49 - function getCompressedSize(code: string, format: CompressionFormat): Promise<number> { 50 - return getCompressedSizeFromBytes(encodeUtf8(code), format); 51 - } 52 - 53 - /** 54 47 * get gzip size of raw bytes. 55 48 */ 56 49 function getGzipSizeFromBytes(data: Uint8Array): Promise<number> { ··· 58 51 } 59 52 60 53 /** 61 - * get gzip size using compression stream. 62 - */ 63 - function getGzipSize(code: string): Promise<number> { 64 - return getCompressedSize(code, 'gzip'); 65 - } 66 - 67 - /** 68 54 * whether brotli compression is supported. 69 55 * - `undefined`: not yet checked 70 56 * - `true`: supported ··· 100 86 } 101 87 102 88 /** 103 - * get brotli size using compression stream, if supported. 104 - * returns `undefined` if brotli is not supported by the browser. 105 - */ 106 - function getBrotliSize(code: string): Promise<number | undefined> { 107 - return getBrotliSizeFromBytes(encodeUtf8(code)); 108 - } 109 - 110 - /** 111 89 * whether native zstd compression is supported. 112 90 * - `undefined`: not yet checked 113 91 * - `true`: supported ··· 175 153 176 154 // @ts-expect-error 'zstd' is not in the type definition yet 177 155 return getCompressedSizeFromBytes(data, 'zstd'); 178 - } 179 - 180 - /** 181 - * get zstd size using compression stream if supported, or WASM fallback. 182 - * returns `undefined` if neither native nor WASM is available. 183 - */ 184 - function getZstdSize(code: string): Promise<number | undefined> { 185 - return getZstdSizeFromBytes(encodeUtf8(code)); 186 156 } 187 157 188 158 // #endregion ··· 300 270 301 271 const chunks: BundleChunk[] = await Promise.all( 302 272 rawChunks.map(async (chunk) => { 303 - const code = chunk.code; 304 - const size = getUtf8Length(code); 273 + const bytes = encodeUtf8(chunk.code); 274 + const size = bytes.byteLength; 305 275 const [gzipSize, brotliSize, zstdSize] = await Promise.all([ 306 - getGzipSize(code), 307 - getBrotliSize(code), 308 - getZstdSize(code), 276 + getGzipSizeFromBytes(bytes), 277 + getBrotliSizeFromBytes(bytes), 278 + getZstdSizeFromBytes(bytes), 309 279 ]); 310 280 311 281 return {
+1 -3
src/npm/lib/fetch.ts
··· 128 128 129 129 // ensure parent directories exist 130 130 const parentDir = fullPath.slice(0, fullPath.lastIndexOf('/')); 131 - if (!volume.existsSync(parentDir)) { 132 - volume.mkdirSync(parentDir, { recursive: true }); 133 - } 131 + volume.mkdirSync(parentDir, { recursive: true }); 134 132 135 133 volume.writeFileSync(fullPath, content); 136 134 }
+3 -1
src/npm/lib/registry.ts
··· 1 1 import * as v from 'valibot'; 2 2 3 + import type { Registry } from '../../lib/package-name'; 4 + 3 5 import { FetchError, InvalidSpecifierError, PackageNotFoundError } from './errors'; 4 - import { abbreviatedPackumentSchema, type AbbreviatedPackument, type Registry } from './types'; 6 + import { abbreviatedPackumentSchema, type AbbreviatedPackument } from './types'; 5 7 6 8 const NPM_REGISTRY = 'https://registry.npmjs.org'; 7 9 const JSR_REGISTRY = 'https://npm.jsr.io';
+9 -40
src/npm/lib/resolve.ts
··· 1 1 import * as semver from 'semver'; 2 2 3 + import { parsePackageSpecifier } from '../../lib/package-name'; 4 + import type { Registry } from '../../lib/package-name'; 3 5 import { progress } from '../events'; 4 6 5 7 import { InvalidSpecifierError, NoMatchingVersionError } from './errors'; 6 8 import { fetchPackument, reverseJsrName } from './registry'; 7 - import type { 8 - AbbreviatedManifest, 9 - PackageSpecifier, 10 - Registry, 11 - ResolvedPackage, 12 - ResolutionResult, 13 - } from './types'; 9 + import type { AbbreviatedManifest, PackageSpecifier, ResolvedPackage, ResolutionResult } from './types'; 14 10 15 11 /** 16 12 * parses a package specifier string into name, range, and registry. ··· 26 22 * @returns parsed specifier with name, range, and registry 27 23 */ 28 24 export function parseSpecifier(spec: string): PackageSpecifier { 29 - let registry: Registry = 'npm'; 30 - let rest = spec; 31 - 32 - // check for registry prefixes 33 - if (spec.startsWith('jsr:')) { 34 - registry = 'jsr'; 35 - rest = spec.slice(4); // remove "jsr:" 36 - } else if (spec.startsWith('npm:')) { 37 - rest = spec.slice(4); // remove "npm:", registry already 'npm' 25 + const parsed = parsePackageSpecifier(spec); 26 + if (!parsed) { 27 + throw new InvalidSpecifierError(spec, `invalid package specifier`); 38 28 } 39 - 40 - // handle scoped packages: @scope/name or @scope/name@version 41 - if (rest.startsWith('@')) { 42 - const slashIdx = rest.indexOf('/'); 43 - if (slashIdx === -1) { 44 - throw new InvalidSpecifierError(spec, 'scoped package missing slash'); 45 - } 46 - const atIdx = rest.indexOf('@', slashIdx); 47 - if (atIdx === -1) { 48 - return { name: rest, range: 'latest', registry }; 49 - } 50 - return { name: rest.slice(0, atIdx), range: rest.slice(atIdx + 1), registry }; 29 + if (parsed.registry === 'jsr' && !parsed.name.startsWith('@')) { 30 + throw new InvalidSpecifierError(spec, `JSR packages must be scoped`); 51 31 } 52 - 53 - // JSR packages must be scoped 54 - if (registry === 'jsr') { 55 - throw new InvalidSpecifierError(spec, 'JSR packages must be scoped'); 56 - } 57 - 58 - // handle regular packages: name or name@version 59 - const atIdx = rest.indexOf('@'); 60 - if (atIdx === -1) { 61 - return { name: rest, range: 'latest', registry }; 62 - } 63 - return { name: rest.slice(0, atIdx), range: rest.slice(atIdx + 1), registry }; 32 + return parsed; 64 33 } 65 34 66 35 /**
+2 -4
src/npm/lib/subpaths.ts
··· 74 74 /** 75 75 * recursively lists all files in a directory. 76 76 */ 77 - function listFilesRecursive(volume: Volume, dir: string): string[] { 78 - const files: string[] = []; 79 - 77 + function listFilesRecursive(volume: Volume, dir: string, files: string[] = []): string[] { 80 78 try { 81 79 const entries = volume.readdirSync(dir, { withFileTypes: true }); 82 80 for (const entry of entries) { 83 81 const fullPath = `${dir}/${entry.name}`; 84 82 if (entry.isDirectory()) { 85 - files.push(...listFilesRecursive(volume, fullPath)); 83 + listFilesRecursive(volume, fullPath, files); 86 84 } else if (entry.isFile()) { 87 85 files.push(fullPath); 88 86 }
+2 -5
src/npm/lib/types.ts
··· 1 1 import * as v from 'valibot'; 2 2 3 + import type { Registry } from '../../lib/package-name'; 4 + 3 5 // #region package.json schema 4 6 5 7 /** ··· 144 146 /** resolved dependencies (name -> ResolvedPackage) */ 145 147 dependencies: Map<string, ResolvedPackage>; 146 148 } 147 - 148 - /** 149 - * supported package registries. 150 - */ 151 - export type Registry = 'npm' | 'jsr'; 152 149 153 150 /** 154 151 * the input to the resolver - a package specifier.
+1 -1
src/primitives/dropdown/context.tsx
··· 20 20 /** currently selected value */ 21 21 selectedValue: Accessor<string | undefined>; 22 22 /** select an option by value */ 23 - selectOption: (value: string, label: string) => void; 23 + selectOption: (value: string) => void; 24 24 /** active descendant controller for keyboard navigation */ 25 25 activeDescendant: ActiveDescendantController; 26 26 /** the listbox element ref for scrolling */
+3 -11
src/primitives/dropdown/dropdown-option.tsx
··· 2 2 3 3 import { LucideCheck } from '../../icons/lucide'; 4 4 import { modality } from '../../lib/modality'; 5 + import { scrollIntoContainerView } from '../../lib/scroll'; 5 6 6 7 import { useDropdownContext } from './context'; 7 8 ··· 31 32 if (props.disabled) { 32 33 return; 33 34 } 34 - const label = typeof props.children === 'string' ? props.children : props.value; 35 - ctx.selectOption(props.value, label); 35 + ctx.selectOption(props.value); 36 36 }; 37 37 38 38 const handleMouseMove = () => { ··· 62 62 createEffect(() => { 63 63 const listbox = ctx.listboxRef(); 64 64 if (isActive() && modality() === 'keyboard' && listbox) { 65 - const padding = 4; 66 - const listboxRect = listbox.getBoundingClientRect(); 67 - const optionRect = el.getBoundingClientRect(); 68 - 69 - if (optionRect.top < listboxRect.top + padding) { 70 - listbox.scrollTop -= listboxRect.top + padding - optionRect.top; 71 - } else if (optionRect.bottom > listboxRect.bottom - padding) { 72 - listbox.scrollTop += optionRect.bottom - (listboxRect.bottom - padding); 73 - } 65 + scrollIntoContainerView(listbox, el); 74 66 } 75 67 }); 76 68 }}
+1 -1
src/primitives/dropdown/dropdown-root.tsx
··· 48 48 const [internalValue, setInternalValue] = createSignal(props.defaultValue); 49 49 const selectedValue = createMemo(() => props.value ?? internalValue()); 50 50 51 - const selectOption = (value: string, _label: string) => { 51 + const selectOption = (value: string) => { 52 52 if (props.value === undefined) { 53 53 setInternalValue(value); 54 54 }
+1 -1
src/primitives/dropdown/dropdown-trigger.tsx
··· 127 127 if (activeId) { 128 128 const value = ctx.getOptionValue(activeId); 129 129 if (value !== undefined) { 130 - ctx.selectOption(value, value); 130 + ctx.selectOption(value); 131 131 } 132 132 } 133 133 break;