Skip to content

Instantly share code, notes, and snippets.

@eps1lon
Last active May 15, 2021 11:00
Show Gist options
  • Save eps1lon/2af3c98d37fa88d14e1b2145555424e9 to your computer and use it in GitHub Desktop.
Save eps1lon/2af3c98d37fa88d14e1b2145555424e9 to your computer and use it in GitHub Desktop.
0 new icons
0 new variants
1 deleted icons
0 deleted variants
0 renamed icons
854 modified variants
const fs = require('fs');
const { snakeCase } = require('lodash');
const fetch = require('cross-fetch');
const path = require('path');
const yargs = require('yargs');
// https://stackoverflow.com/a/49428486/3406963
function streamToString(stream) {
const chunks = [];
return new Promise((resolve, reject) => {
stream.on('data', (chunk) => chunks.push(chunk));
stream.on('error', (err) => reject(err));
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
});
}
function materialIconIdentifier(muiIconName) {
return snakeCase(muiIconName.replace(/(Rounded|Outlined|Sharp|TwoTone)$/, '')).replace(
/(\d)_/,
'$1',
);
}
const iconEndpointsCache = new Map();
async function findHighestIconVersion(muiIconName) {
let endpointsCache = iconEndpointsCache.get(muiIconName);
if (endpointsCache === undefined) {
endpointsCache = Array.from({ length: 12 }, async (_, index) => {
const version = index + 1;
const endpoint = `v${version}`;
const response = await fetch(
`https://fonts.gstatic.com/s/i/materialicons/${materialIconIdentifier(
muiIconName,
)}/${endpoint}/24px.svg`,
);
if (response.status !== 200) {
throw new Error(`${response.status}: ${response.statusText}`);
}
return version;
});
iconEndpointsCache.set(materialIconIdentifier, endpointsCache);
}
const endpoints = await Promise.allSettled(endpointsCache);
return endpoints
.filter((promise) => {
return promise.status === 'fulfilled';
})
.map((promise) => {
return promise;
})
.sort((a, b) => {
return b.value - a.value;
})[0]?.value;
}
/**
* @param {Set<string>} icons
*/
async function iconsToString(icons) {
const lines = await Promise.all(
Array.from(icons, async (icon) => {
const version = await findHighestIconVersion(icon);
return ` - ![${icon}](https://fonts.gstatic.com/s/i/materialicons/${materialIconIdentifier(
icon,
)}/v${version}/24px.svg) ([material.io: ${materialIconIdentifier(
icon,
)}](https://material.io/resources/icons/?search=${materialIconIdentifier(
icon,
)}&icon=${materialIconIdentifier(icon)}&style=baseline))`;
}),
);
return lines.join('\n');
}
/**
* @param {Set<string>} icons
*/
async function renamedIconsToString(icons) {
const lines = await Promise.all(
Array.from(icons, async ([oldIcon, { score, newName: icon }]) => {
const version = await findHighestIconVersion(icon);
return ` - ![${icon}](https://fonts.gstatic.com/s/i/materialicons/${materialIconIdentifier(
icon,
)}/${version}/24px.svg) (was ${oldIcon} (copy score: ${score}), [material.io: ${materialIconIdentifier(
icon,
)}](https://material.io/resources/icons/?search=${materialIconIdentifier(
icon,
)}&icon=${materialIconIdentifier(icon)}&style=baseline))`;
}),
);
return lines.join('\n');
}
async function main(options) {
const { patchFile } = options;
const patchStream = patchFile !== undefined ? fs.createReadStream(patchFile) : process.stdin;
const patch = await streamToString(patchStream);
const newIcons = new Set();
const newIconVariants = new Set();
const deletedIcons = new Set();
const deletedIconVariants = new Set();
const modifiedIcons = new Set();
const renamedIcons = new Map();
patch.split(/\n\r?/).forEach((nameStatus) => {
if (nameStatus.trim() === '') {
return;
}
const [status, name, maybeNewName] = nameStatus.trim().split(/\t/);
const isMuiIconChange = name.startsWith('packages/material-ui-icons/lib/esm');
if (!isMuiIconChange) {
return;
}
const iconVariantName = path.basename(name, '.js');
const variantMatch = iconVariantName.match(/^(\w+)(Outlined|Rounded|Sharp|TwoTone)$/);
const iconName = variantMatch?.[1] ?? iconVariantName;
if (status === 'A') {
// If an icon is added, added variants are implied
if (variantMatch === null) {
newIcons.add(iconName);
} else if (!newIcons.has(iconName)) {
newIconVariants.add(iconVariantName);
}
} else if (status === 'M') {
modifiedIcons.add(iconVariantName);
} else if (status === 'D') {
// If an icon is deleted, deleted variants are implied
if (variantMatch === null) {
deletedIcons.add(iconVariantName);
} else if (!deletedIcons.has(iconName)) {
deletedIconVariants.add(iconVariantName);
}
} else if (status.startsWith('R')) {
const newName = path.basename(maybeNewName, '.js');
const [, score] = status.match(/R(\d+)/);
renamedIcons.set(iconVariantName, { score, newName });
} else {
console.warn(`Unable to parse git diff status '${status}'.`);
}
});
process.stdout.write(`<details>
<summary>${newIcons.size} new icons</summary>
${await iconsToString(newIcons)}
</details>`);
process.stdout.write(`
<details>
<summary>${newIconVariants.size} new variants</summary>
${await iconsToString(newIconVariants)}
</details>`);
process.stdout.write(`
<details>
<summary>${deletedIcons.size} deleted icons</summary>
${await iconsToString(deletedIcons)}
</details>`);
process.stdout.write(`
<details>
<summary>${deletedIconVariants.size} deleted variants </summary>
${await iconsToString(deletedIconVariants)}
</details>`);
process.stdout.write(`
<details>
<summary>${renamedIcons.size} renamed icons</summary>
${await renamedIconsToString(renamedIcons)}
</details>`);
process.stdout.write(`
<details>
<summary>${modifiedIcons.size} modified variants</summary>
${await iconsToString(modifiedIcons)}
</details>
`);
}
yargs
.command({
command: '$0 [patchFile]',
description:
'Parses a patch containing changes to icons in `@material-ui/icons` and creates a markdown file for human review.',
handler: main,
builder: (command) => {
return command
.positional('patchFile', {
type: 'string',
describe:
'If you have put the output of `git diff --name-status next` in a file. By default the script uses stdin.',
})
.example('git diff --name-status next | $0 > icons-patch.md')
.example('$0 icons-patch.diff > icons-patch.md');
},
})
.help()
.strict(true)
.version(false)
.parse();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment