Skip to content

Instantly share code, notes, and snippets.

@dartess
Last active July 30, 2024 09:29
Show Gist options
  • Save dartess/61b31df3acec3fe1ea5a4f8ec02e58ce to your computer and use it in GitHub Desktop.
Save dartess/61b31df3acec3fe1ea5a4f8ec02e58ce to your computer and use it in GitHub Desktop.
ga-to-browserslist.ts
import fs from 'node:fs';
import path from 'node:path';
import * as R from 'remeda';
import type { ArrayValues } from 'type-fest';
import { exhaustiveCheck } from 'shared/helpers/exhaustiveCheck';
const INPUT_FILE = 'input.csv';
const USER_MEASUREMENT_ERROR = 3;
const BROWSER_NAMES_SUPPORTED = [
'Chrome',
'ChromeAndroid',
'Edge',
'Firefox',
'Opera',
'Safari',
'iOS',
'Samsung',
] as const;
const BROWSER_NAMES_KNOWN = [
'Chrome',
'Safari',
'Samsung Internet',
'Firefox',
'Opera',
'Edge',
'Safari (in-app)',
'Waterfox',
'Mozilla Compatible Agent',
'YaBrowser',
'Android Webview',
'Android Runtime',
'UC Browser',
'Android Browser',
'Internet Explorer',
'Whale Browser',
'Aloha Browser',
'Amazon Silk',
'Konqueror',
'Meta Quest Browser',
'PaleMoon',
'Phoenix Browser',
'Vivaldi',
'Galeon',
'Mozilla',
'Opera Mini',
'SeaMonkey',
'BrowserNG',
'NetFront',
'',
] as const;
const BROWSER_NAMES_INVALID = [
'', // unknown
'Mozilla Compatible Agent', // bot
'Mozilla', // bot
'Android Browser', // can't get a valid chromium version
'Android Runtime', // can't get a valid chromium version
'Meta Quest Browser', // can't get a valid chromium version
'Aloha Browser', // can't get a valid chromium version
'Waterfox', // can't get a valid firefox version
'Galeon', // can't get a valid firefox version
'SeaMonkey', // can't get a valid firefox version
'Phoenix Browser', // can't get a valid version
'Internet Explorer', // don't support
'Opera Mini', // don't support
'Konqueror', // don't support
'BrowserNG', // don't support
'NetFront', // don't support
] as const satisfies Array<BrowserNameKnown>;
const OS_NAMES_UNSUPPORTED = [
'SymbianOS',
'Sony',
];
/**
* find the required version and look at the corresponding version of chrome. For example:
* unknown version of YaBrowser 24.6
* founded: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 YaBrowser/24.6.0.0 Safari/537.36
* so '24.6': 124
*/
const BROWSER_VERSION_MAPPERS: Partial<Record<BrowserNameKnown, Record<string, number>>> = {
/** google site:https://useragents.io YaBrowser/24.6 */
YaBrowser: {
'20.12': 87,
'21.9': 93,
'21.11': 94,
'22.3': 98,
'22.7': 114,
'23.3': 110,
'23.5': 112,
'23.7': 114,
'23.9': 116, // almost
'23.11': 118,
'24.1': 120,
'24.2': 120,
'24.4': 122,
'24.6': 124,
},
/** google site:https://useragents.io UC Browser13.7 */
'UC Browser': {
'11.6': 56,
'13.6': 78,
'13.7': 100,
},
/** google site:https://useragents.io whale/3.25 */
'Whale Browser': {
'1.0': 114,
'3.2': 116,
'3.23': 118,
'3.24': 120,
'3.25': 122,
'3.26': 124,
'3.3': 120,
'3.4': 120, // not found, like 3.3
},
/** google site:https://useragents.io "Firefox" PaleMoon/31.3 */
'PaleMoon': {
'31.3': 102,
},
/** google site:https://useragents.io Vivaldi/1.92 */
'Vivaldi': {
'1.0': 40,
'1.92': 60,
},
};
type BrowserNameKnown = ArrayValues<typeof BROWSER_NAMES_KNOWN>;
type BrowserNameInvalid = ArrayValues<typeof BROWSER_NAMES_INVALID>;
type BrowserNameValid = Exclude<BrowserNameKnown, BrowserNameInvalid>;
type BrowserNameBrowserslist = ArrayValues<typeof BROWSER_NAMES_SUPPORTED>;
type BrowserItem<TBrowserName> = {
browserVersionMajor: number;
browserVersionMinor: number;
browserName: TBrowserName;
osKind: string;
usersTotalCount: number;
usersNewCount: number;
osName: string;
};
type Stats = Array<BrowserItem<BrowserNameBrowserslist>>;
function readStats(): Stats {
const inputFilePath = path.resolve(__dirname, INPUT_FILE);
try {
fs.accessSync(inputFilePath, fs.constants.F_OK);
} catch (err) {
throw new Error(`Get a fresh file with statistics from an SEO specialist, rename it "${INPUT_FILE}" and put it in the "scripts/browsersStat" folder.`);
}
const inputLines = fs.readFileSync(inputFilePath, 'utf-8').split('\n');
const inputStat = inputLines.slice(inputLines.findIndex(line => line.startsWith(',,,,')) + 1).map(line => {
const [browserName, browserVersion, osKind, osName, usersNewCount, usersTotalCount] = line.split(',');
return {
browserName: browserName as BrowserNameKnown,
browserVersion,
osKind,
osName,
usersNewCount: parseInt(usersNewCount),
usersTotalCount: parseInt(usersTotalCount),
};
})
.filter(({ browserName, browserVersion }) => {
// filter out incomplete data
return ![browserName, browserVersion].some(value => value === '(not set)');
})
.filter(({ osName }) => {
// filter out unsupported OS
return !OS_NAMES_UNSUPPORTED.includes(osName);
})
.map((item) => {
// make sure we process all entries
if (!BROWSER_NAMES_KNOWN.includes(item.browserName as BrowserNameKnown)) {
throw new Error(`Add "${item.browserName}" into KNOWN_BROWSER_NAMES`);
}
return item;
})
.filter((item): item is Omit<typeof item, 'browserName'> & { browserName: BrowserNameValid } => {
// filter out data that we cannot analyze
return !BROWSER_NAMES_INVALID.includes(item.browserName as BrowserNameInvalid);
})
.map(({ browserName, browserVersion, osKind, osName, usersNewCount, usersTotalCount }) => {
// parse minor and major browser versions
const [browserVersionMajor, browserVersionMinor = 0] = browserVersion.split('.').map(Number);
return { browserName, browserVersionMajor, browserVersionMinor, osKind, osName, usersNewCount, usersTotalCount };
})
.filter(item => {
// trim some iOS "browsers"
if (item.osName === 'iOS') {
return !['Chrome', 'YaBrowser', 'Aloha Browser', 'Phoenix Browser'].includes(item.browserName);
}
return true;
})
.map((item) => {
// fix some iOS "browsers"
if (item.osName === 'iOS') {
switch (item.browserName) {
case 'Safari':
return item;
case 'Safari (in-app)':
return {
...item,
browserName: 'Safari' as const,
};
default:
throw new Error(`Unknown iOS browser, check it! ${JSON.stringify(item)}`);
}
}
return item;
})
.filter((item) => {
// filter out data that broken versions
switch (item.browserName) {
case 'YaBrowser':
return item.browserVersionMajor > 4;
case 'Chrome':
case 'Edge':
case 'Opera':
return item.browserVersionMajor > 40;
case 'Firefox':
return item.browserVersionMajor > 20;
case 'Safari':
return item.browserVersionMajor < 500;
default:
return true;
}
})
.map((item) => {
// convert browsers to browserslist ones
switch (item.browserName) {
case 'Edge':
case 'Opera':
case 'Firefox':
return item;
case 'Samsung Internet':
return { ...item, browserName: 'Samsung' };
case 'Safari':
return { ...item, browserName: item.osKind === 'desktop' ? 'Safari' : 'iOS' };
case 'Chrome':
case 'Android Webview':
case 'Amazon Silk':
return { ...item, browserName: item.osKind === 'desktop' ? 'Chrome' : 'ChromeAndroid' };
case 'YaBrowser':
case 'UC Browser':
case 'Whale Browser':
case 'Vivaldi':
return mapBrowser(item, 'chromium');
case 'PaleMoon':
return mapBrowser(item, 'firefox');
case 'Safari (in-app)':
throw new Error(`Item should be already filtered: ${JSON.stringify(item)}`);
default:
exhaustiveCheck(item.browserName);
}
}).filter((item) => {
// let's make sure we've processed all browsers
if (!BROWSER_NAMES_SUPPORTED.includes(item.browserName as BrowserNameBrowserslist)) {
throw new Error(`Unknown browser name: ${JSON.stringify(item)}`);
}
return true;
});
return inputStat as Stats;
}
function mapBrowser(item: BrowserItem<BrowserNameKnown>, like: 'chromium' | 'firefox') {
const mapper = BROWSER_VERSION_MAPPERS[item.browserName];
const version = `${item.browserVersionMajor}.${item.browserVersionMinor}`;
if (!mapper) {
throw new Error(`Unknown mapper for ${item.browserName}`);
}
if (!(version in mapper)) {
throw new Error(`Unknown ${item.browserName} version, fill version in BROWSER_VERSION_MAPPERS, ${JSON.stringify(item)}`);
}
return {
...item,
browserName: like === 'chromium' ? item.osKind === 'desktop' ? 'Chrome' : 'ChromeAndroid' : 'Firefox',
browserVersionMajor: mapper[version],
browserVersionMinor: 0,
};
}
function summarizeStats(stats: Stats) {
const minorIsSignificant: Record<BrowserNameBrowserslist, boolean> = {
Chrome: false,
ChromeAndroid: false,
Edge: false,
Firefox: false,
Opera: false,
Safari: true,
iOS: true,
Samsung: true,
};
return R.pipe(
stats,
R.map(stat => {
const versionKey = minorIsSignificant[stat.browserName]
? `${stat.browserVersionMajor}.${stat.browserVersionMinor}`
: `${stat.browserVersionMajor}`;
return {
...stat,
key: `${stat.browserName}.${versionKey}`,
};
}),
R.groupBy(R.prop('key')),
R.mapValues(group => ({
usersTotalCount: R.sumBy(group, R.prop('usersTotalCount')),
browserName: group[0].browserName,
browserVersionMajor: group[0].browserVersionMajor,
browserVersionMinor: group[0].browserVersionMinor,
key: group[0].key,
})),
R.values(),
R.filter(stat => stat.usersTotalCount > USER_MEASUREMENT_ERROR),
R.sortBy([R.prop('browserName'), 'asc'], [R.prop('browserVersionMajor'), 'desc'], [R.prop('browserVersionMinor'), 'desc']),
);
}
function simplifyStats(stats: Stats) {
return R.pipe(
stats,
R.map(stat => {
// simplify Chromium browsers
switch (stat.browserName) {
case 'ChromeAndroid':
case 'Edge':
return { ...stat, browserName: 'Chrome' as const };
case 'Opera':
return { ...stat, browserName: 'Chrome' as const, browserVersionMajor: stat.browserVersionMajor + 13 };
default:
return stat;
}
}),
// R.filter(stat => {
// // trim currently already unsupported versions
// switch (stat.browserName) {
// case 'Chrome':
// return stat.browserVersionMajor >= 84;
// case 'Safari':
// return stat.browserVersionMajor >= 15;
// case 'Samsung':
// return stat.browserVersionMajor >= 15;
// case 'iOS':
// return stat.browserVersionMajor >= 15;
// default:
// return true;
// }
// })
)
}
type SummarizedStats = ReturnType<typeof summarizeStats>;
function toCsv(stats: SummarizedStats) {
return [
'browser,major,minor,users',
...stats.map(item => `${item.browserName},${item.browserVersionMajor},${item.browserVersionMinor},${item.usersTotalCount}`),
].join('\n');
}
function toBrowserslistrc(summarizedStats: SummarizedStats) {
return R.pipe(
summarizedStats,
R.groupBy(R.prop('browserName')),
R.mapValues(summarized => R.firstBy(summarized, [R.prop('browserVersionMajor'), 'asc'], [R.prop('browserVersionMinor'), 'asc'])),
R.mapValues(stat => `${stat.browserVersionMajor}.${stat.browserVersionMinor}`),
R.entries(),
R.map(([browserName, browserVersion]) => ({ browserName, browserVersion })),
R.sortBy([R.prop('browserName'), 'asc']),
R.map(({ browserName, browserVersion }) => `${browserName} >= ${browserVersion}`),
R.join('\n'),
)
}
const stats = readStats();
const simplifiedStats = simplifyStats(stats);
const summarizedStats = summarizeStats(simplifiedStats);
const browserslistrc = toBrowserslistrc(summarizedStats);
fs.writeFileSync(path.resolve(__dirname, '.browserslistrc'), browserslistrc)
fs.writeFileSync(path.resolve(__dirname, '.stat.csv'), toCsv(summarizedStats))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment