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.

feat: show file breakdown

Mary 0e0ba4ed 1e7db795

+309 -117
+3
CLAUDE.md
··· 33 33 change it, it should not be a parameter at all 34 34 - avoid optional parameters that change behavioral modes or make the function do different things 35 35 based on presence/absence; prefer a separate function with a clearer name instead 36 + - avoid type assertions (`as Type`, `as const`) unless TypeScript actually errors without them; when 37 + it does error, prefer finding a solution that satisfies the type system naturally before resorting 38 + to an assertion 36 39 37 40 ### documentation 38 41
+23
patches/@emnapi__core@1.8.1.patch
··· 1 + diff --git a/dist/emnapi-core.esm-bundler.js b/dist/emnapi-core.esm-bundler.js 2 + index 9d4947b2ce3e6d7b6d13b6db1e867d51a145fc63..c7cda8cfa51a43a59351219a8dca8f32cd4c6b1b 100644 3 + --- a/dist/emnapi-core.esm-bundler.js 4 + +++ b/dist/emnapi-core.esm-bundler.js 5 + @@ -4084,16 +4084,13 @@ function createNapiModule(options) { 6 + viewDescriptor = { Ctor: Float16Array, address: external_data, length: byte_length >> 1, ownership: 1 /* ReferenceOwnership.kUserland */, runtimeAllocated: 0 }; 7 + break; 8 + case -2 /* emnapi_memory_view_type.emnapi_buffer */: { 9 + - if (!emnapiCtx.feature.Buffer) { 10 + - throw emnapiCtx.createNotSupportBufferError('emnapi_create_memory_view', ''); 11 + - } 12 + - viewDescriptor = { Ctor: emnapiCtx.feature.Buffer, address: external_data, length: byte_length, ownership: 1 /* ReferenceOwnership.kUserland */, runtimeAllocated: 0 }; 13 + + viewDescriptor = { Ctor: emnapiCtx.feature.Buffer || Uint8Array, address: external_data, length: byte_length, ownership: 1 /* ReferenceOwnership.kUserland */, runtimeAllocated: 0 }; 14 + break; 15 + } 16 + default: return envObject.setLastError(1 /* napi_status.napi_invalid_arg */); 17 + } 18 + var Ctor = viewDescriptor.Ctor; 19 + - var typedArray = typedarray_type === -2 /* emnapi_memory_view_type.emnapi_buffer */ 20 + + var typedArray = typedarray_type === -2 /* emnapi_memory_view_type.emnapi_buffer */ && emnapiCtx.feature.Buffer 21 + ? emnapiCtx.feature.Buffer.from(wasmMemory.buffer, viewDescriptor.address, viewDescriptor.length) 22 + : new Ctor(wasmMemory.buffer, viewDescriptor.address, viewDescriptor.length); 23 + var handle = emnapiCtx.addToCurrentScope(typedArray);
+7 -2
pnpm-lock.yaml
··· 4 4 autoInstallPeers: true 5 5 excludeLinksFromLockfile: false 6 6 7 + patchedDependencies: 8 + '@emnapi/core@1.8.1': 9 + hash: a77aede501e9bb46e765e06258fd6d8ad4bf1d678c7d0a2d9995b9fbaa99ea02 10 + path: patches/@emnapi__core@1.8.1.patch 11 + 7 12 importers: 8 13 9 14 .: ··· 1958 1963 dependencies: 1959 1964 '@jridgewell/trace-mapping': 0.3.9 1960 1965 1961 - '@emnapi/core@1.8.1': 1966 + '@emnapi/core@1.8.1(patch_hash=a77aede501e9bb46e765e06258fd6d8ad4bf1d678c7d0a2d9995b9fbaa99ea02)': 1962 1967 dependencies: 1963 1968 '@emnapi/wasi-threads': 1.1.0 1964 1969 tslib: 2.8.1 ··· 2313 2318 2314 2319 '@napi-rs/wasm-runtime@1.1.1': 2315 2320 dependencies: 2316 - '@emnapi/core': 1.8.1 2321 + '@emnapi/core': 1.8.1(patch_hash=a77aede501e9bb46e765e06258fd6d8ad4bf1d678c7d0a2d9995b9fbaa99ea02) 2317 2322 '@emnapi/runtime': 1.8.1 2318 2323 '@tybys/wasm-util': 0.10.1 2319 2324
+2
pnpm-workspace.yaml
··· 1 + patchedDependencies: 2 + '@emnapi/core@1.8.1': patches/@emnapi__core@1.8.1.patch
+177 -71
src/components/package-bundle.tsx
··· 1 - import { createSignal, For, Match, onCleanup, Show, Switch } from 'solid-js'; 1 + import { createMemo, createSignal, For, Match, onCleanup, Show, Switch } from 'solid-js'; 2 2 3 - import { LucideCheck, LucideCircleAlert, LucideInfo, LucideLoader } from '../icons/lucide'; 3 + import { LucideCheck, LucideChevronDown, LucideCircleAlert, LucideInfo, LucideLoader } from '../icons/lucide'; 4 + import { formatBytes } from '../lib/format'; 4 5 import { LRUCache } from '../lib/lru'; 5 6 import { createQuery } from '../lib/query'; 6 7 import { createDerivedSignal } from '../lib/signals'; 7 8 import { progress } from '../npm/events'; 8 - import type { BundleResult, DiscoveredSubpaths, ProgressMessage } from '../npm/types'; 9 + import type { BundleOutput, BundleResult, DiscoveredSubpaths, ProgressMessage } from '../npm/types'; 9 10 import type { BundlerWorker } from '../npm/worker-client'; 10 11 import Button from '../primitives/button'; 11 12 import * as Dropdown from '../primitives/dropdown'; ··· 30 31 } 31 32 } 32 33 return true; 34 + } 35 + 36 + /** sorts output: entry chunk first, then chunks alphabetically, then assets alphabetically */ 37 + function sortOutput(output: BundleOutput[]): BundleOutput[] { 38 + return output.toSorted((a, b) => { 39 + // entry chunk comes first 40 + if (a.type === 'chunk' && a.isEntry) { 41 + return -1; 42 + } 43 + if (b.type === 'chunk' && b.isEntry) { 44 + return 1; 45 + } 46 + // chunks before assets 47 + if (a.type !== b.type) { 48 + return a.type === 'chunk' ? -1 : 1; 49 + } 50 + // lexicographical within same type 51 + if (a.filename < b.filename) { 52 + return -1; 53 + } 54 + if (a.filename > b.filename) { 55 + return 1; 56 + } 57 + return 0; 58 + }); 59 + } 60 + 61 + function computeTotals(output: BundleOutput[]) { 62 + const size = output.reduce((s, f) => s + f.size, 0); 63 + const gzipSize = output.reduce((s, f) => s + f.gzipSize, 0); 64 + const brotliSize = output.every((f) => f.brotliSize !== undefined) 65 + ? output.reduce((s, f) => s + f.brotliSize!, 0) 66 + : undefined; 67 + const zstdSize = output.every((f) => f.zstdSize !== undefined) 68 + ? output.reduce((s, f) => s + f.zstdSize!, 0) 69 + : undefined; 70 + 71 + return { size, gzipSize, brotliSize, zstdSize }; 33 72 } 34 73 35 74 // #endregion ··· 192 231 </Match> 193 232 194 233 <Match when={initialBundle() && bundle()}> 195 - {(bundleData) => ( 196 - <div class="flex flex-col gap-5"> 197 - {/* size display card */} 198 - <div class="sticky top-1 flex flex-wrap items-stretch gap-4 rounded-lg border border-neutral-stroke-3 bg-neutral-background-1 px-4 py-3"> 199 - <SizeStat label="Minified" size={bundleData().size} /> 234 + {(bundleData) => { 235 + const totals = createMemo(() => computeTotals(bundleData().output)); 236 + const sortedOutput = createMemo(() => sortOutput(bundleData().output)); 237 + const hasMultipleOutputs = createMemo(() => bundleData().output.length > 1); 238 + const [breakdownOpen, setBreakdownOpen] = createSignal(false); 200 239 201 - <SizeStat label="Gzip" size={bundleData().gzipSize} /> 240 + return ( 241 + <div class="flex flex-col gap-5"> 242 + {/* size display card */} 243 + <div class="sticky top-1 flex flex-wrap items-stretch gap-4 rounded-lg border border-neutral-stroke-3 bg-neutral-background-1 px-4 py-3"> 244 + <SizeStat label="Minified" size={totals().size} /> 202 245 203 - {bundleData().brotliSize! && <SizeStat label="Brotli" size={bundleData().brotliSize!} />} 246 + <SizeStat label="Gzip" size={totals().gzipSize} /> 204 247 205 - {bundleData().zstdSize !== undefined && ( 206 - <SizeStat label="Zstd" size={bundleData().zstdSize!} /> 207 - )} 208 - </div> 248 + {totals().brotliSize !== undefined && ( 249 + <SizeStat label="Brotli" size={totals().brotliSize!} /> 250 + )} 209 251 210 - <Switch> 211 - <Match when={bundleData().isCjs}> 212 - <div class="flex items-center gap-2 text-base-200 text-neutral-foreground-3"> 213 - <LucideInfo class="size-4" /> 214 - <span>CommonJS module — tree-shaking unavailable</span> 215 - </div> 216 - </Match> 252 + {totals().zstdSize !== undefined && <SizeStat label="Zstd" size={totals().zstdSize!} />} 253 + </div> 217 254 218 - <Match when={!initialBundle()?.exports.length}> 219 - <div class="flex items-center gap-2 text-base-200 text-neutral-foreground-3"> 220 - <LucideInfo class="size-4" /> 221 - <span>No exports detected — side-effects only module</span> 222 - </div> 223 - </Match> 255 + {/* breakdown table */} 256 + <Show when={hasMultipleOutputs()}> 257 + <div class="flex flex-col gap-2"> 258 + <button 259 + class="flex items-center gap-1 text-base-200 text-neutral-foreground-2 hover:text-neutral-foreground-1" 260 + onClick={() => setBreakdownOpen((v) => !v)} 261 + > 262 + <LucideChevronDown 263 + class="size-4 transition-transform duration-150" 264 + classList={{ 'rotate-0': breakdownOpen(), '-rotate-90': !breakdownOpen() }} 265 + /> 266 + <span>Breakdown ({sortedOutput().length} files)</span> 267 + </button> 224 268 225 - <Match when={initialBundle()?.exports}> 226 - {(allExports) => ( 227 - <div class="flex flex-col gap-3"> 228 - <div class="flex items-center justify-between"> 229 - <span class="text-base-300 font-medium text-neutral-foreground-2"> 230 - Exports ({allExports().length}) 231 - </span> 232 - <div class="flex gap-1"> 233 - <Button appearance="subtle" size="small" onClick={selectAll}> 234 - All 235 - </Button> 236 - <Button appearance="subtle" size="small" onClick={selectNone}> 237 - None 238 - </Button> 239 - </div> 240 - </div> 241 - <div class="flex flex-wrap gap-1.5"> 242 - <For each={allExports()}> 243 - {(exp) => { 244 - const selected = () => isExportSelected(exp); 245 - return ( 246 - <button 247 - onClick={() => toggleExport(exp)} 248 - class="inline-flex items-center gap-1 rounded-md border px-2 py-1 text-base-300 transition duration-100 select-none" 269 + <Show when={breakdownOpen()}> 270 + <table class="w-full text-base-200"> 271 + <thead> 272 + <tr class="text-left text-neutral-foreground-3"> 273 + <th class="py-1 pr-4 font-medium">File</th> 274 + <th class="py-1 pr-4 text-right font-medium">Minified</th> 275 + <th class="py-1 pr-4 text-right font-medium">Gzip</th> 276 + {totals().brotliSize !== undefined && ( 277 + <th class="py-1 pr-4 text-right font-medium">Brotli</th> 278 + )} 279 + {totals().zstdSize !== undefined && ( 280 + <th class="py-1 text-right font-medium">Zstd</th> 281 + )} 282 + </tr> 283 + </thead> 284 + <tbody> 285 + <For each={sortedOutput()}> 286 + {(item) => ( 287 + <tr 249 288 classList={{ 250 - 'border-brand-stroke-1 bg-brand-background-2 text-brand-foreground-2 hover:bg-brand-background-2-hover hover:text-brand-foreground-2-hover active:bg-brand-background-2-pressed active:text-brand-foreground-2-pressed': 251 - selected(), 252 - 'border-neutral-stroke-1 bg-neutral-background-1 text-neutral-foreground-2 hover:bg-neutral-background-1-hover hover:text-neutral-foreground-2-hover active:bg-neutral-background-1-pressed active:text-neutral-foreground-2-pressed': 253 - !selected(), 289 + 'text-neutral-foreground-2': item.type === 'chunk', 290 + 'text-neutral-foreground-3': item.type === 'asset', 254 291 }} 255 292 > 256 - <LucideCheck 257 - class="duration-fast size-3.5 transition" 293 + <td class="py-1 pr-4 font-mono">{item.filename}</td> 294 + <td class="py-1 pr-4 text-right">{formatBytes(item.size)}</td> 295 + <td class="py-1 pr-4 text-right">{formatBytes(item.gzipSize)}</td> 296 + {totals().brotliSize !== undefined && ( 297 + <td class="py-1 pr-4 text-right"> 298 + {item.brotliSize !== undefined ? formatBytes(item.brotliSize) : '—'} 299 + </td> 300 + )} 301 + {totals().zstdSize !== undefined && ( 302 + <td class="py-1 text-right"> 303 + {item.zstdSize !== undefined ? formatBytes(item.zstdSize) : '—'} 304 + </td> 305 + )} 306 + </tr> 307 + )} 308 + </For> 309 + </tbody> 310 + </table> 311 + </Show> 312 + </div> 313 + </Show> 314 + 315 + <Switch> 316 + <Match when={bundleData().isCjs}> 317 + <div class="flex items-center gap-2 text-base-200 text-neutral-foreground-3"> 318 + <LucideInfo class="size-4" /> 319 + <span>CommonJS module — tree-shaking unavailable</span> 320 + </div> 321 + </Match> 322 + 323 + <Match when={!initialBundle()?.exports.length}> 324 + <div class="flex items-center gap-2 text-base-200 text-neutral-foreground-3"> 325 + <LucideInfo class="size-4" /> 326 + <span>No exports detected — side-effects only module</span> 327 + </div> 328 + </Match> 329 + 330 + <Match when={initialBundle()?.exports}> 331 + {(allExports) => ( 332 + <div class="flex flex-col gap-3"> 333 + <div class="flex items-center justify-between"> 334 + <span class="text-base-300 font-medium text-neutral-foreground-2"> 335 + Exports ({allExports().length}) 336 + </span> 337 + <div class="flex gap-1"> 338 + <Button appearance="subtle" size="small" onClick={selectAll}> 339 + All 340 + </Button> 341 + <Button appearance="subtle" size="small" onClick={selectNone}> 342 + None 343 + </Button> 344 + </div> 345 + </div> 346 + <div class="flex flex-wrap gap-1.5"> 347 + <For each={allExports()}> 348 + {(exp) => { 349 + const selected = () => isExportSelected(exp); 350 + return ( 351 + <button 352 + onClick={() => toggleExport(exp)} 353 + class="inline-flex items-center gap-1 rounded-md border px-2 py-1 text-base-300 transition duration-100 select-none" 258 354 classList={{ 259 - 'opacity-100': selected(), 260 - 'opacity-0': !selected(), 355 + 'border-brand-stroke-1 bg-brand-background-2 text-brand-foreground-2 hover:bg-brand-background-2-hover hover:text-brand-foreground-2-hover active:bg-brand-background-2-pressed active:text-brand-foreground-2-pressed': 356 + selected(), 357 + 'border-neutral-stroke-1 bg-neutral-background-1 text-neutral-foreground-2 hover:bg-neutral-background-1-hover hover:text-neutral-foreground-2-hover active:bg-neutral-background-1-pressed active:text-neutral-foreground-2-pressed': 358 + !selected(), 261 359 }} 262 - /> 263 - <span>{exp}</span> 264 - </button> 265 - ); 266 - }} 267 - </For> 360 + > 361 + <LucideCheck 362 + class="duration-fast size-3.5 transition" 363 + classList={{ 364 + 'opacity-100': selected(), 365 + 'opacity-0': !selected(), 366 + }} 367 + /> 368 + <span>{exp}</span> 369 + </button> 370 + ); 371 + }} 372 + </For> 373 + </div> 268 374 </div> 269 - </div> 270 - )} 271 - </Match> 272 - </Switch> 273 - </div> 274 - )} 375 + )} 376 + </Match> 377 + </Switch> 378 + </div> 379 + ); 380 + }} 275 381 </Match> 276 382 277 383 <Match when>
+79 -36
src/npm/lib/bundler.ts
··· 3 3 import { memfs } from '@rolldown/browser/experimental'; 4 4 5 5 import { progress } from '../events'; 6 - import type { BundleChunk, BundleOptions, BundleResult } from '../types'; 6 + import type { BundleAsset, BundleChunk, BundleOptions, BundleResult } from '../types'; 7 7 8 8 import { BundleError } from './errors'; 9 9 import { analyzeModule } from './module-type'; ··· 15 15 const VIRTUAL_ENTRY_ID = '\0virtual:entry'; 16 16 17 17 /** 18 - * get compressed size using a compression stream. 18 + * get compressed size of raw bytes using a compression stream. 19 19 */ 20 - async function getCompressedSize(code: string, format: CompressionFormat): Promise<number> { 20 + async function getCompressedSizeFromBytes(data: Uint8Array, format: CompressionFormat): Promise<number> { 21 21 const { readable, writable } = new CompressionStream(format); 22 22 23 23 { 24 24 const writer = writable.getWriter(); 25 - writer.write(encodeUtf8(code)); 25 + writer.write(data as Uint8Array<ArrayBuffer>); 26 26 writer.close(); 27 27 } 28 28 ··· 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 + * get gzip size of raw bytes. 55 + */ 56 + function getGzipSizeFromBytes(data: Uint8Array): Promise<number> { 57 + return getCompressedSizeFromBytes(data, 'gzip'); 58 + } 59 + 60 + /** 47 61 * get gzip size using compression stream. 48 62 */ 49 63 function getGzipSize(code: string): Promise<number> { ··· 59 73 let isBrotliSupported: boolean | undefined; 60 74 61 75 /** 62 - * get brotli size using compression stream, if supported. 76 + * get brotli size of raw bytes, if supported. 63 77 * returns `undefined` if brotli is not supported by the browser. 64 78 */ 65 - async function getBrotliSize(code: string): Promise<number | undefined> { 79 + async function getBrotliSizeFromBytes(data: Uint8Array): Promise<number | undefined> { 66 80 if (isBrotliSupported === false) { 67 81 return undefined; 68 82 } ··· 70 84 if (isBrotliSupported === undefined) { 71 85 try { 72 86 // @ts-expect-error 'brotli' is not in the type definition yet 73 - const size = await getCompressedSize(code, 'brotli'); 87 + const size = await getCompressedSizeFromBytes(data, 'brotli'); 74 88 console.log(`[worker] brotli supported`); 75 89 isBrotliSupported = true; 76 90 return size; ··· 82 96 } 83 97 84 98 // @ts-expect-error 'brotli' is not in the type definition yet 85 - return getCompressedSize(code, 'brotli'); 99 + return getCompressedSizeFromBytes(data, 'brotli'); 100 + } 101 + 102 + /** 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)); 86 108 } 87 109 88 110 /** ··· 102 124 let zstdWasm: typeof import('@bokuweb/zstd-wasm') | null | undefined; 103 125 104 126 /** 105 - * get zstd-compressed size using WASM fallback. 127 + * get zstd-compressed size of raw bytes using WASM fallback. 106 128 * returns `undefined` if WASM failed to load. 107 129 */ 108 - async function getZstdSizeWasm(code: string): Promise<number | undefined> { 130 + async function getZstdSizeWasmFromBytes(data: Uint8Array): Promise<number | undefined> { 109 131 if (zstdWasm === null) { 110 132 return undefined; 111 133 } ··· 122 144 } 123 145 } 124 146 125 - const encoded = encodeUtf8(code); 126 - const compressed = zstdWasm.compress(encoded); 147 + const compressed = zstdWasm.compress(data); 127 148 128 149 return compressed.byteLength; 129 150 } 130 151 131 152 /** 132 - * get zstd size using compression stream if supported, or WASM fallback. 153 + * get zstd size of raw bytes using compression stream if supported, or WASM fallback. 133 154 * returns `undefined` if neither native nor WASM is available. 134 155 */ 135 - async function getZstdSize(code: string): Promise<number | undefined> { 156 + async function getZstdSizeFromBytes(data: Uint8Array): Promise<number | undefined> { 136 157 // use WASM fallback if native is known to be unsupported 137 158 if (isZstdSupported === false) { 138 - return getZstdSizeWasm(code); 159 + return getZstdSizeWasmFromBytes(data); 139 160 } 140 161 141 162 if (isZstdSupported === undefined) { 142 163 try { 143 164 // @ts-expect-error 'zstd' is not in the type definition yet 144 - const size = await getCompressedSize(code, 'zstd'); 165 + const size = await getCompressedSizeFromBytes(data, 'zstd'); 145 166 console.log(`[worker] zstd supported`); 146 167 isZstdSupported = true; 147 168 return size; 148 169 } catch { 149 170 console.log(`[worker] zstd not supported, trying wasm fallback`); 150 171 isZstdSupported = false; 151 - return getZstdSizeWasm(code); 172 + return getZstdSizeWasmFromBytes(data); 152 173 } 153 174 } 154 175 155 176 // @ts-expect-error 'zstd' is not in the type definition yet 156 - return getCompressedSize(code, 'zstd'); 177 + 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)); 157 186 } 158 187 159 188 // #endregion ··· 183 212 input: { main: VIRTUAL_ENTRY_ID }, 184 213 cwd: '/', 185 214 external: options.rolldown?.external, 215 + experimental: { resolveNewUrlToAsset: true }, 186 216 plugins: [ 187 217 { 188 218 name: 'virtual-entry', ··· 262 292 minify: options.rolldown?.minify ?? true, 263 293 }); 264 294 265 - // process all chunks 295 + // split output into chunks and assets 266 296 const rawChunks = output.output.filter((o) => o.type === 'chunk'); 297 + const rawAssets = output.output.filter((o) => o.type === 'asset'); 267 298 268 299 progress.emit({ type: 'progress', kind: 'compress' }); 269 300 ··· 278 309 ]); 279 310 280 311 return { 281 - fileName: chunk.fileName, 282 - code, 312 + type: 'chunk' as const, 313 + filename: chunk.fileName, 283 314 size, 284 315 gzipSize, 285 316 brotliSize, 286 317 zstdSize, 287 318 isEntry: chunk.isEntry, 288 - exports: chunk.exports || [], 319 + }; 320 + }), 321 + ); 322 + 323 + const assets: BundleAsset[] = await Promise.all( 324 + rawAssets.map(async (asset) => { 325 + const raw = typeof asset.source === 'string' ? encodeUtf8(asset.source) : asset.source; 326 + // rolldown uses SharedArrayBuffer for WASM memory; CompressionStream rejects 327 + // views backed by shared buffers, so copy into a regular ArrayBuffer 328 + const data = raw.buffer instanceof SharedArrayBuffer ? raw.slice() : raw; 329 + const size = data.byteLength; 330 + const [gzipSize, brotliSize, zstdSize] = await Promise.all([ 331 + getGzipSizeFromBytes(data), 332 + getBrotliSizeFromBytes(data), 333 + getZstdSizeFromBytes(data), 334 + ]); 335 + 336 + return { 337 + type: 'asset' as const, 338 + filename: asset.fileName, 339 + size, 340 + gzipSize, 341 + brotliSize, 342 + zstdSize, 289 343 }; 290 344 }), 291 345 ); 292 346 293 347 // find entry chunk for exports 294 - const entryChunk = chunks.find((c) => c.isEntry); 348 + const entryChunk = rawChunks.find((c) => c.isEntry); 295 349 if (!entryChunk) { 296 350 throw new BundleError('no entry chunk found in bundle output'); 297 351 } 298 352 299 - // aggregate sizes 300 - const totalSize = chunks.reduce((acc, c) => acc + c.size, 0); 301 - const totalGzipSize = chunks.reduce((acc, c) => acc + c.gzipSize, 0); 302 - const totalBrotliSize = isBrotliSupported ? chunks.reduce((acc, c) => acc + c.brotliSize!, 0) : undefined; 303 - const totalZstdSize = 304 - isZstdSupported || zstdWasm != null ? chunks.reduce((acc, c) => acc + c.zstdSize!, 0) : undefined; 305 - 306 353 await bundle.close(); 307 354 308 355 return { 309 - chunks, 310 - size: totalSize, 311 - gzipSize: totalGzipSize, 312 - brotliSize: totalBrotliSize, 313 - zstdSize: totalZstdSize, 314 - exports: entryChunk.exports, 356 + output: [...chunks, ...assets], 357 + exports: entryChunk.exports || [], 315 358 isCjs, 316 359 }; 317 360 }
+18 -8
src/npm/types.ts
··· 80 80 81 81 export type InitResult = v.InferOutput<typeof initResultSchema>; 82 82 83 - const bundleChunkSchema = v.object({ 84 - fileName: v.string(), 85 - code: v.string(), 83 + const bundleAssetSchema = v.object({ 84 + type: v.literal('asset'), 85 + filename: v.string(), 86 86 size: v.number(), 87 87 gzipSize: v.number(), 88 88 brotliSize: v.optional(v.number()), 89 89 zstdSize: v.optional(v.number()), 90 - isEntry: v.boolean(), 91 - exports: v.array(v.string()), 92 90 }); 93 91 94 - export type BundleChunk = v.InferOutput<typeof bundleChunkSchema>; 92 + export type BundleAsset = v.InferOutput<typeof bundleAssetSchema>; 95 93 96 - const bundleResultSchema = v.object({ 97 - chunks: v.array(bundleChunkSchema), 94 + const bundleChunkSchema = v.object({ 95 + type: v.literal('chunk'), 96 + filename: v.string(), 98 97 size: v.number(), 99 98 gzipSize: v.number(), 100 99 brotliSize: v.optional(v.number()), 101 100 zstdSize: v.optional(v.number()), 101 + isEntry: v.boolean(), 102 + }); 103 + 104 + export type BundleChunk = v.InferOutput<typeof bundleChunkSchema>; 105 + 106 + const bundleOutputSchema = v.variant('type', [bundleAssetSchema, bundleChunkSchema]); 107 + 108 + export type BundleOutput = v.InferOutput<typeof bundleOutputSchema>; 109 + 110 + const bundleResultSchema = v.object({ 111 + output: v.array(bundleOutputSchema), 102 112 exports: v.array(v.string()), 103 113 isCjs: v.boolean(), 104 114 });