t0n
Version:
Collection of elegant typescript resources for web artisans
215 lines (167 loc) • 6.76 kB
text/typescript
import { LRUCache } from './lru-cache'
export type OsType = 'desktop' | 'tablet' | 'mobile' | 'tv' | 'bot' | 'unknown'
const UNKNOWN = 'unknown'
export type IUAResult = {
browser: {
name: string,
version: string,
}
os: {
name: string,
type: OsType,
}
raw?: string | null,
}
type IBrowserRule = {
name: string,
pattern: RegExp,
}
type IOsRule = {
name: string,
pattern: RegExp,
type: OsType,
}
const BROWSER_RULES: readonly IBrowserRule[] = [
// Bots / Crawlers
{ name: 'Googlebot', pattern: /Googlebot\/([\d.]+)/i },
{ name: 'Googlebot-Image', pattern: /Googlebot-Image/i },
{ name: 'Googlebot-News', pattern: /Googlebot-News/i },
{ name: 'GoogleOther', pattern: /GoogleOther/i },
{ name: 'Bingbot', pattern: /bingbot\/([\d.]+)/i },
{ name: 'YandexBot', pattern: /YandexBot\/([\d.]+)/i },
{ name: 'DuckDuckBot', pattern: /DuckDuckBot\/([\d.]+)/i },
{ name: 'Baiduspider', pattern: /Baiduspider\/([\d.]+)/i },
{ name: 'Slurp', pattern: /Slurp/i },
{ name: 'Applebot', pattern: /Applebot\/([\d.]+)/i },
{ name: 'SemrushBot', pattern: /SemrushBot/i },
{ name: 'AhrefsBot', pattern: /AhrefsBot/i },
{ name: 'MJ12bot', pattern: /MJ12bot/i },
{ name: 'DotBot', pattern: /DotBot/i },
{ name: 'PetalBot', pattern: /PetalBot/i },
// AI Crawlers
{ name: 'GPTBot', pattern: /GPTBot/i },
{ name: 'ChatGPT-User', pattern: /ChatGPT-User/i },
{ name: 'CCBot', pattern: /CCBot/i },
{ name: 'PerplexityBot', pattern: /PerplexityBot/i },
{ name: 'ClaudeBot', pattern: /ClaudeBot/i },
{ name: 'Amazonbot', pattern: /Amazonbot/i },
// In-app browsers
{ name: 'Instagram', pattern: /Instagram/i },
{ name: 'Facebook', pattern: /FBAN|FBAV/i },
{ name: 'TikTok', pattern: /TikTok/i },
{ name: 'LinkedIn', pattern: /LinkedInApp/i },
{ name: 'Discord', pattern: /Discord/i },
{ name: 'Telegram', pattern: /Telegram/i },
{ name: 'Snapchat', pattern: /Snapchat/i },
{ name: 'Line', pattern: /Line\/([\d.]+)/i },
// Chromium family
{ name: 'Arc', pattern: /Arc\/([\d.]+)/i },
{ name: 'Arc Search', pattern: /ArcSearch/i },
{ name: 'Edge', pattern: /Edg\/([\d.]+)/i },
{ name: 'Edge (Legacy)', pattern: /Edge\/([\d.]+)/i },
{ name: 'Opera', pattern: /OPR\/([\d.]+)/i },
{ name: 'Opera', pattern: /Opera\/([\d.]+)/i },
{ name: 'Brave', pattern: /Brave\/([\d.]+)/i },
{ name: 'Vivaldi', pattern: /Vivaldi\/([\d.]+)/i },
{ name: 'Thorium', pattern: /Thorium\/([\d.]+)/i },
{ name: 'YaBrowser', pattern: /YaBrowser\/([\d.]+)/i },
{ name: 'CocCoc', pattern: /coc_coc_browser\/([\d.]+)/i },
{ name: 'Maxthon', pattern: /Maxthon\/([\d.]+)/i },
{ name: 'Comodo Dragon', pattern: /Dragon\/([\d.]+)/i },
{ name: 'Whale', pattern: /Whale\/([\d.]+)/i },
{ name: 'Puffin', pattern: /Puffin\/([\d.]+)/i },
// Mobile browsers
{ name: 'Samsung Browser', pattern: /SamsungBrowser\/([\d.]+)/i },
{ name: 'Huawei Browser', pattern: /HuaweiBrowser\/([\d.]+)/i },
{ name: 'Miui Browser', pattern: /MiuiBrowser\/([\d.]+)/i },
{ name: 'UCBrowser', pattern: /UCBrowser\/([\d.]+)/i },
{ name: 'QQBrowser', pattern: /QQBrowser\/([\d.]+)/i },
{ name: 'Sogou', pattern: /SogouMobileBrowser/i },
{ name: 'DuckDuckGo', pattern: /DuckDuckGo\/([\d.]+)/i },
// Firefox family
{ name: 'Firefox', pattern: /Firefox\/([\d.]+)/i },
{ name: 'Waterfox', pattern: /Waterfox\/([\d.]+)/i },
{ name: 'Pale Moon', pattern: /PaleMoon\/([\d.]+)/i },
{ name: 'SeaMonkey', pattern: /SeaMonkey\/([\d.]+)/i },
// Chrome family
{ name: 'Chrome', pattern: /CriOS\/([\d.]+)/i },
{ name: 'Chrome', pattern: /Chrome\/([\d.]+)/i },
{ name: 'Chromium', pattern: /Chromium\/([\d.]+)/i },
// Safari
{ name: 'Safari', pattern: /Version\/([\d.]+).*Safari/i },
// Legacy
{ name: 'MSIE', pattern: /MSIE ([\d.]+)/i },
{ name: 'IE', pattern: /Trident\/.*rv:([\d.]+)/i },
]
const OS_RULES: readonly IOsRule[] = [
{ name: 'Bot', pattern: /bot|crawl|spider|slurp|crawler/i, type: 'bot' },
// TVs
{ name: 'Android TV', pattern: /Android TV/i, type: 'tv' },
{ name: 'Tizen', pattern: /Tizen/i, type: 'tv' },
{ name: 'WebOS', pattern: /webOS/i, type: 'tv' },
{ name: 'SmartTV', pattern: /SmartTV|SMART-TV/i, type: 'tv' },
{ name: 'PlayStation', pattern: /PlayStation/i, type: 'tv' },
{ name: 'Xbox', pattern: /Xbox/i, type: 'tv' },
{ name: 'Nintendo', pattern: /Nintendo/i, type: 'tv' },
// Mobile OS
{ name: 'HarmonyOS', pattern: /HarmonyOS/i, type: 'mobile' },
{ name: 'KaiOS', pattern: /KaiOS/i, type: 'mobile' },
{ name: 'FireOS', pattern: /KF[A-Z]{2,}/i, type: 'tablet' },
{ name: 'iPadOS', pattern: /iPad/i, type: 'tablet' },
{ name: 'Android', pattern: /Android(?!.*Mobile)/i, type: 'tablet' },
{ name: 'iOS', pattern: /iPhone|iPod/i, type: 'mobile' },
{ name: 'Android', pattern: /Android.*Mobile/i, type: 'mobile' },
{ name: 'Windows Phone', pattern: /Windows Phone|IEMobile/i, type: 'mobile' },
{ name: 'BlackBerry', pattern: /BlackBerry|BB10/i, type: 'mobile' },
// Desktop
{ name: 'Windows', pattern: /Windows NT/i, type: 'desktop' },
{ name: 'macOS', pattern: /Macintosh|Mac OS X/i, type: 'desktop' },
{ name: 'ChromeOS', pattern: /CrOS/i, type: 'desktop' },
{ name: 'Ubuntu', pattern: /Ubuntu/i, type: 'desktop' },
{ name: 'Debian', pattern: /Debian/i, type: 'desktop' },
{ name: 'Fedora', pattern: /Fedora/i, type: 'desktop' },
{ name: 'Arch', pattern: /Arch Linux/i, type: 'desktop' },
{ name: 'Linux', pattern: /Linux/i, type: 'desktop' }
]
const cache = new LRUCache<string, IUAResult>(512)
export const parseUA = (ua?: string | null): IUAResult => {
if (!ua) {
return {
browser: { name: UNKNOWN, version: '' },
os: { name: UNKNOWN, type: UNKNOWN },
raw: ua,
}
}
const cached = cache.get(ua)
if (cached) return cached
let browserName = UNKNOWN
let browserVersion = ''
for (const rule of BROWSER_RULES) {
const match = rule.pattern.exec(ua)
if (match) {
browserName = rule.name
browserVersion = match[1] ?? ''
break
}
}
let osName = UNKNOWN
let osType: OsType = UNKNOWN
for (const rule of OS_RULES) {
if (rule.pattern.test(ua)) {
osName = rule.name
osType = rule.type
break
}
}
const result: IUAResult = {
browser: { name: browserName, version: browserVersion },
os: { name: osName, type: osType },
raw: ua,
}
cache.set(ua, result)
return result
}
export const isBot = (ua: string) => parseUA(ua).os.type === 'bot'
export const isMobile = (ua: string) => parseUA(ua).os.type === 'mobile'
export const isTablet = (ua: string) => parseUA(ua).os.type === 'tablet'
export const isDesktop = (ua: string) => parseUA(ua).os.type === 'desktop'