#!/usr/bin/env python3 from collections import namedtuple import json import os import re import sys Bundle = namedtuple('Bundle' , 'bundleName totalBytes files') Diff = namedtuple('Diff', 'status path oldBytes newBytes isBigger diff') BundleDiff = namedtuple('BundleDiff', 'total files') class Status: added = 'added' modified = 'modified' deleted = 'deleted' def humanize(nbytes): suffixes = ['B', 'KB', 'MB'] i = 0 while abs(nbytes) >= 1024 and i < len(suffixes)-1: nbytes /= 1024. i += 1 f = ('%.1f' % nbytes).rstrip('0').rstrip('.') return '%s %s' % (f, suffixes[i]) def normalizePath(path): if path.startswith('node_modules/') or '/node_modules/' in path: mod = path.partition('/node_modules/')[2].split('/') if mod[0].startswith('@'): return '/'.join(mod[0:2]) return mod[0] return path def normalizeBundleName(path): return re.sub(r'\-.{8}\.([a-zA-Z-])', r'.\1', path) def mergeFiles(allFiles): files = {} for path, meta in allFiles.items(): np = normalizePath(path) p = os.path.dirname(np) or np if p not in files: files[p] = 0 files[p] += meta['size'] return files def mergeSize(a, b): for k,v in b.items(): if k in a: a[k] += v else: a[k] = v return a def mergeBundles(allBundles): bundles = {} for a in allBundles: if a.bundleName not in bundles: bundles[a.bundleName] = Bundle( bundleName=a.bundleName, totalBytes=0, files={}, ) bundles[a.bundleName] = Bundle( bundleName=a.bundleName, totalBytes=bundles[a.bundleName].totalBytes + a.totalBytes, files=mergeSize(bundles[a.bundleName].files, a.files), ) return bundles.values() def readBundle(path): bundles = [] with open(path) as f: data = json.load(f) for result in data['results']: bundle = Bundle( bundleName=normalizeBundleName(result['bundleName']), totalBytes=result['totalBytes'], files=mergeFiles(result['files'])) bundles.append(bundle) return mergeBundles(bundles) def printRow(diff, element): if diff.status == Status.added: status = '+' elif diff.status == Status.modified: status = 'M' else: status = '-' sign = '+' if diff.isBigger else '' ob = humanize(diff.oldBytes) if diff.oldBytes > 0 else '' nb = humanize(diff.newBytes) if diff.newBytes > 0 else '' return ''' <{element}>{status} <{element}>{path} <{element}>{ob} <{element}>{nb} <{element}>{sign}{diff} '''.format( element=element, status=status, path=diff.path, ob=ob, nb=nb, sign=sign, diff=humanize(diff.diff)) def printBundleTable(diffs, element): rows = [] for diff in diffs: rows.append(printRow(diff, element)) return '{body}
'.format(body='\n'.join(rows)) def printBundleDiff(bundleDiff): summary = printBundleTable([bundleDiff.total], 'th') details = printBundleTable(bundleDiff.files, 'td') return '''
{summary} {details}
'''.format(summary=summary, details=details) def makeDiff(path, oldBytes, newBytes): if oldBytes > 0 and newBytes > 0: status = Status.modified elif oldBytes == 0: status = Status.added else: status = Status.deleted return Diff( status=status, path=path, oldBytes=oldBytes, newBytes=newBytes, isBigger=newBytes > oldBytes, diff=newBytes - oldBytes, ) def diffBundle(ba, bb): filesDiffs = [] diff = bb.totalBytes - ba.totalBytes if abs(diff) > 500: totalDiff = makeDiff(ba.bundleName, ba.totalBytes, bb.totalBytes) for aFile in ba.files: aSize = ba.files[aFile] if aFile in bb.files: bSize = bb.files[aFile] if aSize != bSize: filesDiffs.append(makeDiff(aFile, aSize, bSize)) else: filesDiffs.append(makeDiff(aFile, aSize, 0)) for bFile in bb.files: bSize = bb.files[bFile] if bFile not in ba.files: filesDiffs.append(makeDiff(bFile, 0, bSize)) return BundleDiff(total=totalDiff, files=filesDiffs) def summarize(diffs): totalDiff = 0 for diff in diffs: totalDiff += diff.total.diff sign = '+' if totalDiff > 0 else '' return '### Total diff: {sign}{totalDiff}'.format( sign=sign, totalDiff=humanize(totalDiff)) def printDiff(diffs): rows = [] for diff in diffs: rows.append(printBundleDiff(diff)) summary = summarize(diffs) print('{summary}\n{rows}'.format(summary=summary, rows='\n'.join(rows))) def diffBundles(a, b): diffs = [] for ba in a: found = False for bb in b: if ba.bundleName == bb.bundleName: found = True d = diffBundle(ba, bb) if d: diffs.append(d) break if not found: bb = Bundle( bundleName=ba.bundleName, totalBytes=0, files={} ) d = diffBundle(ba, bb) if d: diffs.append(d) for bb in b: found = False for ba in a: if ba.bundleName == bb.bundleName: found = True if not found: ba = Bundle( bundleName=bb.bundleName, totalBytes=0, files={} ) d = diffBundle(ba, bb) if d: diffs.append(d) diffs.sort(key=lambda x: x.total.newBytes, reverse=True) return diffs if __name__ == '__main__': if len(sys.argv) != 3: print('Usage: PATH1 PATH2') sys.exit(1) bundleA = readBundle(sys.argv[1]) bundleB = readBundle(sys.argv[2]) if not bundleA or not bundleB: print('Usage: PATH1 PATH2') sys.exit(1) diffs = diffBundles(bundleA, bundleB) if diffs: printDiff(diffs)