Files
squoosh/config/size-report.js
2019-02-22 11:15:50 +00:00

204 lines
5.8 KiB
JavaScript

const path = require('path');
const { URL } = require('url');
const gzipSize = require('gzip-size');
const fetch = require('node-fetch');
const prettyBytes = require('pretty-bytes');
const escapeRE = require('escape-string-regexp');
const readdirp = require('readdirp');
const chalk = new require('chalk').constructor({ level: 4 });
function fetchTravis(path, options = {}) {
const url = new URL(path, 'https://api.travis-ci.org');
url.search = new URLSearchParams(options);
return fetch(url, {
headers: { 'Travis-API-Version': '3' },
});
}
function fetchTravisBuildInfo(user, repo, branch) {
return fetchTravis(`/repo/${encodeURIComponent(`${user}/${repo}`)}/builds`, {
'branch.name': branch,
state: 'passed',
limit: 1,
event_type: 'push',
}).then(r => r.json());
}
function fetchTravisText(path) {
return fetchTravis(path).then(r => r.text());
}
/**
* Recursively-read a directory and turn it into an array of { name, size, gzipSize }
*/
async function dirToInfoArray(startPath) {
const results = await new Promise((resolve, reject) => {
readdirp({ root: startPath }, (err, results) => {
if (err) reject(err); else resolve(results);
});
});
return Promise.all(
results.files.map(async (entry) => ({
name: entry.path,
gzipSize: await gzipSize.file(entry.fullPath),
size: entry.stat.size,
})),
);
}
/**
* Try to treat two entries with different file name hashes as the same file.
*/
function findHashedMatch(name, buildInfo) {
const nameParts = /^(.+\.)[a-f0-9]+(\..+)$/.exec(name);
if (!nameParts) return;
const matchRe = new RegExp(`^${escapeRE(nameParts[1])}[a-f0-9]+${escapeRE(nameParts[2])}$`);
const matchingEntry = buildInfo.find(entry => matchRe.test(entry.name));
return matchingEntry;
}
const buildSizePrefix = '=== BUILD SIZES: ';
const buildSizePrefixRe = new RegExp(`^${escapeRE(buildSizePrefix)}(.+)$`, 'm');
async function getPreviousBuildInfo() {
const buildData = await fetchTravisBuildInfo('GoogleChromeLabs', 'squoosh', 'master');
const jobUrl = buildData.builds[0].jobs[0]['@href'];
const log = await fetchTravisText(jobUrl + '/log.txt');
const reResult = buildSizePrefixRe.exec(log);
if (!reResult) return;
return JSON.parse(reResult[1]);
}
/**
* Generate an array that represents the difference between builds.
* Returns an array of { beforeName, afterName, beforeSize, afterSize }.
* Sizes are gzipped size.
* Before/after properties are missing if resource isn't in the previous/new build.
*/
function getChanges(previousBuildInfo, buildInfo) {
const buildChanges = [];
const alsoInPreviousBuild = new Set();
for (const oldEntry of previousBuildInfo) {
const newEntry = buildInfo.find(entry => entry.name === oldEntry.name) ||
findHashedMatch(oldEntry.name, buildInfo);
// Entry is in previous build, but not the new build.
if (!newEntry) {
buildChanges.push({
beforeName: oldEntry.name,
beforeSize: oldEntry.gzipSize,
});
continue;
}
// Mark this entry so we know we've dealt with it.
alsoInPreviousBuild.add(newEntry);
// If they're the same, just ignore.
// Using size rather than gzip size. I've seen different platforms produce different zipped
// sizes.
if (
oldEntry.size === newEntry.size &&
oldEntry.name === newEntry.name
) continue;
// Entry is in both builds (maybe renamed).
buildChanges.push({
beforeName: oldEntry.name,
afterName: newEntry.name,
beforeSize: oldEntry.gzipSize,
afterSize: newEntry.gzipSize,
});
}
// Look for entries that are only in the new build.
for (const newEntry of buildInfo) {
if (alsoInPreviousBuild.has(newEntry)) continue;
buildChanges.push({
afterName: newEntry.name,
afterSize: newEntry.gzipSize,
});
}
return buildChanges;
}
async function main() {
// Output the current build sizes for later retrieval.
const buildInfo = await dirToInfoArray(__dirname + '/../build');
console.log(buildSizePrefix + JSON.stringify(buildInfo));
console.log('\nBuild change report:');
let previousBuildInfo;
try {
previousBuildInfo = await getPreviousBuildInfo();
} catch (err) {
console.log(` Couldn't parse previous build info`);
return;
}
if (!previousBuildInfo) {
console.log(` Couldn't find previous build info`);
return;
}
const buildChanges = getChanges(previousBuildInfo, buildInfo);
if (buildChanges.length === 0) {
console.log(' No changes');
return;
}
// One letter references, so it's easier to get the spacing right.
const y = chalk.yellow;
const g = chalk.green;
const r = chalk.red;
for (const change of buildChanges) {
// New file.
if (!change.beforeSize) {
console.log(` ${g('ADDED')} ${change.afterName} - ${prettyBytes(change.afterSize)}`);
continue;
}
// Removed file.
if (!change.afterSize) {
console.log(` ${r('REMOVED')} ${change.beforeName} - was ${prettyBytes(change.beforeSize)}`);
continue;
}
// Changed file.
let size;
if (change.beforeSize === change.afterSize) {
// Just renamed.
size = `${prettyBytes(change.afterSize)} -> no change`;
} else {
const color = change.afterSize > change.beforeSize ? r : g;
const sizeDiff = prettyBytes(change.afterSize - change.beforeSize, { signed: true });
const relativeDiff = Math.round((change.afterSize / change.beforeSize) * 1000) / 1000;
size = `${prettyBytes(change.beforeSize)} -> ${prettyBytes(change.afterSize)}` +
' (' +
color(`${sizeDiff}, ${relativeDiff}x`) +
')';
}
console.log(` ${y('CHANGED')} ${change.afterName} - ${size}`);
if (change.beforeName !== change.afterName) {
console.log(` Renamed from: ${change.beforeName}`);
}
}
}
main();