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: enable suspicious and perf lint rules

Mary b1f39c42 519f26a7

+74 -40
+12
.oxlintrc.json
··· 1 + { 2 + "$schema": "https://unpkg.com/oxlint/configuration_schema.json", 3 + "categories": { 4 + "correctness": "warn", 5 + "suspicious": "warn", 6 + "perf": "warn" 7 + }, 8 + "rules": { 9 + "unicorn/prefer-add-event-listener": "off", 10 + "unicorn/require-post-message-target-origin": "off" 11 + } 12 + }
+1
src/app.tsx
··· 53 53 54 54 const recs = sample(RECOMMENDATIONS, 6) 55 55 .flatMap((v) => (Array.isArray(v) ? sampleOne(v) : v)) 56 + // oxlint-disable-next-line unicorn/no-array-sort 56 57 .sort(); 57 58 58 59 return (
+5 -3
src/components/package-dependencies.tsx
··· 149 149 } 150 150 } 151 151 152 + // oxlint-disable-next-line typescript/no-confusing-non-null-assertion 152 153 curr[c]! = cost + bestPrevCost; 154 + // oxlint-disable-next-line typescript/no-confusing-non-null-assertion 153 155 parentRow[c]! = bestPrevColor; 154 156 } 155 157 ··· 172 174 color = parent[i]![color]!; 173 175 reversed.push(color); 174 176 } 175 - return reversed.reverse(); 177 + return reversed.toReversed(); 176 178 } 177 179 178 180 interface SizeBreakdownBarProps { ··· 183 185 const SizeBreakdownBar = (props: SizeBreakdownBarProps) => { 184 186 const segments = createMemo(() => { 185 187 // sort by size descending for the bar 186 - const sorted = [...props.packages].sort((a, b) => b.size - a.size); 188 + const sorted = [...props.packages].toSorted((a, b) => b.size - a.size); 187 189 188 190 // compute canonical colors, then resolve adjacent collisions 189 191 const canonicalIndices = sorted.map((pkg) => getCanonicalColorIndex(pkg.name)); ··· 312 314 result = [...result]; 313 315 } 314 316 315 - return [...result].sort(sortConfig.compare); 317 + return [...result].toSorted(sortConfig.compare); 316 318 }); 317 319 318 320 return (
+1
src/npm/lib/bundler.ts
··· 30 30 { 31 31 const reader = readable.getReader(); 32 32 while (true) { 33 + // oxlint-disable-next-line no-await-in-loop 33 34 const { done, value: chunk } = await reader.read(); 34 35 if (done) { 35 36 break;
+1
src/npm/lib/fetch.ts
··· 185 185 const { node, basePath } = queue[i]!; 186 186 const packagePath = `${basePath}/${node.name}`; 187 187 188 + // oxlint-disable-next-line no-await-in-loop 188 189 const extractedSize = await fetchTarballToVolume(node.tarball, packagePath, volume, exclude); 189 190 190 191 // use extracted size if registry didn't provide unpackedSize (e.g., JSR packages)
+1 -1
src/npm/lib/hoist.test.ts
··· 396 396 const result = hoist([a]); 397 397 const paths = hoistedToPaths(result); 398 398 399 - expect(paths).toEqual([...paths].sort()); 399 + expect(paths).toEqual(paths.toSorted()); 400 400 }); 401 401 402 402 it('includes nested paths with full hierarchy', () => {
+16 -15
src/npm/lib/hoist.ts
··· 38 38 } 39 39 40 40 /** 41 + * creates a hoisted node from a resolved package. 42 + */ 43 + function createNode(pkg: ResolvedPackage): HoistedNode { 44 + return { 45 + name: pkg.name, 46 + version: pkg.version, 47 + tarball: pkg.tarball, 48 + integrity: pkg.integrity, 49 + unpackedSize: pkg.unpackedSize, 50 + dependencyCount: pkg.dependencies.size, 51 + nested: new Map(), 52 + }; 53 + } 54 + 55 + /** 41 56 * hoists dependencies as high as possible in the tree. 42 57 * follows npm's placement algorithm: 43 58 * 1. explicitly requested (root) packages always get placed at root ··· 63 78 const rootPackageVersions = new Map<string, string>(); 64 79 for (const pkg of roots) { 65 80 rootPackageVersions.set(pkg.name, pkg.version); 66 - } 67 - 68 - /** 69 - * creates a hoisted node from a resolved package. 70 - */ 71 - function createNode(pkg: ResolvedPackage): HoistedNode { 72 - return { 73 - name: pkg.name, 74 - version: pkg.version, 75 - tarball: pkg.tarball, 76 - integrity: pkg.integrity, 77 - unpackedSize: pkg.unpackedSize, 78 - dependencyCount: pkg.dependencies.size, 79 - nested: new Map(), 80 - }; 81 81 } 82 82 83 83 /** ··· 164 164 } 165 165 166 166 walk(result.root, 'node_modules'); 167 + // oxlint-disable-next-line unicorn/no-array-sort 167 168 return paths.sort(); 168 169 }
+1
src/npm/lib/resolve.ts
··· 124 124 // find all versions satisfying the range 125 125 const validVersions = Object.keys(versions) 126 126 .filter((v) => semver.satisfies(v, cleanedRange, satisfiesOptions)) 127 + // oxlint-disable-next-line unicorn/no-array-sort 127 128 .sort(semver.rcompare); 128 129 129 130 if (validVersions.length === 0) {
+25 -14
src/npm/lib/worker-entry.ts
··· 23 23 24 24 // forward progress events to main thread 25 25 progress.listen((msg) => { 26 - self.postMessage(msg satisfies WorkerResponse); 26 + self.postMessage(msg); 27 27 }); 28 28 29 29 // #region state ··· 44 44 // #region handlers 45 45 46 46 async function handleInit(id: number, packageSpec: string, options: InitOptions = {}): Promise<void> { 47 - // if already initialized, return cached result 48 - if (initResult !== null) { 49 - self.postMessage({ id, type: 'init', result: initResult } satisfies WorkerResponse); 50 - return; 51 - } 52 - 53 47 try { 54 48 volume.reset(); 55 49 ··· 83 77 peerDependencies, 84 78 }; 85 79 86 - self.postMessage({ id, type: 'init', result: initResult } satisfies WorkerResponse); 80 + const event = { 81 + id, 82 + type: 'init', 83 + result: initResult, 84 + } satisfies WorkerResponse; 85 + 86 + self.postMessage(event); 87 87 } catch (error) { 88 88 console.error('[worker] init error:', error); 89 - self.postMessage({ id, type: 'error', error: stripAnsi(String(error)) } satisfies WorkerResponse); 89 + 90 + const event = { 91 + id, 92 + type: 'error', 93 + error: stripAnsi(String(error)), 94 + } satisfies WorkerResponse; 95 + 96 + self.postMessage(event); 90 97 } 91 98 } 92 99 ··· 97 104 options: BundleOptions = {}, 98 105 ): Promise<void> { 99 106 if (!packageName) { 100 - self.postMessage({ 107 + const event = { 101 108 id, 102 109 type: 'error', 103 110 error: 'not initialized - call init() first', 104 - } satisfies WorkerResponse); 111 + } satisfies WorkerResponse; 112 + 113 + self.postMessage(event); 105 114 return; 106 115 } 107 116 ··· 109 118 if (bundleInProgress) { 110 119 // reject the previous pending request if any 111 120 if (pendingBundleRequest) { 112 - self.postMessage({ 121 + const event = { 113 122 id: pendingBundleRequest.id, 114 123 type: 'error', 115 124 error: 'Superseded by newer request', 116 - } satisfies WorkerResponse); 125 + } satisfies WorkerResponse; 126 + 127 + self.postMessage(event); 117 128 } 118 129 pendingBundleRequest = { id, subpath, selectedExports, options }; 119 130 return; ··· 172 183 }; 173 184 174 185 // signal to main thread that we're ready 175 - self.postMessage({ type: 'ready' }); 186 + self.postMessage({ type: 'ready' } satisfies WorkerResponse); 176 187 177 188 // #endregion
+5
src/npm/types.ts
··· 132 132 133 133 // #region response schemas (main thread parses these) 134 134 135 + const readyResponseSchema = v.object({ 136 + type: v.literal('ready'), 137 + }); 138 + 135 139 const initResponseSchema = v.object({ 136 140 id: v.number(), 137 141 type: v.literal('init'), ··· 177 181 export type ProgressMessage = v.InferOutput<typeof progressResponseSchema>; 178 182 179 183 export const workerResponseSchema = v.variant('type', [ 184 + readyResponseSchema, 180 185 initResponseSchema, 181 186 bundleResponseSchema, 182 187 errorResponseSchema,
+6 -7
src/npm/worker-client.ts
··· 43 43 } 44 44 45 45 private handleMessage(event: MessageEvent<unknown>): void { 46 - // check for ready signal 47 - if (event.data && typeof event.data === 'object' && 'type' in event.data && event.data.type === 'ready') { 48 - console.log('[worker-client] received ready signal'); 49 - this.resolveReady(); 50 - return; 51 - } 52 - 53 46 const parsed = v.safeParse(workerResponseSchema, event.data); 54 47 if (!parsed.success) { 55 48 console.error('[worker-client] invalid response:', parsed.issues, event.data); ··· 57 50 } 58 51 59 52 const response = parsed.output; 53 + 54 + if (response.type === 'ready') { 55 + console.log('[worker-client] received ready signal'); 56 + this.resolveReady(); 57 + return; 58 + } 60 59 61 60 // forward progress messages to global emitter 62 61 if (response.type === 'progress') {