File size: 4,072 Bytes
57ba6cf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
// Attribute a built chunk's minified bytes to source npm packages / workspace dirs via its
// sourcemap (zero-dependency VLQ decoder). The dist-audit companion of the shell chunk-layout
// decision (.agents/notes/implemented/architecture/2026-08-06-web-shell-dist-chunk-layout.md):
// verifies vendor/index membership after changing VENDOR_PACKAGES or bumping render deps.
// Usage: node scripts/attribute-chunk-bytes.mjs <chunk.js> [--top N]
import { readFileSync } from 'node:fs'

const chunkPath = process.argv[2]
const topN = Number(process.argv[process.argv.indexOf('--top') + 1] || 40)
const code = readFileSync(chunkPath, 'utf8')
const map = JSON.parse(readFileSync(chunkPath + '.map', 'utf8'))

const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
const charToInt = new Map([...B64].map((c, i) => [c, i]))

// Decode one VLQ-encoded segment list line by line.
const lines = code.split('\n')
const bySource = new Float64Array(map.sources.length)
let unmapped = 0

let srcIdx = 0, srcLine = 0, srcCol = 0, nameIdx = 0
const mappingLines = map.mappings.split(';')
for (let li = 0; li < mappingLines.length; li++) {
  const lineLen = li < lines.length ? lines[li].length + 1 : 0
  const segsRaw = mappingLines[li]
  if (segsRaw === '') { unmapped += lineLen; continue }
  let genCol = 0
  const segs = []
  for (const segStr of segsRaw.split(',')) {
    const fields = []
    let shift = 0, value = 0
    for (const ch of segStr) {
      const digit = charToInt.get(ch)
      value += (digit & 31) << shift
      if (digit & 32) { shift += 5 } else {
        fields.push((value & 1) ? -(value >>> 1) : (value >>> 1))
        shift = 0; value = 0
      }
    }
    genCol += fields[0]
    if (fields.length > 1) {
      srcIdx += fields[1]; srcLine += fields[2]; srcCol += fields[3]
      if (fields.length > 4) nameIdx += fields[4]
      segs.push([genCol, srcIdx])
    } else {
      segs.push([genCol, -1])
    }
  }
  if (segs[0][0] > 0) unmapped += segs[0][0]
  for (let i = 0; i < segs.length; i++) {
    const end = i + 1 < segs.length ? segs[i + 1][0] : lineLen
    const span = Math.max(0, end - segs[i][0])
    if (segs[i][1] >= 0) bySource[segs[i][1]] += span
    else unmapped += span
  }
}

// Aggregate source path -> package bucket.
function bucketOf(src) {
  if (src.startsWith('') || src.includes('vite/')) return '(vite virtual/helpers)'
  const nm = [...src.matchAll(/node_modules\/(@[^/]+\/[^/]+|[^@./][^/]*)\//g)]
  if (nm.length > 0) return nm[nm.length - 1][1]
  let m = src.match(/packages\/([^/]+\/[^/]+)\//)
  if (m) return 'ws:packages/' + m[1]
  m = src.match(/vendor\/([^/]+)\//)
  if (m) return 'ws:vendor/' + m[1]
  m = src.match(/apps\/web\//)
  if (m) return 'ws:apps/web'
  return src
}

const byBucket = new Map()
for (let i = 0; i < map.sources.length; i++) {
  if (bySource[i] === 0) continue
  const b = bucketOf(map.sources[i])
  byBucket.set(b, (byBucket.get(b) || 0) + bySource[i])
}
byBucket.set('(unmapped: interop glue/helpers)', unmapped)

const total = code.length
const rows = [...byBucket.entries()].sort((a, b) => b[1] - a[1])
const kb = n => (n / 1024).toFixed(1).padStart(8)
const pc = n => ((n / total) * 100).toFixed(1).padStart(5)
console.log(`chunk: ${chunkPath}  total ${(total / 1024).toFixed(1)} kB (minified, pre-gzip)`)
console.log(`${'kB'.padStart(8)} ${'%'.padStart(5)}  package`)
let shown = 0, acc = 0
for (const [name, bytes] of rows) {
  if (shown++ < topN) console.log(`${kb(bytes)} ${pc(bytes)}  ${name}`)
  acc += bytes
}
if (rows.length > topN) console.log(`   ... ${rows.length - topN} more buckets`)
console.log(`accounted: ${(acc / 1024).toFixed(1)} kB of ${(total / 1024).toFixed(1)} kB`)

// Group summary: npm vendor vs workspace.
let vendor = 0, ws = 0, other = 0
for (const [name, bytes] of rows) {
  if (name.startsWith('ws:')) ws += bytes
  else if (name.startsWith('(')) other += bytes
  else vendor += bytes
}
console.log(`\nGROUPS  npm-vendor ${(vendor / 1024).toFixed(1)} kB | workspace ${(ws / 1024).toFixed(1)} kB | glue ${(other / 1024).toFixed(1)} kB`)