From 74064b0cac02131fbe1119ca5a5feb7c0815f8e6 Mon Sep 17 00:00:00 2001
From: Faisal Salman
Date: Sat, 27 Sep 2025 20:20:51 +0700
Subject: [PATCH 01/20] Add featured sponsors in README
---
README.md | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/README.md b/README.md
index 79c1f73..03eb487 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,9 @@
+#### Featured Sponsors
+
+[](https://ref.wisprflow.ai/Rxj3n8H)
+
+---
+
[](https://uaparser.dev)
[](https://uaparser.dev)
[](https://uaparser.dev)
From 737cdd4d40dc0daaf73bfccc67022d4a0c40c8f1 Mon Sep 17 00:00:00 2001
From: Faisal Salman
Date: Mon, 29 Sep 2025 11:32:04 +0700
Subject: [PATCH 02/20] Improve device detection: iPad
---
src/main/ua-parser.js | 5 +-
test/data/ua/device/apple.json | 99 ++++++++++++++++++++++++++++++++++
2 files changed, 101 insertions(+), 3 deletions(-)
diff --git a/src/main/ua-parser.js b/src/main/ua-parser.js
index bc0ef93..816c7da 100755
--- a/src/main/ua-parser.js
+++ b/src/main/ua-parser.js
@@ -568,9 +568,8 @@
// Apple
/(?:\/|\()(ip(?:hone|od)[\w, ]*)(?:\/|;)/i // iPod/iPhone
], [MODEL, [VENDOR, APPLE], [TYPE, MOBILE]], [
- /\((ipad);[-\w\),; ]+apple/i, // iPad
- /applecoremedia\/[\w\.]+ \((ipad)/i,
- /\b(ipad)\d\d?,\d\d?[;\]].+ios/i
+ /\b(?:ios|apple\w+)\/.+[\(\/](ipad)/i, // iPad
+ /\b(ipad)[\d,]*[;\] ].+(mac |i(pad)?)os/i
], [MODEL, [VENDOR, APPLE], [TYPE, TABLET]], [
/(macintosh);/i
], [MODEL, [VENDOR, APPLE]], [
diff --git a/test/data/ua/device/apple.json b/test/data/ua/device/apple.json
index ea68e8a..aca64ee 100644
--- a/test/data/ua/device/apple.json
+++ b/test/data/ua/device/apple.json
@@ -44,6 +44,105 @@
"type": "tablet"
}
},
+ {
+ "desc": "iPad using Chrome",
+ "ua": "Mozilla/5.0 (iPad13,10; CPU OS 16_6_2 like Mac OS X) AppleWebKit/602.3 (KHTML, like Gecko) CriOS/97.0.0.5927.72 Mobile/11S296 Safari/602.3",
+ "expect": {
+ "vendor": "Apple",
+ "model": "iPad",
+ "type": "tablet"
+ }
+ },
+ {
+ "desc": "iPad using DuckDuckGo",
+ "ua": "Mozilla/5.0 (iPad; iPad13,6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) DuckDuckGo/130.0.0.0 Mobile/15E148 Safari/604.1",
+ "expect": {
+ "vendor": "Apple",
+ "model": "iPad",
+ "type": "tablet"
+ }
+ },
+ {
+ "desc": "iPad using Discord",
+ "ua": "Discord/52.0 (iPad; iOS 14.4; Scale/2.00)",
+ "expect": {
+ "vendor": "Apple",
+ "model": "iPad",
+ "type": "tablet"
+ }
+ },
+ {
+ "desc": "iPad using Firefox",
+ "ua": "Mozilla/5.0 (iPad; iPad14,2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Firefox/120.0 Mobile/15E148 Safari/604.1",
+ "expect": {
+ "vendor": "Apple",
+ "model": "iPad",
+ "type": "tablet"
+ }
+ },
+ {
+ "desc": "iPad using iTunes",
+ "ua": "itunesstored/1.0 iOS/15.6.1 model/iPad13,8 hwp/t8103 build/19G82 (5; dt:238)",
+ "expect": {
+ "vendor": "Apple",
+ "model": "iPad",
+ "type": "tablet"
+ }
+ },
+ {
+ "desc": "iPad using MS OneNote",
+ "ua": "Microsoft Office OneNote/16.81/240108 (iOS/16.5.1; Tablet; AppStore; Apple/iPad8,3)",
+ "expect": {
+ "vendor": "Apple",
+ "model": "iPad",
+ "type": "tablet"
+ }
+ },
+ {
+ "desc": "iPad using MS Word",
+ "ua": "Microsoft Office Word/2.44.1211 (iOS/13.7; Tablet; es-MX; AppStore; Apple/iPad11,3)",
+ "expect": {
+ "vendor": "Apple",
+ "model": "iPad",
+ "type": "tablet"
+ }
+ },
+ {
+ "desc": "iPad using Quora",
+ "ua": "Quora 8.4.30 rv:3230 env:prod (iPad11,3; iPadOS 17.7; en_GB) AppleWebKit",
+ "expect": {
+ "vendor": "Apple",
+ "model": "iPad",
+ "type": "tablet"
+ }
+ },
+ {
+ "desc": "iPad using TuneIn Radio",
+ "ua": "TuneIn Radio/27.1.0; iPad6,3; iPadOS/16.6",
+ "expect": {
+ "vendor": "Apple",
+ "model": "iPad",
+ "type": "tablet"
+ }
+ },
+ {
+ "desc": "iPad using TuneIn Radio Pro",
+ "ua": "TuneIn Radio Pro/21.4.1; iPad8,9; iOS/15.0",
+ "expect": {
+ "vendor": "Apple",
+ "model": "iPad",
+ "type": "tablet"
+ }
+ },
+ {
+ "desc": "iPad using YouTube",
+ "ua": "com.google.ios.youtube/20.31.6 (iPad13,5; U; CPU iPadOS 18_6 like Mac OS X; en_US)",
+ "expect": {
+ "vendor": "Apple",
+ "model": "iPad",
+ "type": "tablet"
+ }
+ },
{
"desc": "iPod",
"ua": "Mozilla/5.0 (iPod touch; CPU iPhone OS 7_0_4 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11B554a Safari/9537.53",
From 95d2b151a38cab06f5dce3fb0569846760560fe3 Mon Sep 17 00:00:00 2001
From: Faisal Salman
Date: Tue, 30 Sep 2025 12:59:21 +0700
Subject: [PATCH 03/20] Improve OS detection: iOS
---
src/main/ua-parser.js | 4 +--
test/data/ua/device/apple.json | 9 +++++
test/data/ua/os/ios.json | 63 ++++++++++++++++++++++++++++++++++
3 files changed, 74 insertions(+), 2 deletions(-)
diff --git a/src/main/ua-parser.js b/src/main/ua-parser.js
index 816c7da..42b409a 100755
--- a/src/main/ua-parser.js
+++ b/src/main/ua-parser.js
@@ -566,7 +566,7 @@
], [MODEL, [VENDOR, SAMSUNG], [TYPE, MOBILE]], [
// Apple
- /(?:\/|\()(ip(?:hone|od)[\w, ]*)(?:\/|;)/i // iPod/iPhone
+ /(?:\/|\()(ip(?:hone|od)[\w, ]*)[\/\);]/i // iPod/iPhone
], [MODEL, [VENDOR, APPLE], [TYPE, MOBILE]], [
/\b(?:ios|apple\w+)\/.+[\(\/](ipad)/i, // iPad
/\b(ipad)[\d,]*[;\] ].+(mac |i(pad)?)os/i
@@ -964,7 +964,7 @@
// iOS/macOS
/[adehimnop]{4,7}\b(?:.*os ([\w]+) like mac|; opera)/i, // iOS
- /(?:ios;fbsv\/|iphone.+ios[\/ ])([\d\.]+)/i,
+ /(?:ios;fbsv|ios(?=.+ip(?:ad|hone))|ip(?:ad|hone)(?: |.+i(?:pad)?)os)[\/ ]([\w\.]+)/i,
/cfnetwork\/.+darwin/i
], [[VERSION, /_/g, '.'], [NAME, 'iOS']], [
/(mac os x) ?([\w\. ]*)/i,
diff --git a/test/data/ua/device/apple.json b/test/data/ua/device/apple.json
index aca64ee..0ea909e 100644
--- a/test/data/ua/device/apple.json
+++ b/test/data/ua/device/apple.json
@@ -161,6 +161,15 @@
"type": "mobile"
}
},
+ {
+ "desc": "iPhone using Spotify",
+ "ua": "Spotify/8.7.70 iOS/16.0 (iPhone15,3)",
+ "expect": {
+ "vendor": "Apple",
+ "model": "iPhone15,3",
+ "type": "mobile"
+ }
+ },
{
"desc": "iPhone SE",
"ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 13_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 [FBAN/FBIOS;FBDV/iPhone8,4;FBMD/iPhone;FBSN/iOS;FBSV/13.3.1;FBSS/2;FBID/phone;FBLC/en_US;FBOP/5;FBCR/]",
diff --git a/test/data/ua/os/ios.json b/test/data/ua/os/ios.json
index 86ba8cb..1b0d636 100644
--- a/test/data/ua/os/ios.json
+++ b/test/data/ua/os/ios.json
@@ -17,6 +17,15 @@
"version" : "5.1.1"
}
},
+ {
+ "desc" : "iOS with DuckDuckGo",
+ "ua" : "Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Mobile/15E148 Safari/604.1 Ddg/26.0",
+ "expect" :
+ {
+ "name" : "iOS",
+ "version" : "18.7"
+ }
+ },
{
"desc" : "iOS with Opera Mini",
"ua" : "Opera/9.80 (iPhone; Opera Mini/7.1.32694/27.1407; U; en) Presto/2.8.119 Version/11.10",
@@ -35,6 +44,33 @@
"version" : "13.6.1"
}
},
+ {
+ "desc": "iOS with Instagram",
+ "ua": "Instagram 5.0.2 (iPhone5,1; iPhone OS 7_0_4; en_US; en) AppleWebKit/420+",
+ "expect":
+ {
+ "name" : "iOS",
+ "version" : "7.0.4"
+ }
+ },
+ {
+ "desc": "iOS with MS Word App",
+ "ua": "Microsoft Office Word/2.44.1211 (iOS/13.7; Tablet; es-MX; AppStore; Apple/iPad11,3)",
+ "expect":
+ {
+ "name" : "iOS",
+ "version" : "13.7"
+ }
+ },
+ {
+ "desc": "iOS with Quora App",
+ "ua": "Quora 8.4.30 rv:3230 env:prod (iPad11,3; iPadOS 17.7; en_GB) AppleWebKit",
+ "expect":
+ {
+ "name" : "iOS",
+ "version" : "17.7"
+ }
+ },
{
"desc": "iOS with Slack App",
"ua": "com.tinyspeck.chatlyio/23.04.10 (iPhone; iOS 16.4.1; Scale/3.00)",
@@ -44,6 +80,33 @@
"version" : "16.4.1"
}
},
+ {
+ "desc": "iOS with Snapchat",
+ "ua": "Snapchat/12.12.1.40 (iPhone15,2; iOS 16.2; gzip)",
+ "expect":
+ {
+ "name" : "iOS",
+ "version" : "16.2"
+ }
+ },
+ {
+ "desc": "iOS with Spotify App",
+ "ua": "Spotify/8.7.70 iOS/16.0 (iPhone15,3)",
+ "expect":
+ {
+ "name" : "iOS",
+ "version" : "16.0"
+ }
+ },
+ {
+ "desc": "iOS with TuneIn Radio App",
+ "ua": "TuneIn Radio/27.1.0; iPad6,3; iPadOS/16.6",
+ "expect":
+ {
+ "name" : "iOS",
+ "version" : "16.6"
+ }
+ },
{
"desc" : "iOS BE App",
"ua" : "APP-BE Test/1.0 (iPad; Apple; CPU iPhone OS 7_0_2 like Mac OS X)",
From e7e7aaad4cbe429690e288bfec92bf0a3e68a6c4 Mon Sep 17 00:00:00 2001
From: Faisal Salman
Date: Wed, 1 Oct 2025 11:10:43 +0700
Subject: [PATCH 04/20] Add new browser: Qwant
---
src/enums/ua-parser-enums.js | 1 +
src/main/ua-parser.js | 3 ++-
test/data/ua/browser/browser-all.json | 30 +++++++++++++++++++++++++++
3 files changed, 33 insertions(+), 1 deletion(-)
diff --git a/src/enums/ua-parser-enums.js b/src/enums/ua-parser-enums.js
index 1dd8d9f..ed88bdc 100644
--- a/src/enums/ua-parser-enums.js
+++ b/src/enums/ua-parser-enums.js
@@ -125,6 +125,7 @@ const BrowserName = Object.freeze({
QUARK: 'Quark',
QUPZILLA: 'QupZilla',
QUTEBROWSER: 'qutebrowser',
+ QWANT: 'Qwant',
REKONQ: 'rekonq',
ROCKMELT: 'Rockmelt',
SAFARI: 'Safari',
diff --git a/src/main/ua-parser.js b/src/main/ua-parser.js
index 42b409a..b1abedb 100755
--- a/src/main/ua-parser.js
+++ b/src/main/ua-parser.js
@@ -380,7 +380,8 @@
// Blink/Webkit/KHTML based // Flock/RockMelt/Midori/Epiphany/Silk/Skyfire/Bolt/Iron/Iridium/PhantomJS/Bowser/QupZilla/Falkon/LG Browser/Otter/qutebrowser/Dooble
/(flock|rockmelt|midori|epiphany|silk|skyfire|ovibrowser|bolt|iron|vivaldi|iridium|phantomjs|bowser|qupzilla|falkon|rekonq|puffin|brave|whale(?!.+naver)|qqbrowserlite|duckduckgo|klar|helio|(?=comodo_)?dragon|otter|dooble|(?:lg |qute)browser)\/([-\w\.]+)/i,
// Rekonq/Puffin/Brave/Whale/QQBrowserLite/QQ//Vivaldi/DuckDuckGo/Klar/Helio/Dragon
- /(heytap|ovi|115|surf)browser\/([\d\.]+)/i, // HeyTap/Ovi/115/Surf
+ /(heytap|ovi|115|surf|qwant)browser\/([\d\.]+)/i, // HeyTap/Ovi/115/Surf
+ /(qwant)(?:ios|mobile)\/([\d\.]+)/i, // Qwant
/(ecosia|weibo)(?:__| \w+@)([\d\.]+)/i // Ecosia/Weibo
], [NAME, VERSION], [
/quark(?:pc)?\/([-\w\.]+)/i // Quark
diff --git a/test/data/ua/browser/browser-all.json b/test/data/ua/browser/browser-all.json
index a2a6379..5293bc7 100644
--- a/test/data/ua/browser/browser-all.json
+++ b/test/data/ua/browser/browser-all.json
@@ -1688,6 +1688,36 @@
"major" : "2"
}
},
+ {
+ "desc" : "Qwant",
+ "ua" : "Mozilla/5.0 (iPhone; CPU iPhone OS 16_1_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) QwantMobile/6.7.6 Mobile/15E148 Safari/605.1.15",
+ "expect" :
+ {
+ "name" : "Qwant",
+ "version" : "6.7.6",
+ "major" : "6"
+ }
+ },
+ {
+ "desc" : "Qwant",
+ "ua" : "QwantMobile/2.0 (Android 8.0.0; Mobile; rv:59.0) Gecko/59.0 Firefox/59.0 QwantBrowser/59.0",
+ "expect" :
+ {
+ "name" : "Qwant",
+ "version" : "59.0",
+ "major" : "59"
+ }
+ },
+ {
+ "desc" : "Qwant",
+ "ua" : "QwantMobile/2.0 (iPad; CPU OS 15_8_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) QwantiOS/2.7.0b1 Mobile/15E148 Safari/605.1.15",
+ "expect" :
+ {
+ "name" : "Qwant",
+ "version" : "2.0",
+ "major" : "2"
+ }
+ },
{
"desc" : "Rekonq 2",
"ua" : "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.21 (KHTML, like Gecko) rekonq/2.2.1 Safari/537.21",
From bd6bb216bdff7e823bc2ec5d6ce11f664d0360da Mon Sep 17 00:00:00 2001
From: Faisal Salman
Date: Wed, 1 Oct 2025 11:25:26 +0700
Subject: [PATCH 05/20] [extensions] Add new fetcher: Discordbot, KeybaseBot,
Slackbot, Slackbot-LinkExpanding, Slack-ImgProxy, Twitterbot
---
src/enums/ua-parser-enums.js | 6 +++
src/extensions/ua-parser-extensions.js | 4 +-
test/data/ua/extension/fetcher.json | 62 +++++++++++++++++++++++++-
3 files changed, 69 insertions(+), 3 deletions(-)
diff --git a/src/enums/ua-parser-enums.js b/src/enums/ua-parser-enums.js
index ed88bdc..3dea4fb 100644
--- a/src/enums/ua-parser-enums.js
+++ b/src/enums/ua-parser-enums.js
@@ -628,6 +628,7 @@ const Extension = Object.freeze({
BLUESKY: 'Bluesky',
BUFFER_LINKPREVIEWBOT: 'BufferLinkPreviewBot',
COHERE_AI: 'Cohere-AI',
+ DISCORD_BOT: 'Discordbot',
DUCKDUCKGO_ASSISTBOT: 'DuckAssistBot',
GOOGLE_CHROME_LIGHTHOUSE: 'Chrome-Lighthouse',
GOOGLE_FEEDFETCHER: 'FeedFetcher-Google',
@@ -640,6 +641,7 @@ const Extension = Object.freeze({
HUBSPOT_PAGE_FETCHER: 'HubSpot Page Fetcher',
IFRAMELY: 'Iframely',
KAKAOTALK_SCRAP: 'kakaotalk-scrap',
+ KEYBASE_BOT: 'KeybaseBot',
META_EXTERNALFETCHER: 'meta-externalfetcher',
META_WHATSAPP: 'WhatsApp',
MICROSOFT_BINGPREVIEW: 'BingPreview',
@@ -651,6 +653,9 @@ const Extension = Object.freeze({
PERPLEXITY_USER: 'Perplexity-User',
PINTEREST_BOT: 'Pinterestbot',
SEMRUSH_SITEAUDITBOT: 'SiteAuditBot',
+ SLACK_BOT: 'Slackbot',
+ SLACK_BOT_LINKEXPANDING: 'Slackbot-LinkExpanding',
+ SLACK_IMGPROXY: 'Slack-ImgProxy',
SNAP_URL_PREVIEW: 'Snap URL Preview',
SKYPE_URIPREVIEW: 'SkypeUriPreview',
TELEGRAM_BOT: 'TelegramBot',
@@ -660,6 +665,7 @@ const Extension = Object.freeze({
VERCEL_BOT: 'Vercelbot',
VERCEL_FLAGS: 'vercelflags',
VERCEL_TRACING: 'verceltracing',
+ X_TWITTERBOT: 'Twitterbot',
YANDEX_CALENDAR: 'YandexCalendar',
YANDEX_DIRECT: 'YandexDirect',
YANDEX_DIRECTDYN: 'YandexDirectDyn',
diff --git a/src/extensions/ua-parser-extensions.js b/src/extensions/ua-parser-extensions.js
index 2caf129..8438095 100644
--- a/src/extensions/ua-parser-extensions.js
+++ b/src/extensions/ua-parser-extensions.js
@@ -283,8 +283,8 @@ const Fetchers = Object.freeze({
[NAME, VERSION, [TYPE, FETCHER]],
[
- // Google Bots / Chrome-Lighthouse / Gemini-Deep-Research / Snapchat / Vercelbot / Yandex Bots
- /((?:better uptime |telegram|vercel)bot|chrome-lighthouse|feedfetcher-google|gemini-deep-research|google(?:imageproxy|-read-aloud|-pagerenderer|producer)|snap url preview|vercel(flags|tracing|-(favicon|screenshot)-bot)|yandex(?:sitelinks|userproxy))/i
+ // Google Bots / Chrome-Lighthouse / Gemini-Deep-Research / KeybaseBot / Snapchat / Vercelbot / Yandex Bots
+ /((?:better uptime |keybase|telegram|vercel)bot|chrome-lighthouse|feedfetcher-google|gemini-deep-research|google(?:imageproxy|-read-aloud|-pagerenderer|producer)|snap url preview|vercel(flags|tracing|-(favicon|screenshot)-bot)|yandex(?:sitelinks|userproxy))/i
],
[NAME, [TYPE, FETCHER]],
],
diff --git a/test/data/ua/extension/fetcher.json b/test/data/ua/extension/fetcher.json
index 373b402..6ab908c 100644
--- a/test/data/ua/extension/fetcher.json
+++ b/test/data/ua/extension/fetcher.json
@@ -51,7 +51,7 @@
},
{
"desc" : "Blueno",
- "ua" : "acebookexternalhit/1.1 (compatible; Blueno/1.0; +http://naver.me/scrap)",
+ "ua" : "facebookexternalhit/1.1 (compatible; Blueno/1.0; +http://naver.me/scrap)",
"expect" :
{
"name" : "Blueno",
@@ -119,6 +119,16 @@
"type" : "fetcher"
}
},
+ {
+ "desc" : "Discordbot",
+ "ua" : "Mozilla/5.0 (compatible; Discordbot/2.0; +https://discordapp.com)",
+ "expect" :
+ {
+ "name" : "Discordbot",
+ "version" : "2.0",
+ "type" : "fetcher"
+ }
+ },
{
"desc" : "DuckAssistBot",
"ua" : "DuckAssistBot/1.2; (+http://duckduckgo.com/duckassistbot.html)",
@@ -239,6 +249,16 @@
"type" : "fetcher"
}
},
+ {
+ "desc" : "KeybaseBot",
+ "ua" : "Mozilla/5.0 (compatible; KeybaseBot; +https://keybase.io)",
+ "expect" :
+ {
+ "name" : "KeybaseBot",
+ "version" : "undefined",
+ "type" : "fetcher"
+ }
+ },
{
"desc" : "Meta-ExternalFetcher",
"ua" : "meta-externalfetcher/1.1 (+https://developers.facebook.com/docs/sharing/webmasters/crawler)",
@@ -309,6 +329,36 @@
"type" : "fetcher"
}
},
+ {
+ "desc" : "Slack-ImgProxy",
+ "ua" : "Slack-ImgProxy 0.19 (+https://api.slack.com/robots)",
+ "expect" :
+ {
+ "name" : "Slack-ImgProxy",
+ "version" : "0.19",
+ "type" : "fetcher"
+ }
+ },
+ {
+ "desc" : "Slackbot",
+ "ua" : "Slackbot 1.0 (+https://api.slack.com/robots)",
+ "expect" :
+ {
+ "name" : "Slackbot",
+ "version" : "1.0",
+ "type" : "fetcher"
+ }
+ },
+ {
+ "desc" : "Slackbot-LinkExpanding",
+ "ua" : "Slackbot-LinkExpanding 1.0 (+https://api.slack.com/robots)",
+ "expect" :
+ {
+ "name" : "Slackbot-LinkExpanding",
+ "version" : "1.0",
+ "type" : "fetcher"
+ }
+ },
{
"desc" : "Snap URL Preview",
"ua" : "Snap URL Preview Service; bot; snapchat; https://developers.snap.com/robots ",
@@ -339,6 +389,16 @@
"type" : "fetcher"
}
},
+ {
+ "desc" : "Twitterbot",
+ "ua" : "Twitterbot/1.0",
+ "expect" :
+ {
+ "name" : "Twitterbot",
+ "version" : "1.0",
+ "type" : "fetcher"
+ }
+ },
{
"desc" : "UptimeRobot",
"ua" : "Mozilla/5.0 (compatible; UptimeRobot/2.0; http://www.uptimerobot.com/)",
From b3281b7c12867409755eec71278f4a742f3a41f2 Mon Sep 17 00:00:00 2001
From: Faisal Salman
Date: Wed, 1 Oct 2025 12:19:46 +0700
Subject: [PATCH 06/20] [extensions] Add new crawler: Qwantbot-news,
SurdotlyBot, Swiftbot
---
src/enums/ua-parser-enums.js | 3 +++
src/extensions/ua-parser-extensions.js | 6 ++++--
test/data/ua/extension/crawler.json | 22 +++++++++++++++++++++-
3 files changed, 28 insertions(+), 3 deletions(-)
diff --git a/src/enums/ua-parser-enums.js b/src/enums/ua-parser-enums.js
index 3dea4fb..945dfaa 100644
--- a/src/enums/ua-parser-enums.js
+++ b/src/enums/ua-parser-enums.js
@@ -486,6 +486,7 @@ const Extension = Object.freeze({
DUCKDUCKGO_BOT: 'DuckDuckBot',
DUCKDUCKGO_FAVICONS_BOT: 'DuckDuckGo-Favicons-Bot',
ELASTIC: 'Elastic',
+ ELASTIC_SWIFTYPE_BOT: 'Swiftbot',
EXALEAD_EXABOT: 'Exabot',
FIRECRAWL_AGENT: 'FirecrawlAgent',
FREESPOKE: 'Freespoke',
@@ -535,6 +536,7 @@ const Extension = Object.freeze({
PERPLEXITY_BOT: 'PerplexityBot',
QIHOO_360_SPIDER: '360Spider',
QWANT_BOT: 'Qwantbot',
+ QWANT_BOT_NEWS: 'Qwantbot-news',
REPLICATE_BOT: 'Replicate-Bot',
RUNPOD_BOT: 'RunPod-Bot',
SB_INTUITIONS_BOT: 'SBIntuitionsBot',
@@ -548,6 +550,7 @@ const Extension = Object.freeze({
SOGOU_PIC_SPIDER: 'Sogou Pic Spider',
SOGOU_WEB_SPIDER: 'Sogou web spider',
STARTPAGE: 'Startpage',
+ SURLY_BOT: 'SurdotlyBot',
TIMPI_BOT: 'Timpibot',
TOGETHER_BOT: 'Together-Bot',
TURNITIN_BOT: 'TurnitinBot',
diff --git a/src/extensions/ua-parser-extensions.js b/src/extensions/ua-parser-extensions.js
index 8438095..af8ca1d 100644
--- a/src/extensions/ua-parser-extensions.js
+++ b/src/extensions/ua-parser-extensions.js
@@ -63,8 +63,10 @@ const Crawlers = Object.freeze({
// PerplexityBot - https://perplexity.ai/perplexitybot
// SBIntuitionsBot - https://www.sbintuitions.co.jp/bot/
// SeznamBot - http://napoveda.seznam.cz/seznambot-intro
+ // SurdotlyBot - http://sur.ly/bot.html
+ // Swiftbot - https://swiftype.com/swiftbot
// YepBot - https://yep.com/yepbot/
- /((?:adidx|ahrefs|amazon|bing|brave|cc|contx|coveo|criteo|dot|duckduck(?:go-favicons-)?|exa|facebook|gpt|iask|kagi|kangaroo |linkedin|mj12|mojeek|oai-search|onespot-scraper|perplexity|sbintuitions|semrush|seznam|yep)bot)\/([\w\.-]+)/i,
+ /((?:adidx|ahrefs|amazon|bing|brave|cc|contx|coveo|criteo|dot|duckduck(?:go-favicons-)?|exa|facebook|gpt|iask|kagi|kangaroo |linkedin|mj12|mojeek|oai-search|onespot-scraper|perplexity|sbintuitions|semrush|seznam|surdotly|swift|yep)bot)\/([\w\.-]+)/i,
// Algolia Crawler
/(algolia crawler(?: renderscript)?)\/?([\w\.]*)/i,
@@ -98,7 +100,7 @@ const Crawlers = Object.freeze({
/(oncrawl) mobile\/([\w\.]+)/i,
// Qwantbot - https://help.qwant.com/bot
- /(qwantbot)[-\w]*\/?([\w\.]*)/i,
+ /(qwantbot(?:-news)?)[-\w]*\/?([\w\.]*)/i,
// SemrushBot - http://www.semrush.com/bot.html
/((?:semrush|splitsignal)bot[-abcfimostw]*)\/?([\w\.-]*)/i,
diff --git a/test/data/ua/extension/crawler.json b/test/data/ua/extension/crawler.json
index d00a108..d9d7996 100644
--- a/test/data/ua/extension/crawler.json
+++ b/test/data/ua/extension/crawler.json
@@ -965,7 +965,7 @@
"ua" : "Mozilla/5.0 (compatible; Qwantbot-news/2.0; +https://help.qwant.com/bot/)",
"expect" :
{
- "name" : "Qwantbot",
+ "name" : "Qwantbot-news",
"version" : "2.0",
"type" : "crawler"
}
@@ -1100,6 +1100,26 @@
"type" : "crawler"
}
},
+ {
+ "desc" : "SurdotlyBot",
+ "ua" : "Mozilla/5.0 (compatible; SurdotlyBot/1.0; +http://sur.ly/bot.html)",
+ "expect" :
+ {
+ "name" : "SurdotlyBot",
+ "version" : "1.0",
+ "type" : "crawler"
+ }
+ },
+ {
+ "desc" : "Swiftbot",
+ "ua" : "Swiftbot/1.0 (swiftype.com)",
+ "expect" :
+ {
+ "name" : "Swiftbot",
+ "version" : "1.0",
+ "type" : "crawler"
+ }
+ },
{
"desc" : "Teoma",
"ua" : "Mozilla/2.0 (compatible; Ask Jeeves/Teoma; +http://sp.ask.com/docs/about/tech_crawling.html)",
From 6565d24567621a37be627cbfc22c2bde4522b053 Mon Sep 17 00:00:00 2001
From: Faisal Salman
Date: Sun, 5 Oct 2025 13:27:17 +0700
Subject: [PATCH 07/20] Improve OS detection: iOS 26
---
src/main/ua-parser.js | 15 +++++++++++++--
test/data/ua/os/ios.json | 18 ++++++++++++++++++
2 files changed, 31 insertions(+), 2 deletions(-)
diff --git a/src/main/ua-parser.js b/src/main/ua-parser.js
index b1abedb..e0fdd51 100755
--- a/src/main/ua-parser.js
+++ b/src/main/ua-parser.js
@@ -1253,8 +1253,19 @@
if (this.itemType != UA_RESULT) {
rgxMapper.call(this.data, this.ua, this.rgxMap);
}
- if (this.itemType == UA_BROWSER) {
- this.set(MAJOR, majorize(this.get(VERSION)));
+ switch (this.itemType) {
+ case UA_BROWSER:
+ this.set(MAJOR, majorize(this.get(VERSION)));
+ break;
+ case UA_OS:
+ if (this.get(NAME) == 'iOS' && this.get(VERSION) == '18.6') {
+ // Based on the assumption that iOS version is tightly coupled with Safari version
+ var realVersion = /\) Version\/([\d\.]+)/.exec(this.ua); // Get Safari version
+ if (realVersion && parseInt(realVersion[1].substring(0,2), 10) >= 26) {
+ this.set(VERSION, realVersion[1]); // Set as iOS version
+ }
+ }
+ break;
}
return this;
};
diff --git a/test/data/ua/os/ios.json b/test/data/ua/os/ios.json
index 1b0d636..bf10c3e 100644
--- a/test/data/ua/os/ios.json
+++ b/test/data/ua/os/ios.json
@@ -1,4 +1,22 @@
[
+ {
+ "desc" : "iOS 18.6",
+ "ua" : "Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6 Mobile/15E148 Safari/604.1",
+ "expect" :
+ {
+ "name" : "iOS",
+ "version" : "18.6"
+ }
+ },
+ {
+ "desc" : "iOS 26",
+ "ua" : "Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Mobile/15E148 Safari/604.1",
+ "expect" :
+ {
+ "name" : "iOS",
+ "version" : "26.0"
+ }
+ },
{
"desc" : "iOS in App",
"ua" : "AppName/version CFNetwork/version Darwin/version",
From 5749302c47f60f7fa0bc14b800e55ec00579b3d6 Mon Sep 17 00:00:00 2001
From: Faisal Salman
Date: Sun, 5 Oct 2025 13:55:18 +0700
Subject: [PATCH 08/20] [chore] Rename constants
---
src/main/ua-parser.js | 218 +++++++++++++++++++++---------------------
1 file changed, 109 insertions(+), 109 deletions(-)
diff --git a/src/main/ua-parser.js b/src/main/ua-parser.js
index e0fdd51..370116e 100755
--- a/src/main/ua-parser.js
+++ b/src/main/ua-parser.js
@@ -24,21 +24,21 @@
USER_AGENT = 'user-agent',
EMPTY = '',
UNKNOWN = '?',
-
- // typeof
- FUNC_TYPE = 'function',
- UNDEF_TYPE = 'undefined',
- OBJ_TYPE = 'object',
- STR_TYPE = 'string',
+ TYPEOF = {
+ FUNCTION : 'function',
+ OBJECT : 'object',
+ STRING : 'string',
+ UNDEFINED : 'undefined'
+ },
// properties
- UA_BROWSER = 'browser',
- UA_CPU = 'cpu',
- UA_DEVICE = 'device',
- UA_ENGINE = 'engine',
- UA_OS = 'os',
- UA_RESULT = 'result',
-
+ BROWSER = 'browser',
+ CPU = 'cpu',
+ DEVICE = 'device',
+ ENGINE = 'engine',
+ OS = 'os',
+ RESULT = 'result',
+
NAME = 'name',
TYPE = 'type',
VENDOR = 'vendor',
@@ -66,16 +66,16 @@
PLATFORM = 'platform',
PLATFORMVER = 'platformVersion',
BITNESS = 'bitness',
- CH_HEADER = 'sec-ch-ua',
- CH_HEADER_FULL_VER_LIST = CH_HEADER + '-full-version-list',
- CH_HEADER_ARCH = CH_HEADER + '-arch',
- CH_HEADER_BITNESS = CH_HEADER + '-' + BITNESS,
- CH_HEADER_FORM_FACTORS = CH_HEADER + '-form-factors',
- CH_HEADER_MOBILE = CH_HEADER + '-' + MOBILE,
- CH_HEADER_MODEL = CH_HEADER + '-' + MODEL,
- CH_HEADER_PLATFORM = CH_HEADER + '-' + PLATFORM,
- CH_HEADER_PLATFORM_VER = CH_HEADER_PLATFORM + '-version',
- CH_ALL_VALUES = [BRANDS, FULLVERLIST, MOBILE, MODEL, PLATFORM, PLATFORMVER, ARCHITECTURE, FORMFACTORS, BITNESS],
+ CH = 'sec-ch-ua',
+ CH_FULL_VER_LIST= CH + '-full-version-list',
+ CH_ARCH = CH + '-arch',
+ CH_BITNESS = CH + '-' + BITNESS,
+ CH_FORM_FACTORS = CH + '-form-factors',
+ CH_MOBILE = CH + '-' + MOBILE,
+ CH_MODEL = CH + '-' + MODEL,
+ CH_PLATFORM = CH + '-' + PLATFORM,
+ CH_PLATFORM_VER = CH_PLATFORM + '-version',
+ CH_ALL_VALUES = [BRANDS, FULLVERLIST, MOBILE, MODEL, PLATFORM, PLATFORMVER, ARCHITECTURE, FORMFACTORS, BITNESS],
// device vendors
AMAZON = 'Amazon',
@@ -114,7 +114,7 @@
// os
WINDOWS = 'Windows';
- var isWindow = typeof window !== UNDEF_TYPE,
+ var isWindow = typeof window !== TYPEOF.UNDEFINED,
NAVIGATOR = (isWindow && window.navigator) ?
window.navigator :
undefined,
@@ -150,7 +150,7 @@
return enums;
},
has = function (str1, str2) {
- if (typeof str1 === OBJ_TYPE && str1.length > 0) {
+ if (typeof str1 === TYPEOF.OBJECT && str1.length > 0) {
for (var i in str1) {
if (lowerize(str2) == lowerize(str1[i])) return true;
}
@@ -164,7 +164,7 @@
}
},
isString = function (val) {
- return typeof val === STR_TYPE;
+ return typeof val === TYPEOF.STRING;
},
itemListToArray = function (header) {
if (!header) return undefined;
@@ -191,7 +191,7 @@
if (!arr.hasOwnProperty(i)) continue;
var propName = arr[i];
- if (typeof propName == OBJ_TYPE && propName.length == 2) {
+ if (typeof propName == TYPEOF.OBJECT && propName.length == 2) {
this[propName[0]] = propName[1];
} else {
this[propName] = undefined;
@@ -208,7 +208,7 @@
trim = function (str, len) {
if (isString(str)) {
str = strip(/^\s\s*/, str);
- return typeof len === UNDEF_TYPE ? str : str.substring(0, UA_MAX_LENGTH);
+ return typeof len === TYPEOF.UNDEFINED ? str : str.substring(0, UA_MAX_LENGTH);
}
};
@@ -240,9 +240,9 @@
match = matches[++k];
q = props[p];
// check if given property is actually array
- if (typeof q === OBJ_TYPE && q.length > 0) {
+ if (typeof q === TYPEOF.OBJECT && q.length > 0) {
if (q.length === 2) {
- if (typeof q[1] == FUNC_TYPE) {
+ if (typeof q[1] == TYPEOF.FUNCTION) {
// assign modified match
this[q[0]] = q[1].call(this, match);
} else {
@@ -251,7 +251,7 @@
}
} else if (q.length >= 3) {
// Check whether q[1] FUNCTION or REGEX
- if (typeof q[1] === FUNC_TYPE && !(q[1].exec && q[1].test)) {
+ if (typeof q[1] === TYPEOF.FUNCTION && !(q[1].exec && q[1].test)) {
if (q.length > 3) {
this[q[0]] = match ? q[1].apply(this, q.slice(2)) : undefined;
} else {
@@ -283,7 +283,7 @@
for (var i in map) {
// check if current value is array
- if (typeof map[i] === OBJ_TYPE && map[i].length > 0) {
+ if (typeof map[i] === TYPEOF.OBJECT && map[i].length > 0) {
for (var j = 0; j < map[i].length; j++) {
if (has(map[i][j], str)) {
return (i === UNKNOWN) ? undefined : i;
@@ -1050,27 +1050,27 @@
var defaultProps = (function () {
var props = { init : {}, isIgnore : {}, isIgnoreRgx : {}, toString : {}};
setProps.call(props.init, [
- [UA_BROWSER, [NAME, VERSION, MAJOR, TYPE]],
- [UA_CPU, [ARCHITECTURE]],
- [UA_DEVICE, [TYPE, MODEL, VENDOR]],
- [UA_ENGINE, [NAME, VERSION]],
- [UA_OS, [NAME, VERSION]]
+ [BROWSER, [NAME, VERSION, MAJOR, TYPE]],
+ [CPU, [ARCHITECTURE]],
+ [DEVICE, [TYPE, MODEL, VENDOR]],
+ [ENGINE, [NAME, VERSION]],
+ [OS, [NAME, VERSION]]
]);
setProps.call(props.isIgnore, [
- [UA_BROWSER, [VERSION, MAJOR]],
- [UA_ENGINE, [VERSION]],
- [UA_OS, [VERSION]]
+ [BROWSER, [VERSION, MAJOR]],
+ [ENGINE, [VERSION]],
+ [OS, [VERSION]]
]);
setProps.call(props.isIgnoreRgx, [
- [UA_BROWSER, / ?browser$/i],
- [UA_OS, / ?os$/i]
+ [BROWSER, / ?browser$/i],
+ [OS, / ?os$/i]
]);
setProps.call(props.toString, [
- [UA_BROWSER, [NAME, VERSION]],
- [UA_CPU, [ARCHITECTURE]],
- [UA_DEVICE, [VENDOR, MODEL]],
- [UA_ENGINE, [NAME, VERSION]],
- [UA_OS, [NAME, VERSION]]
+ [BROWSER, [NAME, VERSION]],
+ [CPU, [ARCHITECTURE]],
+ [DEVICE, [VENDOR, MODEL]],
+ [ENGINE, [NAME, VERSION]],
+ [OS, [NAME, VERSION]]
]);
return props;
})();
@@ -1114,14 +1114,14 @@
return item.detectFeature().get();
};
- if (itemType != UA_RESULT) {
+ if (itemType != RESULT) {
IData.prototype.is = function (strToCheck) {
var is = false;
for (var i in this) {
if (this.hasOwnProperty(i) && !has(is_ignoreProps, i) && lowerize(is_ignoreRgx ? strip(is_ignoreRgx, this[i]) : this[i]) == lowerize(is_ignoreRgx ? strip(is_ignoreRgx, strToCheck) : strToCheck)) {
is = true;
- if (strToCheck != UNDEF_TYPE) break;
- } else if (strToCheck == UNDEF_TYPE && is) {
+ if (strToCheck != TYPEOF.UNDEFINED) break;
+ } else if (strToCheck == TYPEOF.UNDEFINED && is) {
is = !is;
break;
}
@@ -1131,11 +1131,11 @@
IData.prototype.toString = function () {
var str = EMPTY;
for (var i in toString_props) {
- if (typeof(this[toString_props[i]]) !== UNDEF_TYPE) {
+ if (typeof(this[toString_props[i]]) !== TYPEOF.UNDEFINED) {
str += (str ? ' ' : EMPTY) + this[toString_props[i]];
}
}
- return str || UNDEF_TYPE;
+ return str || TYPEOF.UNDEFINED;
};
}
@@ -1171,19 +1171,19 @@
setProps.call(this, CH_ALL_VALUES);
if (isHttpUACH) {
setProps.call(this, [
- [BRANDS, itemListToArray(uach[CH_HEADER])],
- [FULLVERLIST, itemListToArray(uach[CH_HEADER_FULL_VER_LIST])],
- [MOBILE, /\?1/.test(uach[CH_HEADER_MOBILE])],
- [MODEL, stripQuotes(uach[CH_HEADER_MODEL])],
- [PLATFORM, stripQuotes(uach[CH_HEADER_PLATFORM])],
- [PLATFORMVER, stripQuotes(uach[CH_HEADER_PLATFORM_VER])],
- [ARCHITECTURE, stripQuotes(uach[CH_HEADER_ARCH])],
- [FORMFACTORS, itemListToArray(uach[CH_HEADER_FORM_FACTORS])],
- [BITNESS, stripQuotes(uach[CH_HEADER_BITNESS])]
+ [BRANDS, itemListToArray(uach[CH])],
+ [FULLVERLIST, itemListToArray(uach[CH_FULL_VER_LIST])],
+ [MOBILE, /\?1/.test(uach[CH_MOBILE])],
+ [MODEL, stripQuotes(uach[CH_MODEL])],
+ [PLATFORM, stripQuotes(uach[CH_PLATFORM])],
+ [PLATFORMVER, stripQuotes(uach[CH_PLATFORM_VER])],
+ [ARCHITECTURE, stripQuotes(uach[CH_ARCH])],
+ [FORMFACTORS, itemListToArray(uach[CH_FORM_FACTORS])],
+ [BITNESS, stripQuotes(uach[CH_BITNESS])]
]);
} else {
for (var prop in uach) {
- if(this.hasOwnProperty(prop) && typeof uach[prop] !== UNDEF_TYPE) this[prop] = uach[prop];
+ if(this.hasOwnProperty(prop) && typeof uach[prop] !== TYPEOF.UNDEFINED) this[prop] = uach[prop];
}
}
}
@@ -1208,30 +1208,30 @@
this.detectFeature = function () {
if (NAVIGATOR && NAVIGATOR.userAgent == this.ua) {
switch (this.itemType) {
- case UA_BROWSER:
+ case BROWSER:
// Brave-specific detection
- if (NAVIGATOR.brave && typeof NAVIGATOR.brave.isBrave == FUNC_TYPE) {
+ if (NAVIGATOR.brave && typeof NAVIGATOR.brave.isBrave == TYPEOF.FUNCTION) {
this.set(NAME, 'Brave');
}
break;
- case UA_DEVICE:
+ case DEVICE:
// Chrome-specific detection: check for 'mobile' value of navigator.userAgentData
if (!this.get(TYPE) && NAVIGATOR_UADATA && NAVIGATOR_UADATA[MOBILE]) {
this.set(TYPE, MOBILE);
}
// iPadOS-specific detection: identified as Mac, but has some iOS-only properties
- if (this.get(MODEL) == 'Macintosh' && NAVIGATOR && typeof NAVIGATOR.standalone !== UNDEF_TYPE && NAVIGATOR.maxTouchPoints && NAVIGATOR.maxTouchPoints > 2) {
+ if (this.get(MODEL) == 'Macintosh' && NAVIGATOR && typeof NAVIGATOR.standalone !== TYPEOF.UNDEFINED && NAVIGATOR.maxTouchPoints && NAVIGATOR.maxTouchPoints > 2) {
this.set(MODEL, 'iPad')
.set(TYPE, TABLET);
}
break;
- case UA_OS:
+ case OS:
// Chrome-specific detection: check for 'platform' value of navigator.userAgentData
if (!this.get(NAME) && NAVIGATOR_UADATA && NAVIGATOR_UADATA[PLATFORM]) {
this.set(NAME, NAVIGATOR_UADATA[PLATFORM]);
}
break;
- case UA_RESULT:
+ case RESULT:
var data = this.data;
var detect = function (itemType) {
return data[itemType]
@@ -1239,25 +1239,25 @@
.detectFeature()
.get();
};
- this.set(UA_BROWSER, detect(UA_BROWSER))
- .set(UA_CPU, detect(UA_CPU))
- .set(UA_DEVICE, detect(UA_DEVICE))
- .set(UA_ENGINE, detect(UA_ENGINE))
- .set(UA_OS, detect(UA_OS));
+ this.set(BROWSER, detect(BROWSER))
+ .set(CPU, detect(CPU))
+ .set(DEVICE, detect(DEVICE))
+ .set(ENGINE, detect(ENGINE))
+ .set(OS, detect(OS));
}
}
return this;
};
this.parseUA = function () {
- if (this.itemType != UA_RESULT) {
+ if (this.itemType != RESULT) {
rgxMapper.call(this.data, this.ua, this.rgxMap);
}
switch (this.itemType) {
- case UA_BROWSER:
+ case BROWSER:
this.set(MAJOR, majorize(this.get(VERSION)));
break;
- case UA_OS:
+ case OS:
if (this.get(NAME) == 'iOS' && this.get(VERSION) == '18.6') {
// Based on the assumption that iOS version is tightly coupled with Safari version
var realVersion = /\) Version\/([\d\.]+)/.exec(this.ua); // Get Safari version
@@ -1275,14 +1275,14 @@
rgxMap = this.rgxMap;
switch (this.itemType) {
- case UA_BROWSER:
- case UA_ENGINE:
+ case BROWSER:
+ case ENGINE:
var brands = uaCH[FULLVERLIST] || uaCH[BRANDS], prevName;
if (brands) {
for (var i=0; i
Date: Sun, 5 Oct 2025 21:01:32 +0700
Subject: [PATCH 09/20] [fix] setUA(): remove trailing space from user-agent
string
---
src/main/ua-parser.js | 9 +++------
test/unit/main.js | 8 ++++++++
2 files changed, 11 insertions(+), 6 deletions(-)
diff --git a/src/main/ua-parser.js b/src/main/ua-parser.js
index 370116e..8292272 100755
--- a/src/main/ua-parser.js
+++ b/src/main/ua-parser.js
@@ -206,10 +206,8 @@
return strip(/\\?\"/g, str);
},
trim = function (str, len) {
- if (isString(str)) {
- str = strip(/^\s\s*/, str);
- return typeof len === TYPEOF.UNDEFINED ? str : str.substring(0, UA_MAX_LENGTH);
- }
+ str = strip(/^\s\s*/, String(str));
+ return typeof len === TYPEOF.UNDEFINED ? str : str.substring(0, len);
};
///////////////
@@ -1464,8 +1462,7 @@
['getResult', createItemFunc(RESULT)],
['getUA', function () { return userAgent; }],
['setUA', function (ua) {
- if (isString(ua))
- userAgent = ua.length > UA_MAX_LENGTH ? trim(ua, UA_MAX_LENGTH) : ua;
+ if (isString(ua)) userAgent = trim(ua, UA_MAX_LENGTH);
return this;
}]
])
diff --git a/test/unit/main.js b/test/unit/main.js
index b99e832..231b5c5 100644
--- a/test/unit/main.js
+++ b/test/unit/main.js
@@ -163,6 +163,14 @@ describe('Extending Regex', function () {
assert.deepEqual(myParser3.setUA(myUA2).getDevice(), {vendor: "MyTab", model: "14 Pro Max", type: "tablet"});
});
+describe('User-agent with trailing space', function () {
+ it ('trailing space will be trimmed', function () {
+ const uastring = ' Opera/9.21 (Windows NT 5.1; U; ru) ';
+ const { ua } = UAParser(uastring);
+ assert.equal(ua, 'Opera/9.21 (Windows NT 5.1; U; ru) ');
+ });
+});
+
describe('User-agent length', function () {
var UA_MAX_LENGTH = 500;
From 4e6259ad7fcf4e913721c69cf66690bd408dd084 Mon Sep 17 00:00:00 2001
From: Faisal Salman
Date: Sun, 5 Oct 2025 23:28:57 +0700
Subject: [PATCH 10/20] [feat] Add new CLI feature: processing batch user-agent
data from file and output as JSON
---
CHANGELOG.md | 8 +++-
package.json | 2 +-
script/cli.js | 94 ++++++++++++++++++++++++++++++++++++++-
test/unit/cli/cli.spec.js | 37 +++++++++++++++
test/unit/cli/input.txt | 2 +
test/unit/cli/output.json | 32 +++++++++++++
6 files changed, 170 insertions(+), 5 deletions(-)
create mode 100644 test/unit/cli/cli.spec.js
create mode 100644 test/unit/cli/input.txt
create mode 100644 test/unit/cli/output.json
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8ea4bb8..fb55cf8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,8 +23,12 @@
- **Support for Custom/Predefined Extensions:**
- Pass custom regexes or predefined extensions as a list to `UAParser()`
-- **Support for CLI Parsing:**
- - Parse a user-agent directly from the command line using `npx ua-parser-js "[User-Agent]"`
+- **Support for CLI Processing:**
+ - Directly parse user-agent strings from the command line:
+ `npx ua-parser-js ""`
+ - Process batch data from files:
+ `npx ua-parser-js --input-file=log.txt >> result.json` or
+ `npx ua-parser-js --input-file=log.txt --output-file=result.json`
- **Enhanced Detection with Client Hints:**
- `withClientHints()`: Improves detection accuracy by leveraging client hints
diff --git a/package.json b/package.json
index 580bbcc..06b4b7b 100755
--- a/package.json
+++ b/package.json
@@ -220,7 +220,7 @@
"test:eslint": "eslint src && eslint script",
"test:jshint": "jshint src/main",
"test:lockfile-lint": "npx lockfile-lint -p package-lock.json",
- "test:mocha": "mocha test/unit",
+ "test:mocha": "mocha --recursive test/unit",
"test:playwright": "npx playwright install && playwright test test/e2e --browser all"
},
"dependencies": {
diff --git a/script/cli.js b/script/cli.js
index 785a104..368d6c5 100755
--- a/script/cli.js
+++ b/script/cli.js
@@ -1,4 +1,94 @@
#!/usr/bin/env node
-const UAParser = require('ua-parser-js');
-console.log(JSON.stringify(process.argv.slice(2).map(ua => UAParser(ua)), null, 4));
\ No newline at end of file
+try {
+ const fs = require('node:fs');
+ const path = require('node:path');
+ const { performance } = require('node:perf_hooks');
+ const readline = require('node:readline');
+ const { parseArgs } = require('node:util');
+ const UAParser = require('ua-parser-js');
+ const { Bots, Emails, ExtraDevices, InApps, Vehicles } = require('ua-parser-js/extensions');
+
+ if (!process.argv[2].startsWith('-')) {
+
+ const results = process.argv.slice(2).map(ua => UAParser(ua));
+ console.log(JSON.stringify(results, null, 4));
+ process.exit(0);
+
+ } else if (['-h', '--help'].includes(process.argv[2])) {
+
+ console.log('Usage: npx ua-parser-js ');
+ console.log(' or npx ua-parser-js --input-file [--output-file ]');
+ console.log('-i, --input-file');
+ console.log('-o, --output-file');
+ process.exit(0);
+
+ } else {
+
+ const startPerf = performance.now();
+ const {
+ values: {
+ 'input-file': inputFile,
+ 'output-file': outputFile
+ },
+ } = parseArgs({
+ options: {
+ 'input-file': { type: 'string', short: 'i' },
+ 'output-file': { type: 'string', short: 'o' }
+ }
+ });
+
+ if (!inputFile) {
+ console.error('Input file must be present');
+ process.exit(1);
+ }
+
+ const inputPath = path.resolve(__dirname, inputFile);
+ const outputPath = outputFile ? path.resolve(__dirname, outputFile) : null;
+
+ if (!fs.existsSync(inputPath)) {
+ console.error(`Input file not found: ${inputPath}`);
+ process.exit(1);
+ }
+
+ const inputStream = fs.createReadStream(inputPath, 'utf8');
+ const rl = readline.createInterface({
+ input: inputStream,
+ crlfDelay: Infinity
+ });
+
+ const outputStream = outputPath ? fs.createWriteStream(outputPath, { encoding : 'utf8' }) : process.stdout;
+
+ const uap = new UAParser([Bots, Emails, ExtraDevices, InApps, Vehicles]);
+ let lineNumber = 0;
+
+ outputStream.write('[\n');
+
+ rl.on('line', line => {
+ const result = uap.setUA(line).getResult();
+ const json = JSON.stringify(result, null, 4);
+ if (lineNumber > 0) outputStream.write(',\n');
+ outputStream.write(json);
+ lineNumber++;
+ });
+
+ rl.on('close', () => {
+ outputStream.write('\n]');
+ if (outputPath) {
+ outputStream.end(() => {
+ const finishPerf = performance.now();
+ console.log(`Done!`);
+ console.log(`Number of lines found: ${lineNumber}`);
+ console.log(`Task finished in: ${(finishPerf - startPerf).toFixed(3)}ms`);
+ console.log(`Output written to: ${outputPath}`);
+ process.exit(0);
+ });
+ } else {
+ process.exit(0);
+ }
+ });
+ }
+} catch (err) {
+ console.error(err);
+ process.exit(1);
+}
\ No newline at end of file
diff --git a/test/unit/cli/cli.spec.js b/test/unit/cli/cli.spec.js
new file mode 100644
index 0000000..7365fe4
--- /dev/null
+++ b/test/unit/cli/cli.spec.js
@@ -0,0 +1,37 @@
+const assert = require('node:assert');
+const { exec } = require('node:child_process');
+const fs = require('node:fs');
+const { UAParser } = require('../../../src/main/ua-parser');
+const uap = new UAParser();
+
+describe('npx ua-parser-js ', () => {
+ it ('print result to stdout', () => {
+ exec('npx ua-parser-js "TEST"', (err, stdout, stderr) => {
+ assert.deepEqual(JSON.parse(stdout), JSON.parse(JSON.stringify([uap.setUA("TEST").getResult()])));
+ });
+ })
+});
+
+describe('npx ua-parser-js --input-file=', () => {
+ it ('load file and print result to stdout', () => {
+ exec('npx ua-parser-js --input-file="../test/unit/cli/input.txt"', (err, stdout, stderr) => {
+ assert.deepEqual(JSON.parse(stdout), JSON.parse(JSON.stringify([
+ uap.setUA('Opera/9.25 (Windows NT 6.0; U; ru)').getResult(),
+ uap.setUA('Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)').getResult()
+ ])));
+ });
+ });
+});
+
+describe('npx ua-parser-js --input-file= --output-file=', () => {
+ it ('load file and save result to file', () => {
+ exec('npx ua-parser-js --input-file="../test/unit/cli/input.txt" --output-file="../test/unit/cli/output.json"', (err, stdout, stderr) => {
+ fs.readFile('test/unit/cli/output.json', (err, data) => {
+ assert.deepEqual(JSON.parse(data), JSON.parse(JSON.stringify([
+ uap.setUA('Opera/9.25 (Windows NT 6.0; U; ru)').getResult(),
+ uap.setUA('Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)').getResult()
+ ])));
+ });
+ });
+ });
+});
diff --git a/test/unit/cli/input.txt b/test/unit/cli/input.txt
new file mode 100644
index 0000000..2d50fb8
--- /dev/null
+++ b/test/unit/cli/input.txt
@@ -0,0 +1,2 @@
+Opera/9.25 (Windows NT 6.0; U; ru)
+Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)
\ No newline at end of file
diff --git a/test/unit/cli/output.json b/test/unit/cli/output.json
new file mode 100644
index 0000000..d5b92c8
--- /dev/null
+++ b/test/unit/cli/output.json
@@ -0,0 +1,32 @@
+[
+{
+ "ua": "Opera/9.25 (Windows NT 6.0; U; ru)",
+ "browser": {
+ "name": "Opera",
+ "version": "9.25",
+ "major": "9"
+ },
+ "cpu": {},
+ "device": {},
+ "engine": {},
+ "os": {
+ "name": "Windows",
+ "version": "Vista"
+ }
+},
+{
+ "ua": "Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)",
+ "browser": {
+ "name": "IE",
+ "version": "5.5",
+ "major": "5"
+ },
+ "cpu": {},
+ "device": {},
+ "engine": {},
+ "os": {
+ "name": "Windows",
+ "version": "NT"
+ }
+}
+]
\ No newline at end of file
From fc5125042c8123d213698eac8fe3c4c02a8356ed Mon Sep 17 00:00:00 2001
From: Faisal Salman
Date: Mon, 6 Oct 2025 11:51:22 +0700
Subject: [PATCH 11/20] Improve browser detection: Mozilla, Pale Moon
---
src/main/ua-parser.js | 8 ++++----
test/data/ua/browser/browser-all.json | 20 ++++++++++++++++++++
2 files changed, 24 insertions(+), 4 deletions(-)
diff --git a/src/main/ua-parser.js b/src/main/ua-parser.js
index 8292272..9f50fad 100755
--- a/src/main/ua-parser.js
+++ b/src/main/ua-parser.js
@@ -375,8 +375,8 @@
/(avant|iemobile|slim(?:browser|boat|jet))[\/ ]?([\d\.]*)/i, // Avant/IEMobile/SlimBrowser/SlimBoat/Slimjet
/(?:ms|\()(ie) ([\w\.]+)/i, // Internet Explorer
- // Blink/Webkit/KHTML based // Flock/RockMelt/Midori/Epiphany/Silk/Skyfire/Bolt/Iron/Iridium/PhantomJS/Bowser/QupZilla/Falkon/LG Browser/Otter/qutebrowser/Dooble
- /(flock|rockmelt|midori|epiphany|silk|skyfire|ovibrowser|bolt|iron|vivaldi|iridium|phantomjs|bowser|qupzilla|falkon|rekonq|puffin|brave|whale(?!.+naver)|qqbrowserlite|duckduckgo|klar|helio|(?=comodo_)?dragon|otter|dooble|(?:lg |qute)browser)\/([-\w\.]+)/i,
+ // Blink/Webkit/KHTML based // Flock/RockMelt/Midori/Epiphany/Silk/Skyfire/Bolt/Iron/Iridium/PhantomJS/Bowser/QupZilla/Falkon/LG Browser/Otter/qutebrowser/Dooble/Palemoon
+ /(flock|rockmelt|midori|epiphany|silk|skyfire|ovibrowser|bolt|iron|vivaldi|iridium|phantomjs|bowser|qupzilla|falkon|rekonq|puffin|brave|whale(?!.+naver)|qqbrowserlite|duckduckgo|klar|helio|(?=comodo_)?dragon|otter|dooble|(?:lg |qute)browser|palemoon)\/([-\w\.]+)/i,
// Rekonq/Puffin/Brave/Whale/QQBrowserLite/QQ//Vivaldi/DuckDuckGo/Klar/Helio/Dragon
/(heytap|ovi|115|surf|qwant)browser\/([\d\.]+)/i, // HeyTap/Ovi/115/Surf
/(qwant)(?:ios|mobile)\/([\d\.]+)/i, // Qwant
@@ -505,10 +505,10 @@
/(swiftfox)/i, // Swiftfox
/(icedragon|iceweasel|camino|chimera|fennec|maemo browser|minimo|conkeror)[\/ ]?([\w\.\+]+)/i,
// IceDragon/Iceweasel/Camino/Chimera/Fennec/Maemo/Minimo/Conkeror
- /(seamonkey|k-meleon|icecat|iceape|firebird|phoenix|palemoon|basilisk|waterfox)\/([-\w\.]+)$/i,
+ /(seamonkey|k-meleon|icecat|iceape|firebird|phoenix|basilisk|waterfox)\/([-\w\.]+)$/i,
// Firefox/SeaMonkey/K-Meleon/IceCat/IceApe/Firebird/Phoenix
/(firefox)\/([\w\.]+)/i, // Other Firefox-based
- /(mozilla)\/([\w\.]+) .+rv\:.+gecko\/\d+/i, // Mozilla
+ /(mozilla)\/([\w\.]+(?= .+rv\:.+gecko\/\d+)|[0-4][\w\.]+(?!.+compatible))/i, // Mozilla
// Other
/(amaya|dillo|doris|icab|ladybird|lynx|mosaic|netsurf|obigo|polaris|w3m|(?:go|ice|up)[\. ]?browser)[-\/ ]?v?([\w\.]+)/i,
diff --git a/test/data/ua/browser/browser-all.json b/test/data/ua/browser/browser-all.json
index 5293bc7..bea4eb9 100644
--- a/test/data/ua/browser/browser-all.json
+++ b/test/data/ua/browser/browser-all.json
@@ -611,6 +611,16 @@
"major" : "55"
}
},
+ {
+ "desc" : "PaleMoon",
+ "ua" : "(Windows NT 6.2; WOW64) KHTML/4.11 Gecko/20130308 Firefox/23.0 (PaleMoon/20.3)",
+ "expect" :
+ {
+ "name" : "PaleMoon",
+ "version" : "20.3",
+ "major" : "20"
+ }
+ },
{
"desc" : "PaleMoon",
"ua" : "Mozilla/5.0 (X11; Linux x86_64; rv:52.9) Gecko/20100101 Goanna/3.4 Firefox/52.9 PaleMoon/27.6.1",
@@ -1338,6 +1348,16 @@
"major" : "5"
}
},
+ {
+ "desc" : "Mozilla",
+ "ua" : "Mozilla/2.02 [fr] (WinNT; I)",
+ "expect" :
+ {
+ "name" : "Mozilla",
+ "version" : "2.02",
+ "major" : "2"
+ }
+ },
{
"desc" : "MSIE",
"ua" : "Mozilla/4.0 (compatible; MSIE 5.0b1; Mac_PowerPC)",
From 44165a6e01cc742f62433ad86de09daa7388ff58 Mon Sep 17 00:00:00 2001
From: Faisal Salman
Date: Mon, 6 Oct 2025 11:59:55 +0700
Subject: [PATCH 12/20] Improve CPU detection: 68k
---
src/main/ua-parser.js | 2 ++
test/data/ua/cpu/cpu-all.json | 10 +++++++++-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/src/main/ua-parser.js b/src/main/ua-parser.js
index 9f50fad..8e35764 100755
--- a/src/main/ua-parser.js
+++ b/src/main/ua-parser.js
@@ -546,6 +546,8 @@
/((ppc|powerpc)(64)?)( mac|;|\))/i, // PowerPC
/(?:osf1|[freopnt]{3,4}bsd) (alpha)/i // Alpha
], [[ARCHITECTURE, /ower/, EMPTY, lowerize]], [
+ /mc680.0/i
+ ], [[ARCHITECTURE, '68k']], [
/winnt.+\[axp/i
], [[ARCHITECTURE, 'alpha']]
],
diff --git a/test/data/ua/cpu/cpu-all.json b/test/data/ua/cpu/cpu-all.json
index e9b749e..6a992e9 100644
--- a/test/data/ua/cpu/cpu-all.json
+++ b/test/data/ua/cpu/cpu-all.json
@@ -289,7 +289,15 @@
},
{
"desc" : "68k",
- "ua" : "'Mozilla/1.1 (Macintosh; U; 68K)'",
+ "ua" : "Mozilla/1.1 (Macintosh; U; 68K)",
+ "expect" :
+ {
+ "architecture" : "68k"
+ }
+ },
+ {
+ "desc" : "MC680x0",
+ "ua" : "AmigaVoyager/3.2 (AmigaOS/MC680x0)",
"expect" :
{
"architecture" : "68k"
From ae7b5e15e5159b5c86fde4ad447e32c407940fb4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20R=C3=A8gne?=
Date: Mon, 6 Oct 2025 15:59:29 +0200
Subject: [PATCH 13/20] chore: Replace Undici by native Headers (#805)
---
package-lock.json | 12 +-----------
package.json | 3 +--
src/main/ua-parser.d.ts | 1 -
test/unit/main.js | 1 -
4 files changed, 2 insertions(+), 15 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index b9334f2..17355c1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -25,8 +25,7 @@
"dependencies": {
"detect-europe-js": "^0.1.2",
"is-standalone-pwa": "^0.1.1",
- "ua-is-frozen": "^0.1.2",
- "undici": "^7.12.0"
+ "ua-is-frozen": "^0.1.2"
},
"bin": {
"ua-parser-js": "script/cli.js"
@@ -2693,15 +2692,6 @@
"node": ">=0.8.0"
}
},
- "node_modules/undici": {
- "version": "7.12.0",
- "resolved": "https://registry.npmjs.org/undici/-/undici-7.12.0.tgz",
- "integrity": "sha512-GrKEsc3ughskmGA9jevVlIOPMiiAHJ4OFUtaAH+NhfTUSiZ1wMPIQqQvAJUrJspFXJt3EBWgpAeoHEDVT1IBug==",
- "license": "MIT",
- "engines": {
- "node": ">=20.18.1"
- }
- },
"node_modules/validate-npm-package-license": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
diff --git a/package.json b/package.json
index 06b4b7b..1c3fd50 100755
--- a/package.json
+++ b/package.json
@@ -226,8 +226,7 @@
"dependencies": {
"detect-europe-js": "^0.1.2",
"is-standalone-pwa": "^0.1.1",
- "ua-is-frozen": "^0.1.2",
- "undici": "^7.12.0"
+ "ua-is-frozen": "^0.1.2"
},
"devDependencies": {
"@babel/parser": "7.15.8",
diff --git a/src/main/ua-parser.d.ts b/src/main/ua-parser.d.ts
index 09c11ee..0dcbd6a 100644
--- a/src/main/ua-parser.d.ts
+++ b/src/main/ua-parser.d.ts
@@ -2,7 +2,6 @@
// Project: https://github.com/faisalman/ua-parser-js
// Definitions by: Faisal Salman
-import type { Headers } from "undici";
import { BrowserType, CPUArch, DeviceType, EngineName } from "../enums/ua-parser-enums";
declare namespace UAParser {
diff --git a/test/unit/main.js b/test/unit/main.js
index 231b5c5..01e0d4e 100644
--- a/test/unit/main.js
+++ b/test/unit/main.js
@@ -10,7 +10,6 @@ var cpus = require('../data/ua/cpu/cpu-all.json');
var devices = readJsonFiles('test/data/ua/device');
var engines = require('../data/ua/engine/engine-all.json');
var os = readJsonFiles('test/data/ua/os');
-var { Headers } = require('undici');
function readJsonFiles(dir) {
var list = [];
From 4c935c0139c10c2115f1c1f5856bb48620d8f284 Mon Sep 17 00:00:00 2001
From: Faisal Salman
Date: Mon, 6 Oct 2025 22:07:35 +0700
Subject: [PATCH 14/20] Improve device detection: Nokia
---
src/main/ua-parser.js | 2 +-
test/data/ua/device/nokia.json | 9 +++++++++
2 files changed, 10 insertions(+), 1 deletion(-)
diff --git a/src/main/ua-parser.js b/src/main/ua-parser.js
index 8e35764..297804c 100755
--- a/src/main/ua-parser.js
+++ b/src/main/ua-parser.js
@@ -658,7 +658,7 @@
/(nokia) (t[12][01])/i
], [VENDOR, MODEL, [TYPE, TABLET]], [
/(?:maemo|nokia).*(n900|lumia \d+|rm-\d+)/i,
- /nokia[-_ ]?(([-\w\. ]*))/i
+ /nokia[-_ ]?(([-\w\. ]*?))( bui|\)|;|\/)/i
], [[MODEL, /_/g, ' '], [TYPE, MOBILE], [VENDOR, 'Nokia']], [
// Google
diff --git a/test/data/ua/device/nokia.json b/test/data/ua/device/nokia.json
index 375cfcc..f1ff7d6 100644
--- a/test/data/ua/device/nokia.json
+++ b/test/data/ua/device/nokia.json
@@ -1,4 +1,13 @@
[
+ {
+ "desc": "Nokia 1",
+ "ua": "Mozilla/5.0 (Linux; Android 10; Nokia 1 Build/QP1A.190711.020) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.5304.15 Mobile Safari/537.36",
+ "expect": {
+ "vendor": "Nokia",
+ "model": "1",
+ "type": "mobile"
+ }
+ },
{
"desc": "Nokia3xx",
"ua": "Nokia303/14.87 CLDC-1.1",
From 9ba4d2b207d19e741c537e7cd0f3f1973f49e5ea Mon Sep 17 00:00:00 2001
From: Faisal Salman
Date: Mon, 6 Oct 2025 22:08:56 +0700
Subject: [PATCH 15/20] Add new device vendor: Hisense -
https://global.hisense.com/
---
src/enums/ua-parser-enums.js | 1 +
src/main/ua-parser.js | 1 +
test/data/ua/device/hisense.json | 20 ++++++++++++++++++++
3 files changed, 22 insertions(+)
create mode 100644 test/data/ua/device/hisense.json
diff --git a/src/enums/ua-parser-enums.js b/src/enums/ua-parser-enums.js
index 945dfaa..8955439 100644
--- a/src/enums/ua-parser-enums.js
+++ b/src/enums/ua-parser-enums.js
@@ -240,6 +240,7 @@ const DeviceVendor = Object.freeze({
GEEKSPHONE: 'GeeksPhone',
GENERIC: 'Generic',
GOOGLE: 'Google',
+ HISENSE: 'Hisense',
HMD: 'HMD',
HP: 'HP',
HTC: 'HTC',
diff --git a/src/main/ua-parser.js b/src/main/ua-parser.js
index 297804c..afff9b8 100755
--- a/src/main/ua-parser.js
+++ b/src/main/ua-parser.js
@@ -773,6 +773,7 @@
/(hp) ([\w ]+\w)/i, // HP iPAQ
/(microsoft); (lumia[\w ]+)/i, // Microsoft Lumia
/(oppo) ?([\w ]+) bui/i, // OPPO
+ /(hisense) ([ehv][\w ]+)\)/i, // Hisense
/droid[^;]+; (philips)[_ ]([sv-x][\d]{3,4}[xz]?)/i // Philips
], [VENDOR, MODEL, [TYPE, MOBILE]], [
diff --git a/test/data/ua/device/hisense.json b/test/data/ua/device/hisense.json
new file mode 100644
index 0000000..d13f6c9
--- /dev/null
+++ b/test/data/ua/device/hisense.json
@@ -0,0 +1,20 @@
+[
+ {
+ "desc": "Hisense E50 Lite",
+ "ua": "Mozilla/5.0 (Linux; Android 11; Hisense E50 Lite) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.5249.126 Mobile Safari/537.36",
+ "expect": {
+ "vendor": "Hisense",
+ "model": "E50 Lite",
+ "type": "mobile"
+ }
+ },
+ {
+ "desc": "Hisense V40s",
+ "ua": "Mozilla/5.0 (Linux; Android 11; Hisense V40s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.5195.125 Mobile Safari/537.36",
+ "expect": {
+ "vendor": "Hisense",
+ "model": "V40s",
+ "type": "mobile"
+ }
+ }
+]
\ No newline at end of file
From 5349bb52ed30f3048800ca92d5be6f264cf9f348 Mon Sep 17 00:00:00 2001
From: Faisal Salman
Date: Tue, 7 Oct 2025 14:51:46 +0700
Subject: [PATCH 16/20] Improve device detection: BlackBerry, Huawei, Xiaomi
---
src/main/ua-parser.js | 12 +++++-----
test/data/ua/device/blackberry.json | 22 ++++++++++++++++--
test/data/ua/device/huawei.json | 36 +++++++++++++++++++++++++++++
test/data/ua/device/xiaomi.json | 36 +++++++++++++++++++++++++++++
4 files changed, 98 insertions(+), 8 deletions(-)
diff --git a/src/main/ua-parser.js b/src/main/ua-parser.js
index afff9b8..27e79ce 100755
--- a/src/main/ua-parser.js
+++ b/src/main/ua-parser.js
@@ -588,13 +588,13 @@
// Huawei
/\b((?:ag[rs][2356]?k?|bah[234]?|bg[2o]|bt[kv]|cmr|cpn|db[ry]2?|jdn2|got|kob2?k?|mon|pce|scm|sht?|[tw]gr|vrd)-[ad]?[lw][0125][09]b?|605hw|bg2-u03|(?:gem|fdr|m2|ple|t1)-[7a]0[1-4][lu]|t1-a2[13][lw]|mediapad[\w\. ]*(?= bui|\)))\b(?!.+d\/s)/i
], [MODEL, [VENDOR, HUAWEI], [TYPE, TABLET]], [
- /(?:huawei)([-\w ]+)[;\)]/i,
- /\b(nexus 6p|\w{2,4}e?-[atu]?[ln][\dx][012359c][adn]?)\b(?!.+d\/s)/i
+ /(?:huawei) ?([-\w ]+)[;\)]/i,
+ /\b(nexus 6p|\w{2,4}e?-[atu]?[ln][\dx][\dc][adnt]?)\b(?!.+d\/s)/i
], [MODEL, [VENDOR, HUAWEI], [TYPE, MOBILE]], [
// Xiaomi
/oid[^\)]+; (2[\dbc]{4}(182|283|rp\w{2})[cgl]|m2105k81a?c)(?: bui|\))/i,
- /\b((?:red)?mi[-_ ]?pad[\w- ]*)(?: bui|\))/i // Mi Pad tablets
+ /\b(?:xiao)?((?:red)?mi[-_ ]?pad[\w- ]*)(?: bui|\))/i // Mi Pad tablets
],[[MODEL, /_/g, ' '], [VENDOR, XIAOMI], [TYPE, TABLET]], [
/\b(poco[\w ]+|m2\d{3}j\d\d[a-z]{2})(?: bui|\))/i, // Xiaomi POCO
@@ -602,7 +602,7 @@
/\b(hm[-_ ]?note?[_ ]?(?:\d\w)?) bui/i, // Xiaomi Hongmi
/\b(redmi[\-_ ]?(?:note|k)?[\w_ ]+)(?: bui|\))/i, // Xiaomi Redmi
/oid[^\)]+; (m?[12][0-389][01]\w{3,6}[c-y])( bui|; wv|\))/i, // Xiaomi Redmi 'numeric' models
- /\b(mi[-_ ]?(?:a\d|one|one[_ ]plus|note lte|max|cc)?[_ ]?(?:\d?\w?)[_ ]?(?:plus|se|lite|pro)?)(?: bui|\))/i, // Xiaomi Mi
+ /\b(mi[-_ ]?(?:a\d|one|one[_ ]plus|note|max|cc)?[_ ]?(?:\d{0,2}\w?)[_ ]?(?:plus|se|lite|pro)?( 5g|lte)?)(?: bui|\))/i, // Xiaomi Mi
/ ([\w ]+) miui\/v?\d/i
], [[MODEL, /_/g, ' '], [VENDOR, XIAOMI], [TYPE, MOBILE]], [
@@ -689,7 +689,7 @@
/(playbook);[-\w\),; ]+(rim)/i // BlackBerry PlayBook
], [MODEL, VENDOR, [TYPE, TABLET]], [
/\b((?:bb[a-f]|st[hv])100-\d)/i,
- /\(bb10; (\w+)/i // BlackBerry 10
+ /(?:blackberry|\(bb10;) (\w+)/i
], [MODEL, [VENDOR, BLACKBERRY], [TYPE, MOBILE]], [
// Asus
@@ -913,7 +913,7 @@
], [MODEL, [TYPE, SMARTTV]], [
/\b((4k|android|smart|opera)[- ]?tv|tv; rv:|large screen[\w ]+safari)\b/i
], [[TYPE, SMARTTV]], [
- /droid .+?; ([^;]+?)(?: bui|; wv\)|\) applew).+?(mobile|vr|\d) safari/i
+ /droid .+?; ([^;]+?)(?: bui|; wv\)|\) applew|; hmsc).+?(mobile|vr|\d) safari/i
], [MODEL, [TYPE, strMapper, { 'mobile' : 'Mobile', 'xr' : 'VR', '*' : TABLET }]], [
/\b((tablet|tab)[;\/]|focus\/\d(?!.+mobile))/i // Unidentifiable Tablet
], [[TYPE, TABLET]], [
diff --git a/test/data/ua/device/blackberry.json b/test/data/ua/device/blackberry.json
index f809d55..396a139 100644
--- a/test/data/ua/device/blackberry.json
+++ b/test/data/ua/device/blackberry.json
@@ -1,7 +1,25 @@
[
+ {
+ "desc": "BlackBerry 9650",
+ "ua": "Mozilla/5.0 (BlackBerry; U; BlackBerry 9650; en-US) AppleWebKit/534.8+ (KHTML, like Gecko) Version/6.0.0.524 Mobile Safari/534.8+",
+ "expect": {
+ "vendor": "BlackBerry",
+ "model": "9650",
+ "type": "mobile"
+ }
+ },
+ {
+ "desc": "BlackBerry 9780",
+ "ua": "Mozilla/5.0 (BlackBerry; U; BlackBerry 9780; en) AppleWebKit/534.8+ (KHTML, like Gecko) Version/6.0.0.546 Mobile Safari/534.8+",
+ "expect": {
+ "vendor": "BlackBerry",
+ "model": "9780",
+ "type": "mobile"
+ }
+ },
{
"desc": "BlackBerry Priv",
- "ua": "User-Agent: Mozilla/5.0 (Linux; Android 5.1.1; STV100-1 Build/LMY47V; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/46.0.2490.76 Mobile Safari/537.36",
+ "ua": "Mozilla/5.0 (Linux; Android 5.1.1; STV100-1 Build/LMY47V; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/46.0.2490.76 Mobile Safari/537.36",
"expect": {
"vendor": "BlackBerry",
"model": "STV100-1",
@@ -28,7 +46,7 @@
},
{
"desc": "BlackBerry Key2 LE",
- "ua": "User-Agent: Mozilla/5.0 (Linux; Android 8.1.0; BBE100-1 Build/OPM1.171019.026) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497 Mobile Safari/537.36",
+ "ua": "Mozilla/5.0 (Linux; Android 8.1.0; BBE100-1 Build/OPM1.171019.026) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497 Mobile Safari/537.36",
"expect": {
"vendor": "BlackBerry",
"model": "BBE100-1",
diff --git a/test/data/ua/device/huawei.json b/test/data/ua/device/huawei.json
index 670879e..14af421 100644
--- a/test/data/ua/device/huawei.json
+++ b/test/data/ua/device/huawei.json
@@ -701,6 +701,15 @@
"type": "mobile"
}
},
+ {
+ "desc": "Huawei P10 Lite",
+ "ua": "Mozilla/5.0 (Linux; Android 8.0.0; WAS-L03T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.5199.205 Mobile Safari/537.36",
+ "expect": {
+ "vendor": "Huawei",
+ "model": "WAS-L03T",
+ "type": "mobile"
+ }
+ },
{
"desc": "Huawei P20 Lite",
"ua": "Mozilla/5.0 (Linux; Android 8.0.0; ANE-LX1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.143 Mobile Safari/537.36",
@@ -728,6 +737,15 @@
"type": "mobile"
}
},
+ {
+ "desc": "Huawei P30",
+ "ua": "Mozilla/5.0 (Linux; Android 10; ELE-L04) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.109 Mobile Safari/537.36",
+ "expect": {
+ "vendor": "Huawei",
+ "model": "ELE-L04",
+ "type": "mobile"
+ }
+ },
{
"desc": "Huawei P30",
"ua": "Mozilla/5.0 (Linux; Android 9; ELE-L29) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.90 Mobile Safari/537.36",
@@ -782,6 +800,15 @@
"type": "mobile"
}
},
+ {
+ "desc": "Huawei Nova",
+ "ua": "Mozilla/5.0 (Linux; Android 7.0; HUAWEI CAN-L13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.5113.212 Mobile Safari/537.36",
+ "expect": {
+ "vendor": "Huawei",
+ "model": "CAN-L13",
+ "type": "mobile"
+ }
+ },
{
"desc": "Huawei Nova 5T",
"ua": "Mozilla/5.0 (Linux; Android 10; YAL-L21) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Mobile Safari/537.36",
@@ -899,6 +926,15 @@
"type": "mobile"
}
},
+ {
+ "desc": "Huawei Y7p",
+ "ua": "Mozilla/5.0 (Linux; Android 10; ART-L28; HMSCore 6.8.0.311) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4476.0 HuaweiBrowser/12.1.2.312 Mobile Safari/537.36",
+ "expect": {
+ "vendor": "Huawei",
+ "model": "ART-L28",
+ "type": "mobile"
+ }
+ },
{
"desc": "Huawei Y7p",
"ua": "Mozilla/5.0 (Linux; Android 9; ART-L29) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.92 Mobile Safari/537.36",
diff --git a/test/data/ua/device/xiaomi.json b/test/data/ua/device/xiaomi.json
index 86593ef..053ef69 100644
--- a/test/data/ua/device/xiaomi.json
+++ b/test/data/ua/device/xiaomi.json
@@ -116,6 +116,15 @@
"type": "mobile"
}
},
+ {
+ "desc": "Xiaomi Mi 11 Lite 5G",
+ "ua": "Mozilla/5.0 (Linux; U; Android 12; zh-CN; Mi 11 Lite 5G Build/SKQ1.211006.001) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.136 Mobile Safari/537.36 XiaoMi/MiuiBrowser/13.17.0-gn",
+ "expect": {
+ "vendor": "Xiaomi",
+ "model": "Mi 11 Lite 5G",
+ "type": "mobile"
+ }
+ },
{
"desc": "Xiaomi Mi 5s Plus",
"ua": "Mozilla/5.0 (Linux; U; Android 6.0.1; zh-cn; MI 5s Plus Build/MXB48T) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/53.0.2785.146 Mobile Safari/537.36 XiaoMi/MiuiBrowser/8.7.1",
@@ -188,6 +197,15 @@
"type": "mobile"
}
},
+ {
+ "desc": "Xiaomi Mi 10T",
+ "ua": "Mozilla/5.0 (Linux; U; Android 12; fr-CA; Mi 10T Build/SKQ1.211006.001) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.79 Mobile Safari/537.36 XiaoMi/MiuiBrowser/13.16.1-gn",
+ "expect": {
+ "vendor": "Xiaomi",
+ "model": "Mi 10T",
+ "type": "mobile"
+ }
+ },
{
"desc": "Xiaomi Mi A2",
"ua": "Mozilla/5.0 (Linux; Android 9; Mi A2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Mobile Safari/537.36",
@@ -314,6 +332,15 @@
"type": "tablet"
}
},
+ {
+ "desc": "Xiaomi Pad 5",
+ "ua": "Mozilla/5.0 (Linux; U; Android 12; ca-ES; Xiaomi Pad 5 Build/SKQ1.220303.001) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/89.0.4389.86 Mobile Safari/537.36 XiaoMi/MiuiBrowser/13.6.0-gn",
+ "expect": {
+ "vendor": "Xiaomi",
+ "model": "mi Pad 5",
+ "type": "tablet"
+ }
+ },
{
"desc": "Xiaomi Pad 6S Pro 12.4",
"ua": "Mozilla/5.0 (Linux; Android 14; 24018RPACC) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36",
@@ -458,6 +485,15 @@
"type": "mobile"
}
},
+ {
+ "desc": "XiaoMi Redmi Note 10 Lite",
+ "ua": "Mozilla/5.0 (Linux; U; Android 12; es-VE; Mi Note 10 Lite Build/SKQ1.210908.001) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.88 Mobile Safari/537.36 XiaoMi/MiuiBrowser/13.16.1-gn",
+ "expect": {
+ "vendor": "Xiaomi",
+ "model": "Mi Note 10 Lite",
+ "type": "mobile"
+ }
+ },
{
"desc": "XiaoMi Redmi Note 10 Pro",
"ua": "Mozilla/5.0 (Linux; Android 13; M2101K6P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Mobile Safari/537.36",
From 5f1ed83225d81863a20c43ce13e73482c7835953 Mon Sep 17 00:00:00 2001
From: Faisal Salman
Date: Wed, 8 Oct 2025 13:13:17 +0700
Subject: [PATCH 17/20] [chore] Update CLI import & unit test
---
script/cli.js | 4 ++--
test/unit/cli/cli.spec.js | 16 ++++++++--------
2 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/script/cli.js b/script/cli.js
index 368d6c5..813cfc3 100755
--- a/script/cli.js
+++ b/script/cli.js
@@ -6,8 +6,8 @@ try {
const { performance } = require('node:perf_hooks');
const readline = require('node:readline');
const { parseArgs } = require('node:util');
- const UAParser = require('ua-parser-js');
- const { Bots, Emails, ExtraDevices, InApps, Vehicles } = require('ua-parser-js/extensions');
+ const UAParser = require('../src/main/ua-parser');
+ const { Bots, Emails, ExtraDevices, InApps, Vehicles } = require('../src/extensions/ua-parser-extensions');
if (!process.argv[2].startsWith('-')) {
diff --git a/test/unit/cli/cli.spec.js b/test/unit/cli/cli.spec.js
index 7365fe4..ab5523e 100644
--- a/test/unit/cli/cli.spec.js
+++ b/test/unit/cli/cli.spec.js
@@ -4,6 +4,12 @@ const fs = require('node:fs');
const { UAParser } = require('../../../src/main/ua-parser');
const uap = new UAParser();
+const input = [
+ 'Opera/9.25 (Windows NT 6.0; U; ru)',
+ 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
+];
+const output = input.map(x => uap.setUA(x).getResult());
+
describe('npx ua-parser-js ', () => {
it ('print result to stdout', () => {
exec('npx ua-parser-js "TEST"', (err, stdout, stderr) => {
@@ -15,10 +21,7 @@ describe('npx ua-parser-js ', () => {
describe('npx ua-parser-js --input-file=', () => {
it ('load file and print result to stdout', () => {
exec('npx ua-parser-js --input-file="../test/unit/cli/input.txt"', (err, stdout, stderr) => {
- assert.deepEqual(JSON.parse(stdout), JSON.parse(JSON.stringify([
- uap.setUA('Opera/9.25 (Windows NT 6.0; U; ru)').getResult(),
- uap.setUA('Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)').getResult()
- ])));
+ assert.deepEqual(JSON.parse(stdout), JSON.parse(JSON.stringify(output)));
});
});
});
@@ -27,10 +30,7 @@ describe('npx ua-parser-js --input-file= --output-file=', ()
it ('load file and save result to file', () => {
exec('npx ua-parser-js --input-file="../test/unit/cli/input.txt" --output-file="../test/unit/cli/output.json"', (err, stdout, stderr) => {
fs.readFile('test/unit/cli/output.json', (err, data) => {
- assert.deepEqual(JSON.parse(data), JSON.parse(JSON.stringify([
- uap.setUA('Opera/9.25 (Windows NT 6.0; U; ru)').getResult(),
- uap.setUA('Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)').getResult()
- ])));
+ assert.deepEqual(JSON.parse(data), JSON.parse(JSON.stringify(output)));
});
});
});
From 3eea0643c58ae4ff235bd343cc8c71585976e933 Mon Sep 17 00:00:00 2001
From: Faisal Salman
Date: Wed, 8 Oct 2025 21:51:36 +0700
Subject: [PATCH 18/20] Add new device vendor: Wiko -
https://world.wikomobile.com/
---
src/enums/ua-parser-enums.js | 1 +
src/main/ua-parser.js | 3 ++-
test/data/ua/device/wiko.json | 29 +++++++++++++++++++++++++++++
3 files changed, 32 insertions(+), 1 deletion(-)
create mode 100644 test/data/ua/device/wiko.json
diff --git a/src/enums/ua-parser-enums.js b/src/enums/ua-parser-enums.js
index 8955439..202a8f6 100644
--- a/src/enums/ua-parser-enums.js
+++ b/src/enums/ua-parser-enums.js
@@ -289,6 +289,7 @@ const DeviceVendor = Object.freeze({
VIVO: 'Vivo',
VIZIO: 'Vizio',
VODAFONE: 'Vodafone',
+ WIKO: 'Wiko',
XBOX: 'Xbox',
XIAOMI: 'Xiaomi',
ZEBRA: 'Zebra',
diff --git a/src/main/ua-parser.js b/src/main/ua-parser.js
index 27e79ce..7c962a9 100755
--- a/src/main/ua-parser.js
+++ b/src/main/ua-parser.js
@@ -769,7 +769,8 @@
/(blackberry|benq|palm(?=\-)|sonyericsson|acer|asus(?! zenw)|dell|jolla|meizu|motorola|polytron|tecno|micromax|advan)[-_ ]?([-\w]*)/i,
// BlackBerry/BenQ/Palm/Sony-Ericsson/Acer/Asus/Dell/Meizu/Motorola/Polytron/Tecno/Micromax/Advan
- /; (blu|hmd|imo|infinix|lava|oneplus|tcl)[_ ]([\w\+ ]+?)(?: bui|\)|; r)/i, // BLU/HMD/IMO/Infinix/Lava/OnePlus/TCL
+ // BLU/HMD/IMO/Infinix/Lava/OnePlus/TCL/Wiko
+ /; (blu|hmd|imo|infinix|lava|oneplus|tcl|wiko)[_ ]([\w\+ ]+?)(?: bui|\)|; r)/i,
/(hp) ([\w ]+\w)/i, // HP iPAQ
/(microsoft); (lumia[\w ]+)/i, // Microsoft Lumia
/(oppo) ?([\w ]+) bui/i, // OPPO
diff --git a/test/data/ua/device/wiko.json b/test/data/ua/device/wiko.json
new file mode 100644
index 0000000..9030e7a
--- /dev/null
+++ b/test/data/ua/device/wiko.json
@@ -0,0 +1,29 @@
+[
+ {
+ "desc": "Wiko Life 3",
+ "ua": "Mozilla/5.0 (Linux; Android 11; Wiko U316AT) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.5199.205 Mobile Safari/537.36",
+ "expect": {
+ "vendor": "Wiko",
+ "model": "U316AT",
+ "type": "mobile"
+ }
+ },
+ {
+ "desc": "Wiko Ride 3",
+ "ua": "Mozilla/5.0 (Linux; Android 11; Wiko U614AS) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.181 Mobile Safari/537.36",
+ "expect": {
+ "vendor": "Wiko",
+ "model": "U614AS",
+ "type": "mobile"
+ }
+ },
+ {
+ "desc": "Wiko T10",
+ "ua": "Mozilla/5.0 (Linux; Android 11; WIKO T10 Build/RP1A.200720.011) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.5304.62 Mobile Safari/537.36",
+ "expect": {
+ "vendor": "WIKO",
+ "model": "T10",
+ "type": "mobile"
+ }
+ }
+]
\ No newline at end of file
From 2882014f0ebf1b052b3bd128f5f5e83762aa7752 Mon Sep 17 00:00:00 2001
From: Faisal Salman
Date: Thu, 9 Oct 2025 11:34:03 +0700
Subject: [PATCH 19/20] Add new inApp browser: Bing
---
src/enums/ua-parser-enums.js | 1 +
src/main/ua-parser.js | 1 +
test/data/ua/browser/browser-all.json | 20 ++++++++++++++++++++
3 files changed, 22 insertions(+)
diff --git a/src/enums/ua-parser-enums.js b/src/enums/ua-parser-enums.js
index 202a8f6..4867fec 100644
--- a/src/enums/ua-parser-enums.js
+++ b/src/enums/ua-parser-enums.js
@@ -20,6 +20,7 @@ const BrowserName = Object.freeze({
AVG: 'AVG Secure Browser',
BAIDU: 'Baidu Browser',
BASILISK: 'Basilisk',
+ BING: 'Bing',
BLAZER: 'Blazer',
BOLT: 'Bolt',
BOWSER: 'Bowser',
diff --git a/src/main/ua-parser.js b/src/main/ua-parser.js
index 7c962a9..b461dc3 100755
--- a/src/main/ua-parser.js
+++ b/src/main/ua-parser.js
@@ -448,6 +448,7 @@
/\b(line)\/([\w\.]+)\/iab/i, // Line App for Android
/(alipay)client\/([\w\.]+)/i, // Alipay
/(twitter)(?:and| f.+e\/([\w\.]+))/i, // Twitter
+ /(bing)(?:web|sapphire)\/([\w\.]+)/i, // Bing
/(instagram|snapchat|klarna)[\/ ]([-\w\.]+)/i // Instagram/Snapchat/Klarna
], [NAME, VERSION, [TYPE, INAPP]], [
/\bgsa\/([\w\.]+) .*safari\//i // Google Search Appliance on iOS
diff --git a/test/data/ua/browser/browser-all.json b/test/data/ua/browser/browser-all.json
index bea4eb9..a7372d5 100644
--- a/test/data/ua/browser/browser-all.json
+++ b/test/data/ua/browser/browser-all.json
@@ -271,6 +271,26 @@
"major" : "11"
}
},
+ {
+ "desc" : "Bing",
+ "ua" : "Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/605.1.15 BingSapphire/31.8.430522001",
+ "expect" :
+ {
+ "name" : "Bing",
+ "version" : "31.8.430522001",
+ "major" : "31"
+ }
+ },
+ {
+ "desc" : "Bing",
+ "ua" : "Mozilla/5.0 (Linux; Android 9; MIX 2 Build/PKQ1.190118.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4893.0 Mobile Safari/537.36 BingWeb/6.9.12",
+ "expect" :
+ {
+ "name" : "Bing",
+ "version" : "6.9.12",
+ "major" : "6"
+ }
+ },
{
"desc" : "Blazer",
"ua" : "Mozilla/4.0 (compatible; MSIE 6.0; Windows 98; PalmSource/hspr-H102; Blazer/4.0) 16;320x320",
From 061cf0e90f3651a040a1ee64fc5c63d442b93c64 Mon Sep 17 00:00:00 2001
From: Faisal Salman
Date: Fri, 10 Oct 2025 09:58:40 +0700
Subject: [PATCH 20/20] Bump version `2.0.6`
---
CHANGELOG.md | 14 ++
README.md | 46 ++--
dist/ua-parser.min.js | 4 +-
dist/ua-parser.min.mjs | 4 +-
dist/ua-parser.pack.js | 4 +-
dist/ua-parser.pack.mjs | 4 +-
package-lock.json | 4 +-
package.json | 10 +-
src/enums/ua-parser-enums.d.ts | 15 +-
src/enums/ua-parser-enums.js | 2 +-
src/enums/ua-parser-enums.mjs | 15 +-
src/extensions/ua-parser-extensions.js | 2 +-
src/extensions/ua-parser-extensions.mjs | 12 +-
src/helpers/ua-parser-helpers.js | 2 +-
src/helpers/ua-parser-helpers.mjs | 2 +-
src/main/ua-parser.d.ts | 2 +-
src/main/ua-parser.js | 4 +-
src/main/ua-parser.mjs | 275 +++++++++++++-----------
test/unit/main.js | 15 +-
19 files changed, 247 insertions(+), 189 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fb55cf8..5ef6810 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -69,6 +69,20 @@
---
+## Version 2.0.6
+- Add new CLI feature: processing batch user-agent data from file and output as JSON
+- Fix `setUA()`: trim leading space from user-agent string input
+- Replace `undici` dependency with node's internal `Headers`
+- Add new browser: Bing, Qwant
+- Add new device vendor: Hisense, Wiko
+- Improve browser detection: Mozilla, Pale Moon
+- Improve CPU detection: 68k
+- Improve device detection: Apple, BlackBerry, Huawei, Nokia, Xiaomi
+- Improve OS detection: iOS 26
+- `extensions` submodule:
+ - Add new fetcher: Discordbot, KeybaseBot, Slackbot, Slackbot-LinkExpanding, Slack-ImgProxy, Twitterbot
+ - Add new crawler: Qwantbot-news, SurdotlyBot, SwiftBot
+
## Version 2.0.5
- Add new browser: Zalo
diff --git a/README.md b/README.md
index 03eb487..7bb630f 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@
-
+
# UAParser.js
@@ -58,7 +58,7 @@ see what's new & breaking.
PRO Enterprise |
- | Browser detection |
+ Browser Detection |
⚠️ |
✅ |
✅ |
@@ -66,7 +66,7 @@ see what's new & breaking.
✅ |
- | CPU detection |
+ CPU Detection |
⚠️ |
✅ |
✅ |
@@ -74,7 +74,7 @@ see what's new & breaking.
✅ |
- | Device detection |
+ Device Detection |
⚠️ |
✅ |
✅ |
@@ -82,7 +82,7 @@ see what's new & breaking.
✅ |
- | Engine detection |
+ Rendering Engine Detection |
⚠️ |
✅ |
✅ |
@@ -98,7 +98,7 @@ see what's new & breaking.
✅ |
- | Bot detection |
+ Enhanced+ Accuracy |
❌ |
✅ |
✅ |
@@ -106,7 +106,7 @@ see what's new & breaking.
✅ |
- | AI Bot detection |
+ Bot Detection |
❌ |
✅ |
✅ |
@@ -114,7 +114,7 @@ see what's new & breaking.
✅ |
- | Extras (Apps, Libs, Emails, Media Players, etc) detection |
+ AI Detection |
❌ |
✅ |
✅ |
@@ -122,7 +122,7 @@ see what's new & breaking.
✅ |
- | Enhanced detection result |
+ Extra Detections (Apps, Libs, Emails, Media Players, Crawlers, and more) |
❌ |
✅ |
✅ |
@@ -130,7 +130,7 @@ see what's new & breaking.
✅ |
- | Client Hints support |
+ Client Hints Support |
❌ |
✅ |
✅ |
@@ -138,7 +138,7 @@ see what's new & breaking.
✅ |
- | CommonJS support |
+ CommonJS Support |
✅ |
✅ |
✅ |
@@ -146,7 +146,7 @@ see what's new & breaking.
✅ |
- | ES modules support |
+ ESM Support |
❌ |
✅ |
✅ |
@@ -154,15 +154,15 @@ see what's new & breaking.
✅ |
- | TypeScript declarations |
- ⚠️ |
+ TypeScript Definitions |
+ ✅ |
✅ |
✅ |
✅ |
✅ |
- | npm module available |
+ npm Module Available |
✅ |
✅ |
✅ |
@@ -170,7 +170,7 @@ see what's new & breaking.
✅ |
- | Direct downloads available |
+ Direct Downloads Available |
✅ |
✅ |
✅ |
@@ -178,7 +178,7 @@ see what's new & breaking.
✅ |
- | Allows commercial usage |
+ Commercial Use Allowed |
✅ |
✅ |
❌ |
@@ -186,7 +186,7 @@ see what's new & breaking.
✅ |
- | Permissive (non-copyleft) license |
+ Permissive (non-Copyleft) License |
✅ |
❌ |
✅ |
@@ -194,7 +194,7 @@ see what's new & breaking.
✅ |
- | No open-source obligations |
+ No Open-Source Obligations |
✅ |
❌ |
✅ |
@@ -202,7 +202,7 @@ see what's new & breaking.
✅ |
- | Unlimited end-products |
+ Unlimited End-Products |
✅ |
✅ |
✅ |
@@ -210,7 +210,7 @@ see what's new & breaking.
✅ |
- | Unlimited deployments |
+ Unlimited Deployments |
✅ |
✅ |
✅ |
@@ -218,7 +218,7 @@ see what's new & breaking.
✅ |
- | 1-year product support |
+ 1-year Product Support |
❌ |
❌ |
✅ |
@@ -226,7 +226,7 @@ see what's new & breaking.
✅ |
- | Lifetime updates |
+ Lifetime Updates |
✅ |
✅ |
✅ |
diff --git a/dist/ua-parser.min.js b/dist/ua-parser.min.js
index 98f9ad6..ceb9508 100644
--- a/dist/ua-parser.min.js
+++ b/dist/ua-parser.min.js
@@ -1,4 +1,4 @@
-/* UAParser.js v2.0.5
+/* UAParser.js v2.0.6
Copyright © 2012-2025 Faisal Salman
AGPLv3 License */
-(function(window,undefined){"use strict";var LIBVERSION="2.0.5",UA_MAX_LENGTH=500,USER_AGENT="user-agent",EMPTY="",UNKNOWN="?",FUNC_TYPE="function",UNDEF_TYPE="undefined",OBJ_TYPE="object",STR_TYPE="string",UA_BROWSER="browser",UA_CPU="cpu",UA_DEVICE="device",UA_ENGINE="engine",UA_OS="os",UA_RESULT="result",NAME="name",TYPE="type",VENDOR="vendor",VERSION="version",ARCHITECTURE="architecture",MAJOR="major",MODEL="model",CONSOLE="console",MOBILE="mobile",TABLET="tablet",SMARTTV="smarttv",WEARABLE="wearable",XR="xr",EMBEDDED="embedded",INAPP="inapp",BRANDS="brands",FORMFACTORS="formFactors",FULLVERLIST="fullVersionList",PLATFORM="platform",PLATFORMVER="platformVersion",BITNESS="bitness",CH_HEADER="sec-ch-ua",CH_HEADER_FULL_VER_LIST=CH_HEADER+"-full-version-list",CH_HEADER_ARCH=CH_HEADER+"-arch",CH_HEADER_BITNESS=CH_HEADER+"-"+BITNESS,CH_HEADER_FORM_FACTORS=CH_HEADER+"-form-factors",CH_HEADER_MOBILE=CH_HEADER+"-"+MOBILE,CH_HEADER_MODEL=CH_HEADER+"-"+MODEL,CH_HEADER_PLATFORM=CH_HEADER+"-"+PLATFORM,CH_HEADER_PLATFORM_VER=CH_HEADER_PLATFORM+"-version",CH_ALL_VALUES=[BRANDS,FULLVERLIST,MOBILE,MODEL,PLATFORM,PLATFORMVER,ARCHITECTURE,FORMFACTORS,BITNESS],AMAZON="Amazon",APPLE="Apple",ASUS="ASUS",BLACKBERRY="BlackBerry",GOOGLE="Google",HUAWEI="Huawei",LENOVO="Lenovo",HONOR="Honor",LG="LG",MICROSOFT="Microsoft",MOTOROLA="Motorola",NVIDIA="Nvidia",ONEPLUS="OnePlus",OPPO="OPPO",SAMSUNG="Samsung",SHARP="Sharp",SONY="Sony",XIAOMI="Xiaomi",ZEBRA="Zebra",CHROME="Chrome",CHROMIUM="Chromium",CHROMECAST="Chromecast",EDGE="Edge",FIREFOX="Firefox",OPERA="Opera",FACEBOOK="Facebook",SOGOU="Sogou",PREFIX_MOBILE="Mobile ",SUFFIX_BROWSER=" Browser",WINDOWS="Windows";var isWindow=typeof window!==UNDEF_TYPE,NAVIGATOR=isWindow&&window.navigator?window.navigator:undefined,NAVIGATOR_UADATA=NAVIGATOR&&NAVIGATOR.userAgentData?NAVIGATOR.userAgentData:undefined;var extend=function(defaultRgx,extensions){var mergedRgx={};var extraRgx=extensions;if(!isExtensions(extensions)){extraRgx={};for(var i in extensions){for(var j in extensions[i]){extraRgx[j]=extensions[i][j].concat(extraRgx[j]?extraRgx[j]:[])}}}for(var k in defaultRgx){mergedRgx[k]=extraRgx[k]&&extraRgx[k].length%2===0?extraRgx[k].concat(defaultRgx[k]):defaultRgx[k]}return mergedRgx},enumerize=function(arr){var enums={};for(var i=0;i0){for(var i in str1){if(lowerize(str2)==lowerize(str1[i]))return true}return false}return isString(str1)?lowerize(str2)==lowerize(str1):false},isExtensions=function(obj,deep){for(var prop in obj){return/^(browser|cpu|device|engine|os)$/.test(prop)||(deep?isExtensions(obj[prop]):false)}},isString=function(val){return typeof val===STR_TYPE},itemListToArray=function(header){if(!header)return undefined;var arr=[];var tokens=strip(/\\?\"/g,header).split(",");for(var i=0;i-1){var token=trim(tokens[i]).split(";v=");arr[i]={brand:token[0],version:token[1]}}else{arr[i]=trim(tokens[i])}}return arr},lowerize=function(str){return isString(str)?str.toLowerCase():str},majorize=function(version){return isString(version)?strip(/[^\d\.]/g,version).split(".")[0]:undefined},setProps=function(arr){for(var i in arr){if(!arr.hasOwnProperty(i))continue;var propName=arr[i];if(typeof propName==OBJ_TYPE&&propName.length==2){this[propName[0]]=propName[1]}else{this[propName]=undefined}}return this},strip=function(pattern,str){return isString(str)?str.replace(pattern,EMPTY):str},stripQuotes=function(str){return strip(/\\?\"/g,str)},trim=function(str,len){if(isString(str)){str=strip(/^\s\s*/,str);return typeof len===UNDEF_TYPE?str:str.substring(0,UA_MAX_LENGTH)}};var rgxMapper=function(ua,arrays){if(!ua||!arrays)return;var i=0,j,k,p,q,matches,match;while(i0){if(q.length===2){if(typeof q[1]==FUNC_TYPE){this[q[0]]=q[1].call(this,match)}else{this[q[0]]=q[1]}}else if(q.length>=3){if(typeof q[1]===FUNC_TYPE&&!(q[1].exec&&q[1].test)){if(q.length>3){this[q[0]]=match?q[1].apply(this,q.slice(2)):undefined}else{this[q[0]]=match?q[1].call(this,match,q[2]):undefined}}else{if(q.length==3){this[q[0]]=match?match.replace(q[1],q[2]):undefined}else if(q.length==4){this[q[0]]=match?q[3].call(this,match.replace(q[1],q[2])):undefined}else if(q.length>4){this[q[0]]=match?q[3].apply(this,[match.replace(q[1],q[2])].concat(q.slice(4))):undefined}}}}else{this[q]=match?match:undefined}}}}i+=2}},strMapper=function(str,map){for(var i in map){if(typeof map[i]===OBJ_TYPE&&map[i].length>0){for(var j=0;j