Last active
July 30, 2024 09:29
-
-
Save dartess/61b31df3acec3fe1ea5a4f8ec02e58ce to your computer and use it in GitHub Desktop.
ga-to-browserslist.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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