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.

at trunk 189 lines 6.7 kB view raw
1import { sample, sampleOne } from '@mary/array-fns'; 2import { createMemo, Match, Switch } from 'solid-js'; 3import * as v from 'valibot'; 4 5import PackageResult from './components/package-result'; 6import PackageSearchInput from './components/package-search-input'; 7import { CentralExclamationTriangleSolid } from './icons/central'; 8import { 9 LucideArrowDown, 10 LucideCircleAlert, 11 LucideHandHeart, 12 LucideLoader, 13 LucideScissorsLineDashed, 14} from './icons/lucide'; 15import { TangledDolly } from './icons/tangled'; 16import { 17 formatPackageSpecifier, 18 PACKAGE_SPECIFIER_RE, 19 parsePackageSpecifier, 20 type Registry, 21} from './lib/package-name'; 22import { createQuery } from './lib/query'; 23import { createDerivedSignal } from './lib/signals'; 24import { useSearchParams } from './lib/use-search-params'; 25import { fetchPackageManifest } from './npm/packument'; 26import Button from './primitives/button'; 27import Tooltip from './primitives/tooltip'; 28import { RECOMMENDATIONS } from './recommendations'; 29 30const isSafari = (() => { 31 const ua = navigator.userAgent; 32 return /AppleWebKit/.test(ua) && !/Chrome|Chromium/.test(ua); 33})(); 34 35function App() { 36 const [params, setParams] = useSearchParams({ 37 q: v.pipe(v.string(), v.regex(PACKAGE_SPECIFIER_RE)), 38 }); 39 40 const parsed = createMemo(() => { 41 const q = params().q; 42 return q ? parsePackageSpecifier(q) : null; 43 }); 44 45 const identity = createMemo<{ registry: Registry; name: string } | undefined>( 46 () => { 47 const p = parsed(); 48 return p ? { registry: p.registry, name: p.name } : undefined; 49 }, 50 undefined, 51 { equals: (a, b) => a?.registry === b?.registry && a?.name === b?.name }, 52 ); 53 54 const range = createMemo(() => parsed()?.range ?? 'latest'); 55 56 const [query, setQuery] = createDerivedSignal(() => params().q ?? ''); 57 58 const [manifest, { refetch }] = createQuery(identity, (id) => fetchPackageManifest(id.registry, id.name), { 59 keepPreviousData: false, 60 }); 61 62 const recs = sample(RECOMMENDATIONS, 6) 63 .flatMap((v) => (Array.isArray(v) ? sampleOne(v) : v)) 64 // oxlint-disable-next-line unicorn/no-array-sort 65 .sort(); 66 67 return ( 68 <div class="mx-auto flex max-w-xl flex-col gap-6 p-4"> 69 <div class="flex h-12 items-center justify-between gap-1 rounded-lg border border-neutral-stroke-3 bg-neutral-background-1 px-4"> 70 <div class="flex shrink-0 items-center gap-2"> 71 <LucideScissorsLineDashed class="size-5 text-brand-foreground-2" /> 72 <h1 class="text-base-400 font-medium">teardown</h1> 73 </div> 74 75 <div class="-mr-2 flex items-center gap-1"> 76 <Tooltip content="Donate!" relationship="label" placement="bottom"> 77 {(triggerProps) => ( 78 <a 79 {...triggerProps} 80 target="_blank" 81 href="https://github.com/sponsors/mary-ext" 82 class="grid h-8 w-8 shrink-0 place-items-center rounded-md border border-transparent bg-subtle-background text-neutral-foreground-2 outline-2 -outline-offset-2 outline-transparent transition duration-100 hover:bg-subtle-background-hover focus-visible:outline-compound-brand-stroke active:bg-subtle-background-pressed" 83 > 84 <LucideHandHeart class="size-4" /> 85 </a> 86 )} 87 </Tooltip> 88 89 <Tooltip content="Source code on tangled.org" relationship="label" placement="bottom"> 90 {(triggerProps) => ( 91 <a 92 {...triggerProps} 93 target="_blank" 94 href="https://tangled.org/did:plc:ia76kvnndjutgedggx2ibrem/teardown" 95 class="grid h-8 w-8 shrink-0 place-items-center rounded-md border border-transparent bg-subtle-background text-neutral-foreground-2 outline-2 -outline-offset-2 outline-transparent transition duration-100 hover:bg-subtle-background-hover focus-visible:outline-compound-brand-stroke active:bg-subtle-background-pressed" 96 > 97 <TangledDolly class="size-4" /> 98 </a> 99 )} 100 </Tooltip> 101 </div> 102 </div> 103 104 <div class="flex min-h-0 grow flex-col gap-4 sm:px-4"> 105 <PackageSearchInput 106 autofocus={/* @once */ !query()} 107 value={query()} 108 onChange={setQuery} 109 onSelect={(specifier) => setParams({ q: specifier })} 110 /> 111 112 {isSafari && ( 113 <div class="flex gap-2 rounded-md border border-status-warning-border-1 bg-status-warning-background-1 px-3 py-1.75"> 114 <CentralExclamationTriangleSolid class="size-5 shrink-0 text-status-warning-foreground-3" /> 115 116 <div class="min-w-0 grow text-base-300 text-neutral-foreground-1"> 117 <span class="font-semibold">Not compatible with Safari.</span> Sorry, not sure why it doesn't 118 work there. It seems to be Rolldown and WASI related. 119 </div> 120 </div> 121 )} 122 123 <Switch> 124 <Match when={manifest()} keyed> 125 {(m) => ( 126 <PackageResult 127 manifest={m} 128 range={range()} 129 onVersionChange={(version) => { 130 setParams({ 131 q: formatPackageSpecifier({ registry: m.registry, name: m.name, range: version }), 132 }); 133 }} 134 /> 135 )} 136 </Match> 137 138 <Match when={manifest.state === 'errored'}> 139 <div class="flex flex-col items-center justify-center gap-3 py-12"> 140 <LucideCircleAlert class="text-danger-foreground-1 size-5" /> 141 <span class="text-base-300 text-neutral-foreground-2">{manifest.error?.message}</span> 142 <Button appearance="subtle" onClick={() => refetch()}> 143 Retry 144 </Button> 145 </div> 146 </Match> 147 148 <Match when={manifest.loading}> 149 <div class="flex flex-col items-center justify-center gap-3 py-12"> 150 <LucideLoader class="size-5 animate-spin-linear text-neutral-foreground-3" /> 151 <span class="text-base-300 text-neutral-foreground-2">Loading...</span> 152 </div> 153 </Match> 154 155 <Match when> 156 <div class="flex flex-col gap-4"> 157 <div> 158 <p class="text-base-300 text-neutral-foreground-2"> 159 Find the cost of adding an npm package to your app's bundle size. <br /> 160 Makes use of <a>Rolldown</a> to bundle packages in your browser. 161 </p> 162 </div> 163 164 <div class="flex flex-col gap-3"> 165 <p class="text-base-200 font-bold text-neutral-foreground-4 uppercase">Example packages</p> 166 167 {recs.map((name) => { 168 return ( 169 <div class="flex items-center gap-3"> 170 <LucideArrowDown class="size-4 rotate-270 text-neutral-foreground-3" /> 171 <button 172 onClick={() => setParams({ q: `npm:${name}` })} 173 class="cursor-pointer text-base-300 text-brand-foreground-2 transition hover:text-brand-foreground-2-hover hover:underline active:text-brand-foreground-2-pressed" 174 > 175 {name} 176 </button> 177 </div> 178 ); 179 })} 180 </div> 181 </div> 182 </Match> 183 </Switch> 184 </div> 185 </div> 186 ); 187} 188 189export default App;