Compare commits

...

18 Commits

Author SHA1 Message Date
Faisal Salman
7bc177de79 Bump version 2.0.7 2025-12-09 13:15:29 +07:00
Faisal Salman
82d50451a2 Improve device detection: Xiaomi 2025-12-01 16:51:25 +07:00
Faisal Salman
96e3518e2e Fix #812 - Add support for chaining withClientHints() & withFeatureCheck() 2025-11-28 14:51:22 +07:00
Faisal Salman
f1b9a12bda Improve OS detection: identify AppleTV's tvOS as iOS 2025-11-16 14:16:17 +07:00
Faisal Salman
82801a36b6 Add new browser: Steam 2025-11-13 12:15:10 +07:00
Faisal Salman
6f35728377 Add new device vendor: Valve - https://www.valvesoftware.com 2025-11-13 12:07:49 +07:00
Faisal Salman
1d8eab09f1 Move properties inside UAItem class into shared prototype 2025-11-13 11:42:53 +07:00
Faisal Salman
5a5b321347 [extensions] Add new library: Bun, Dart, Deno, hackney, Node.js, rest-client, undici 2025-11-09 15:33:15 +07:00
Faisal Salman
e9f78ceb80 Add new device vendor: Logitech
- G-Cloud https://www.logitechg.com/en-ch/shop/p/cloud-handheld-gaming
2025-11-08 10:06:04 +07:00
Faisal Salman
38301f8803 Add new device vendor: Anbernic - https://anbernic.com/ 2025-11-06 13:42:23 +07:00
Faisal Salman
e5648826b9 [cli] Update to use extensions when parsing string input 2025-11-06 12:36:26 +07:00
Faisal Salman
a558cc1a5b [extensions][bot-detection] Add new bot: Amazon Nova Act 2025-11-03 13:11:35 +07:00
Faisal Salman
cb9b50a81a Fix import path 2025-10-27 12:50:52 +07:00
Faisal Salman
042c57cc10 [bot-detection] Add new method: isAIAssistant() to check whether user-agent is an AI assistant 2025-10-25 16:42:36 +07:00
Faisal Salman
232fb321f1 [test] Add tests for the new submodules 2025-10-25 10:28:48 +07:00
Faisal Salman
a3a7a5e377 Break some methods in helpers as new submodules: bot-detection, browser-detection, device-detection
`isAIBot()` & `isBot()` => `bot-detection`
`isChromeFamily()`, `isElectron()`, `isFromEU()`, & `isStandalonePWA()` => `browser-detection`
`getDeviceVendor()` & `isAppleSilicon()` => `device-detection`
2025-10-25 09:51:42 +07:00
Faisal Salman
2d8c8fa142 Fix #809 - Detect OpenAI's Atlas browser
https://openai.com/index/introducing-chatgpt-atlas/
2025-10-24 22:59:48 +07:00
Faisal Salman
d84ba1888b Update image link in README 2025-10-13 20:38:26 +07:00
46 changed files with 1737 additions and 914 deletions

View File

@@ -54,21 +54,52 @@
- `browser.name`, `browser.type`, `cpu.architecture`, `device.type`, `device.vendor`, `engine.name`, `os.name`
- **`'ua-parser-js/extensions'`**: Predefined extensions for various use cases:
- `Bots`, `Crawlers`, `CLIs`, `Emails`, `ExtraDevices`, `Fetchers`, `InApps`, `Libraries`, `Mediaplayers`
- `Bots`, `Crawlers`, `CLIs`, `Emails`, `ExtraDevices`, `Fetchers`, `InApps`, `Libraries`, `Mediaplayers`, `Vehicles`
- **`'ua-parser-js/helpers'`**: Provides utility methods to extend detection functionality:
- `getDeviceVendor()`: Guesses the device vendor based on its model name
- `isAppleSilicon()`: Detects Apple Silicon device properties
- `isAIBot()`: Checks if the user-agent is an AI bot
- `isFrozenUA()`: Checks if the user-agent matches a frozen/reduced user-agent pattern
- **`'ua-parser-js/bot-detection'`**:
- `isAIAssistant()`: Checks if the user-agent is an AI assistant
- `isAICrawler()`: Checks if the user-agent is an AI crawler
- `isBot()`: Checks if the user-agent is a bot
- **`'ua-parser-js/browser-detection'`**:
- `isChromeFamily()`: Checks if the browser is Chrome-based (uses Blink engine) — e.g., New Opera, New Edge, Vivaldi, Brave, Arc, etc.
- `isElectron()`: Detects if current window is running within Electron
- `isFromEU()`: Detects if current browser's timezone is from an EU country
- `isFrozenUA()`: Checks if the user-agent matches a frozen/reduced user-agent pattern
- `isStandalonePWA()`: Detects if current window is a standalone PWA
- **`'ua-parser-js/device-detection'`**:
- `getDeviceVendor()`: Guesses the device vendor based on its model name
- `isAppleSilicon()`: Detects Apple Silicon device properties
---
## Version 2.0.7
- Add support for chaining `withClientHints()` & `withFeatureCheck()`
- Add new browser: Atlas, Steam
- Add new device vendor: Anbernic, Logitech, Valve
- Improve device detection: Xiaomi
- Improve OS detection: iOS
- Split `helpers` submodule into several new submodules:
- `bot-detection`:
- `isAIAssistant()`
- `isAICrawler()`
- `isBot()`
- `browser-detection`
- `isChromeFamily()`
- `isElectron()`
- `isFromEU()`
- `isStandalonePWA()`
- `device-detection`
- `getDeviceVendor()`
- `isAppleSilicon()`
- Update `extensions` submodule:
- Add new fetcher: Nova Act
- Add new library: Bun, Dart, Deno, hackney, Node.js, rest-client, undici
## 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

View File

@@ -5,7 +5,7 @@
---
[![https://uaparser.dev](https://raw.githubusercontent.com/faisalman/ua-parser-js/gh-pages/images/uap-header.png)](https://uaparser.dev)
[![https://uaparser.dev](https://github.com/user-attachments/assets/9f30f3d4-5cfe-441c-8f86-ead7c955f940)](https://uaparser.dev)
[![https://uaparser.dev](https://github.com/user-attachments/assets/a626166c-17cc-45e3-8ff6-d7e948a5ded0)](https://uaparser.dev)
[![https://uaparser.dev](https://github.com/user-attachments/assets/50da50fc-7c8a-46e3-a2bc-6a8249914372)](https://uaparser.dev)
[![https://uaparser.dev](https://github.com/user-attachments/assets/9f2aaff0-a9b4-4ac9-bdf3-eea8081a2582)](https://uaparser.dev)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

32
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "ua-parser-js",
"version": "2.0.6",
"version": "2.0.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ua-parser-js",
"version": "2.0.6",
"version": "2.0.7",
"funding": [
{
"type": "opencollective",
@@ -33,7 +33,7 @@
"devDependencies": {
"@babel/parser": "7.15.8",
"@babel/traverse": "7.23.2",
"@playwright/test": "^1.49.0",
"@playwright/test": "^1.57.0",
"jshint": "~2.13.6",
"mocha": "~8.2.0",
"requirejs": "2.3.2",
@@ -343,12 +343,13 @@
}
},
"node_modules/@playwright/test": {
"version": "1.49.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0.tgz",
"integrity": "sha512-DMulbwQURa8rNIQrf94+jPJQ4FmOVdpE5ZppRNvWVjvhC+6sOeo28r8MgIpQRYouXRtt/FCCXU7zn20jnHR4Qw==",
"version": "1.57.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz",
"integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.49.0"
"playwright": "1.57.0"
},
"bin": {
"playwright": "cli.js"
@@ -2042,12 +2043,13 @@
}
},
"node_modules/playwright": {
"version": "1.49.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0.tgz",
"integrity": "sha512-eKpmys0UFDnfNb3vfsf8Vx2LEOtflgRebl0Im2eQQnYMA4Aqd+Zw8bEOB+7ZKvN76901mRnqdsiOGKxzVTbi7A==",
"version": "1.57.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz",
"integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.49.0"
"playwright-core": "1.57.0"
},
"bin": {
"playwright": "cli.js"
@@ -2060,10 +2062,11 @@
}
},
"node_modules/playwright-core": {
"version": "1.49.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0.tgz",
"integrity": "sha512-R+3KKTQF3npy5GTiKH/T+kdhoJfJojjHESR1YEWhYuEKRVfVaxH3+4+GvXE5xyCngCxhxnykk0Vlah9v8fs3jA==",
"version": "1.57.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz",
"integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
@@ -2077,6 +2080,7 @@
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"

View File

@@ -1,7 +1,7 @@
{
"title": "UAParser.js",
"name": "ua-parser-js",
"version": "2.0.6",
"version": "2.0.7",
"author": "Faisal Salman <f@faisalman.com> (http://faisalman.com)",
"description": "Detect Browser, Engine, OS, CPU, and Device type/model from User-Agent & Client Hints data. Supports browser & node.js environment",
"keywords": [
@@ -190,6 +190,21 @@
"import": "./src/main/ua-parser.mjs",
"types": "./src/main/ua-parser.d.ts"
},
"./bot-detection": {
"require": "./src/bot-detection/bot-detection.js",
"import": "./src/bot-detection/bot-detection.mjs",
"types": "./src/bot-detection/bot-detection.d.ts"
},
"./browser-detection": {
"require": "./src/browser-detection/browser-detection.js",
"import": "./src/browser-detection/browser-detection.mjs",
"types": "./src/browser-detection/browser-detection.d.ts"
},
"./device-detection": {
"require": "./src/device-detection/device-detection.js",
"import": "./src/device-detection/device-detection.mjs",
"types": "./src/device-detection/device-detection.d.ts"
},
"./enums": {
"require": "./src/enums/ua-parser-enums.js",
"import": "./src/enums/ua-parser-enums.mjs",
@@ -231,7 +246,7 @@
"devDependencies": {
"@babel/parser": "7.15.8",
"@babel/traverse": "7.23.2",
"@playwright/test": "^1.49.0",
"@playwright/test": "^1.57.0",
"jshint": "~2.13.6",
"mocha": "~8.2.0",
"requirejs": "2.3.2",

View File

@@ -40,6 +40,24 @@ const files = [
...defaultReplacements.mjs
]
},
{
src : 'src/bot-detection/bot-detection.js',
dest :'src/bot-detection/bot-detection.mjs',
title : 'Generated ESM version of ua-parser-js/bot-detection',
replacements : [...defaultReplacements.mjs]
},
{
src : 'src/browser-detection/browser-detection.js',
dest :'src/browser-detection/browser-detection.mjs',
title : 'Generated ESM version of ua-parser-js/browser-detection',
replacements : [...defaultReplacements.mjs]
},
{
src : 'src/device-detection/device-detection.js',
dest :'src/device-detection/device-detection.mjs',
title : 'Generated ESM version of ua-parser-js/device-detection',
replacements : [...defaultReplacements.mjs]
},
{
src : 'src/enums/ua-parser-enums.js',
dest :'src/enums/ua-parser-enums.mjs',

View File

@@ -11,7 +11,7 @@ try {
if (!process.argv[2].startsWith('-')) {
const results = process.argv.slice(2).map(ua => UAParser(ua));
const results = process.argv.slice(2).map(ua => UAParser(ua, [Bots, Emails, ExtraDevices, InApps, Vehicles]));
console.log(JSON.stringify(results, null, 4));
process.exit(0);

7
src/bot-detection/bot-detection.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
// Type definitions for bot-detection submodule of UAParser.js v2.0.7
// Project: https://github.com/faisalman/ua-parser-js
// Definitions by: Faisal Salman <https://github.com/faisalman>
export function isAIAssistant(ua: string): boolean;
export function isAICrawler(ua: string): boolean;
export function isBot(ua: string): boolean;

View File

@@ -0,0 +1,188 @@
//////////////////////////////////////////////////
/* bot-detection submodule of UAParser.js v2.0.7
https://github.com/faisalman/ua-parser-js
Author: Faisal Salman <f@faisalman.com>
AGPLv3 License */
/////////////////////////////////////////////////
/*jshint esversion: 6 */
const { UAParser } = require('../main/ua-parser');
const { Bots, Crawlers, Fetchers } = require('../extensions/ua-parser-extensions');
const { BrowserType, Extension } = require('../enums/ua-parser-enums');
const { Crawler, Fetcher } = Extension.BrowserName;
class BotList {
constructor(ext, prop, list) {
this.ext = ext;
this.prop = prop;
this.list = list.map(x => x.toLowerCase());
}
includes(ua) {
return this.list.includes(
(typeof ua === 'string' ?
new UAParser(ua, this.ext).getBrowser() :
ua.browser
)[this.prop]?.toLowerCase());
}
}
const BotTypes = new BotList(Bots, 'type', [
BrowserType.CLI,
BrowserType.CRAWLER,
BrowserType.FETCHER,
BrowserType.LIBRARY
]);
const AIAssistants = new BotList(Fetchers, 'name', [
// Amazon
Fetcher.AMAZON_NOVA_ACT,
// Anthropic
Fetcher.ANTHROPIC_CLAUDE_USER,
// Cohere
Fetcher.COHERE_AI,
// DuckDuckGo
Fetcher.DUCKDUCKGO_ASSISTBOT,
// Google
Fetcher.GOOGLE_GEMINI_DEEP_RESEARCH,
// Mistral AI
Fetcher.MISTRALAI_USER,
// OpenAI
Fetcher.OPENAI_CHATGPT_USER,
// Perplexity
Fetcher.PERPLEXITY_USER
]);
const AICrawlers = new BotList(Crawlers, 'name', [
// AI2
Crawler.AI2_BOT,
// Amazon
Crawler.AMAZON_BOT,
// Anthropic
Crawler.ANTHROPIC_AI,
Crawler.ANTHROPIC_CLAUDE_BOT,
Crawler.ANTHROPIC_CLAUDE_SEARCHBOT,
Crawler.ANTHROPIC_CLAUDE_WEB,
// Apple
Crawler.APPLE_BOT,
Crawler.APPLE_BOT_EXTENDED,
// Brave
Crawler.BRAVE_BOT,
// ByteDance
Crawler.BYTEDANCE_BYTESPIDER,
Crawler.BYTEDANCE_TIKTOKSPIDER,
// Cohere
Crawler.COHERE_TRAINING_DATA_CRAWLER,
// Common Crawl
Crawler.COMMON_CRAWL_CCBOT,
// Coveo
Crawler.COVEO_BOT,
// DataForSeo
Crawler.DATAFORSEO_BOT,
// DeepSeek
Crawler.DEEPSEEK_BOT,
// Diffbot
Crawler.DIFFBOT,
// Google
Crawler.GOOGLE_EXTENDED,
Crawler.GOOGLE_OTHER,
Crawler.GOOGLE_OTHER_IMAGE,
Crawler.GOOGLE_OTHER_VIDEO,
Crawler.GOOGLE_CLOUDVERTEXBOT,
// Hive AI
Crawler.HIVE_IMAGESIFTBOT,
// Huawei
Crawler.HUAWEI_PETALBOT,
Crawler.HUAWEI_PANGUBOT,
// Hugging Face
Crawler.HUGGINGFACE_BOT,
// Kangaroo
Crawler.KANGAROO_BOT,
// Mendable.ai
Crawler.FIRECRAWL_AGENT,
// Meta
Crawler.META_FACEBOOKBOT,
Crawler.META_EXTERNALAGENT,
// OpenAI
Crawler.OPENAI_GPTBOT,
Crawler.OPENAI_SEARCH_BOT,
// Perplexity
Crawler.PERPLEXITY_BOT,
// Replicate
Crawler.REPLICATE_BOT,
// Runpod
Crawler.RUNPOD_BOT,
// SB Intuitions
Crawler.SB_INTUITIONS_BOT,
// Semrush
Crawler.SEMRUSH_BOT_CONTENTSHAKE,
// Timpi
Crawler.TIMPI_BOT,
// Together AI
Crawler.TOGETHER_BOT,
// Velen.io
Crawler.HUNTER_VELENPUBLICWEBCRAWLER,
// Vercel
Crawler.VERCEL_V0BOT,
// Webz.io
Crawler.WEBZIO_OMGILI,
Crawler.WEBZIO_OMGILI_BOT,
Crawler.WEBZIO_EXTENDED,
// X
Crawler.XAI_BOT,
// You.com
Crawler.YOU_BOT,
// Zhipu AI
Crawler.ZHIPU_CHATGLM_SPIDER
]);
const isBot = ua => BotTypes.includes(ua);
const isAIAssistant = ua => AIAssistants.includes(ua);
const isAICrawler = ua => AICrawlers.includes(ua);
module.exports = {
isAIAssistant,
isAICrawler,
isBot
}

View File

@@ -0,0 +1,192 @@
// Generated ESM version of ua-parser-js/bot-detection
// DO NOT EDIT THIS FILE!
// Source: /src/bot-detection/bot-detection.js
//////////////////////////////////////////////////
/* bot-detection submodule of UAParser.js v2.0.7
https://github.com/faisalman/ua-parser-js
Author: Faisal Salman <f@faisalman.com>
AGPLv3 License */
/////////////////////////////////////////////////
/*jshint esversion: 6 */
import { UAParser } from '../main/ua-parser.mjs';
import { Bots, Crawlers, Fetchers } from '../extensions/ua-parser-extensions.mjs';
import { BrowserType, Extension } from '../enums/ua-parser-enums.mjs';
const { Crawler, Fetcher } = Extension.BrowserName;
class BotList {
constructor(ext, prop, list) {
this.ext = ext;
this.prop = prop;
this.list = list.map(x => x.toLowerCase());
}
includes(ua) {
return this.list.includes(
(typeof ua === 'string' ?
new UAParser(ua, this.ext).getBrowser() :
ua.browser
)[this.prop]?.toLowerCase());
}
}
const BotTypes = new BotList(Bots, 'type', [
BrowserType.CLI,
BrowserType.CRAWLER,
BrowserType.FETCHER,
BrowserType.LIBRARY
]);
const AIAssistants = new BotList(Fetchers, 'name', [
// Amazon
Fetcher.AMAZON_NOVA_ACT,
// Anthropic
Fetcher.ANTHROPIC_CLAUDE_USER,
// Cohere
Fetcher.COHERE_AI,
// DuckDuckGo
Fetcher.DUCKDUCKGO_ASSISTBOT,
// Google
Fetcher.GOOGLE_GEMINI_DEEP_RESEARCH,
// Mistral AI
Fetcher.MISTRALAI_USER,
// OpenAI
Fetcher.OPENAI_CHATGPT_USER,
// Perplexity
Fetcher.PERPLEXITY_USER
]);
const AICrawlers = new BotList(Crawlers, 'name', [
// AI2
Crawler.AI2_BOT,
// Amazon
Crawler.AMAZON_BOT,
// Anthropic
Crawler.ANTHROPIC_AI,
Crawler.ANTHROPIC_CLAUDE_BOT,
Crawler.ANTHROPIC_CLAUDE_SEARCHBOT,
Crawler.ANTHROPIC_CLAUDE_WEB,
// Apple
Crawler.APPLE_BOT,
Crawler.APPLE_BOT_EXTENDED,
// Brave
Crawler.BRAVE_BOT,
// ByteDance
Crawler.BYTEDANCE_BYTESPIDER,
Crawler.BYTEDANCE_TIKTOKSPIDER,
// Cohere
Crawler.COHERE_TRAINING_DATA_CRAWLER,
// Common Crawl
Crawler.COMMON_CRAWL_CCBOT,
// Coveo
Crawler.COVEO_BOT,
// DataForSeo
Crawler.DATAFORSEO_BOT,
// DeepSeek
Crawler.DEEPSEEK_BOT,
// Diffbot
Crawler.DIFFBOT,
// Google
Crawler.GOOGLE_EXTENDED,
Crawler.GOOGLE_OTHER,
Crawler.GOOGLE_OTHER_IMAGE,
Crawler.GOOGLE_OTHER_VIDEO,
Crawler.GOOGLE_CLOUDVERTEXBOT,
// Hive AI
Crawler.HIVE_IMAGESIFTBOT,
// Huawei
Crawler.HUAWEI_PETALBOT,
Crawler.HUAWEI_PANGUBOT,
// Hugging Face
Crawler.HUGGINGFACE_BOT,
// Kangaroo
Crawler.KANGAROO_BOT,
// Mendable.ai
Crawler.FIRECRAWL_AGENT,
// Meta
Crawler.META_FACEBOOKBOT,
Crawler.META_EXTERNALAGENT,
// OpenAI
Crawler.OPENAI_GPTBOT,
Crawler.OPENAI_SEARCH_BOT,
// Perplexity
Crawler.PERPLEXITY_BOT,
// Replicate
Crawler.REPLICATE_BOT,
// Runpod
Crawler.RUNPOD_BOT,
// SB Intuitions
Crawler.SB_INTUITIONS_BOT,
// Semrush
Crawler.SEMRUSH_BOT_CONTENTSHAKE,
// Timpi
Crawler.TIMPI_BOT,
// Together AI
Crawler.TOGETHER_BOT,
// Velen.io
Crawler.HUNTER_VELENPUBLICWEBCRAWLER,
// Vercel
Crawler.VERCEL_V0BOT,
// Webz.io
Crawler.WEBZIO_OMGILI,
Crawler.WEBZIO_OMGILI_BOT,
Crawler.WEBZIO_EXTENDED,
// X
Crawler.XAI_BOT,
// You.com
Crawler.YOU_BOT,
// Zhipu AI
Crawler.ZHIPU_CHATGLM_SPIDER
]);
const isBot = ua => BotTypes.includes(ua);
const isAIAssistant = ua => AIAssistants.includes(ua);
const isAICrawler = ua => AICrawlers.includes(ua);
export {
isAIAssistant,
isAICrawler,
isBot
}

View File

@@ -0,0 +1,10 @@
// Type definitions for browser-detection submodule of UAParser.js v2.0.7
// Project: https://github.com/faisalman/ua-parser-js
// Definitions by: Faisal Salman <https://github.com/faisalman>
import type { IResult } from "../main/ua-parser";
export function isChromeFamily(resultOrUA: IResult | string): boolean;
export function isElectron(): boolean;
export function isFromEU(): boolean;
export function isStandalonePWA(): boolean;

View File

@@ -0,0 +1,30 @@
//////////////////////////////////////////////////////
/* browser-detection submodule of UAParser.js v2.0.7
https://github.com/faisalman/ua-parser-js
Author: Faisal Salman <f@faisalman.com>
AGPLv3 License */
/////////////////////////////////////////////////////
/*jshint esversion: 6 */
const { UAParser } = require('../main/ua-parser');
const { EngineName } = require('../enums/ua-parser-enums');
const { isStandalonePWA } = require('is-standalone-pwa');
const { isFromEU } = require('detect-europe-js');
const isChromeFamily = val => !!(
(typeof val === 'string' ?
new UAParser(val).getEngine() :
val.engine
)?.is(EngineName.BLINK));
const isElectron = () => !!(
process?.versions?.hasOwnProperty('electron') || // node.js
/ electron\//i.test(navigator?.userAgent)); // browser
module.exports = {
isChromeFamily,
isElectron,
isFromEU,
isStandalonePWA
}

View File

@@ -0,0 +1,34 @@
// Generated ESM version of ua-parser-js/browser-detection
// DO NOT EDIT THIS FILE!
// Source: /src/browser-detection/browser-detection.js
//////////////////////////////////////////////////////
/* browser-detection submodule of UAParser.js v2.0.7
https://github.com/faisalman/ua-parser-js
Author: Faisal Salman <f@faisalman.com>
AGPLv3 License */
/////////////////////////////////////////////////////
/*jshint esversion: 6 */
import { UAParser } from '../main/ua-parser.mjs';
import { EngineName } from '../enums/ua-parser-enums.mjs';
import { isStandalonePWA } from 'is-standalone-pwa';
import { isFromEU } from 'detect-europe-js';
const isChromeFamily = val => !!(
(typeof val === 'string' ?
new UAParser(val).getEngine() :
val.engine
)?.is(EngineName.BLINK));
const isElectron = () => !!(
process?.versions?.hasOwnProperty('electron') || // node.js
/ electron\//i.test(navigator?.userAgent)); // browser
export {
isChromeFamily,
isElectron,
isFromEU,
isStandalonePWA
}

View File

@@ -0,0 +1,8 @@
// Type definitions for device-detection submodule of UAParser.js v2.0.7
// Project: https://github.com/faisalman/ua-parser-js
// Definitions by: Faisal Salman <https://github.com/faisalman>
import type { IResult } from "../main/ua-parser";
export function getDeviceVendor(model: string): string | undefined;
export function isAppleSilicon(resultOrUA: IResult | string): boolean;

View File

@@ -0,0 +1,43 @@
/////////////////////////////////////////////////////
/* device-detection submodule of UAParser.js v2.0.7
https://github.com/faisalman/ua-parser-js
Author: Faisal Salman <f@faisalman.com>
AGPLv3 License */
////////////////////////////////////////////////////
/*jshint esversion: 6 */
const { UAParser } = require('../main/ua-parser');
const { CPUArch, OSName } = require('../enums/ua-parser-enums');
const getDeviceVendor = (model) => new UAParser(`Mozilla/5.0 (Linux; Android 10; ${model}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36`).getDevice().vendor;
const isAppleSilicon = (val) => {
const { os, cpu } = typeof val !== 'string' ? val : {
os: new UAParser(val).getOS(),
cpu: new UAParser(val).getCPU()
};
if (os.is(OSName.MACOS)) {
if (cpu.is(CPUArch.ARM)) {
return true;
} else if (typeof window !== 'undefined') {
try {
const canvas = document.createElement('canvas');
const webgl = canvas.getContext('webgl2') ||
canvas.getContext('webgl') ||
canvas.getContext('experimental-webgl');
return webgl
.getParameter(webgl.getExtension('WEBGL_debug_renderer_info').UNMASKED_RENDERER_WEBGL)
.match(/apple m\d/i);
} catch {
return false;
}
}
}
return false;
}
module.exports = {
getDeviceVendor,
isAppleSilicon
}

View File

@@ -0,0 +1,47 @@
// Generated ESM version of ua-parser-js/device-detection
// DO NOT EDIT THIS FILE!
// Source: /src/device-detection/device-detection.js
/////////////////////////////////////////////////////
/* device-detection submodule of UAParser.js v2.0.7
https://github.com/faisalman/ua-parser-js
Author: Faisal Salman <f@faisalman.com>
AGPLv3 License */
////////////////////////////////////////////////////
/*jshint esversion: 6 */
import { UAParser } from '../main/ua-parser.mjs';
import { CPUArch, OSName } from '../enums/ua-parser-enums.mjs';
const getDeviceVendor = (model) => new UAParser(`Mozilla/5.0 (Linux; Android 10; ${model}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36`).getDevice().vendor;
const isAppleSilicon = (val) => {
const { os, cpu } = typeof val !== 'string' ? val : {
os: new UAParser(val).getOS(),
cpu: new UAParser(val).getCPU()
};
if (os.is(OSName.MACOS)) {
if (cpu.is(CPUArch.ARM)) {
return true;
} else if (typeof window !== 'undefined') {
try {
const canvas = document.createElement('canvas');
const webgl = canvas.getContext('webgl2') ||
canvas.getContext('webgl') ||
canvas.getContext('experimental-webgl');
return webgl
.getParameter(webgl.getExtension('WEBGL_debug_renderer_info').UNMASKED_RENDERER_WEBGL)
.match(/apple m\d/i);
} catch {
return false;
}
}
}
return false;
}
export {
getDeviceVendor,
isAppleSilicon
}

View File

@@ -3,7 +3,7 @@
// Source: /src/enums/ua-parser-enums.js
///////////////////////////////////////////////
/* Enums for UAParser.js v2.0.6
/* Enums for UAParser.js v2.0.7
https://github.com/faisalman/ua-parser-js
Author: Faisal Salman <f@faisalman.com>
AGPLv3 License */
@@ -19,6 +19,7 @@ export const BrowserName: Readonly<{
AMAYA: 'Amaya',
ANDROID: 'Android Browser',
ARORA: 'Arora',
ATLAS: 'Atlas',
AVANT: 'Avant',
AVAST: 'Avast Secure Browser',
AVG: 'AVG Secure Browser',
@@ -147,6 +148,7 @@ export const BrowserName: Readonly<{
SNAPCHAT: 'Snapchat',
SOGOU_EXPLORER: 'Sogou Explorer',
SOGOU_MOBILE: 'Sogou Mobile',
STEAM: 'Steam',
SURF: 'Surf',
SWIFTFOX: 'Swiftfox',
TESLA: 'Tesla',
@@ -228,8 +230,9 @@ export const DeviceVendor: Readonly<{
ACER: 'Acer',
ADVAN: 'Advan',
ALCATEL: 'Alcatel',
APPLE: 'Apple',
AMAZON: 'Amazon',
ANBERNIC: 'Anbernic',
APPLE: 'Apple',
ARCHOS: 'Archos',
ASUS: 'ASUS',
ATT: 'AT&T',
@@ -258,6 +261,7 @@ export const DeviceVendor: Readonly<{
LAVA: 'Lava',
LENOVO: 'Lenovo',
LG: 'LG',
LOGITECH: 'Logitech',
MEIZU: 'Meizu',
MICROMAX: 'Micromax',
MICROSOFT: 'Microsoft',
@@ -291,6 +295,7 @@ export const DeviceVendor: Readonly<{
TECNO: 'TECNO',
TESLA: 'Tesla',
ULEFONE: 'Ulefone',
VALVE: 'Valve',
VIVO: 'Vivo',
VIZIO: 'Vizio',
VODAFONE: 'Vodafone',
@@ -631,6 +636,7 @@ export const Extension: Readonly<{
},
Fetcher: {
AHREFS_SITEAUDIT: 'AhrefsSiteAudit',
AMAZON_NOVA_ACT: 'NovaAct',
ANTHROPIC_CLAUDE_USER: 'Claude-User',
ASANA: 'Asana',
BETTER_UPTIME_BOT: 'Better Uptime Bot',
@@ -708,9 +714,13 @@ export const Extension: Readonly<{
AIOHTTP: 'aiohttp',
APACHE_HTTPCLIENT: 'Apache-HttpClient',
AXIOS: 'axios',
BUN: 'Bun',
DART: 'Dart',
DENO: 'Deno',
GO_HTTP_CLIENT: 'go-http-client',
GOT: 'got',
GUZZLEHTTP: 'GuzzleHttp',
HACKNEY: 'hackney',
JAVA: 'Java',
JAVA_HTTPCLIENT: 'Java-http-client',
JSDOM: 'jsdom',
@@ -718,16 +728,19 @@ export const Extension: Readonly<{
LUA_RESTY_HTTP: 'lua-resty-http',
NEEDLE: 'Needle',
NUTCH: 'Nutch',
OKHTTP: 'OkHttp',
NODE_FETCH: 'node-fetch',
NODE_JS: 'Node.js',
NODE_SUPERAGENT: 'node-superagent',
OKHTTP: 'OkHttp',
PHP_SOAP: 'PHP-SOAP',
POSTMAN_RUNTIME: 'PostmanRuntime',
PYTHON_HTTPX: 'python-httpx',
PYTHON_URLLIB: 'python-urllib',
PYTHON_URLLIB3: 'python-urllib3',
PYTHON_REQUESTS: 'python-requests',
SCRAPY: 'Scrapy'
REST_CLIENT: 'rest-client',
SCRAPY: 'Scrapy',
UNDICI: 'undici'
}
},
DeviceVendor: {

View File

@@ -1,5 +1,5 @@
///////////////////////////////////////////////
/* Enums for UAParser.js v2.0.6
/* Enums for UAParser.js v2.0.7
https://github.com/faisalman/ua-parser-js
Author: Faisal Salman <f@faisalman.com>
AGPLv3 License */
@@ -15,6 +15,7 @@ const BrowserName = Object.freeze({
AMAYA: 'Amaya',
ANDROID: 'Android Browser',
ARORA: 'Arora',
ATLAS: 'Atlas',
AVANT: 'Avant',
AVAST: 'Avast Secure Browser',
AVG: 'AVG Secure Browser',
@@ -143,6 +144,7 @@ const BrowserName = Object.freeze({
SNAPCHAT: 'Snapchat',
SOGOU_EXPLORER: 'Sogou Explorer',
SOGOU_MOBILE: 'Sogou Mobile',
STEAM: 'Steam',
SURF: 'Surf',
SWIFTFOX: 'Swiftfox',
TESLA: 'Tesla',
@@ -224,8 +226,9 @@ const DeviceVendor = Object.freeze({
ACER: 'Acer',
ADVAN: 'Advan',
ALCATEL: 'Alcatel',
APPLE: 'Apple',
AMAZON: 'Amazon',
ANBERNIC: 'Anbernic',
APPLE: 'Apple',
ARCHOS: 'Archos',
ASUS: 'ASUS',
ATT: 'AT&T',
@@ -254,6 +257,7 @@ const DeviceVendor = Object.freeze({
LAVA: 'Lava',
LENOVO: 'Lenovo',
LG: 'LG',
LOGITECH: 'Logitech',
MEIZU: 'Meizu',
MICROMAX: 'Micromax',
MICROSOFT: 'Microsoft',
@@ -287,6 +291,7 @@ const DeviceVendor = Object.freeze({
TECNO: 'TECNO',
TESLA: 'Tesla',
ULEFONE: 'Ulefone',
VALVE: 'Valve',
VIVO: 'Vivo',
VIZIO: 'Vizio',
VODAFONE: 'Vodafone',
@@ -627,6 +632,7 @@ const Extension = Object.freeze({
},
Fetcher: {
AHREFS_SITEAUDIT: 'AhrefsSiteAudit',
AMAZON_NOVA_ACT: 'NovaAct',
ANTHROPIC_CLAUDE_USER: 'Claude-User',
ASANA: 'Asana',
BETTER_UPTIME_BOT: 'Better Uptime Bot',
@@ -704,9 +710,13 @@ const Extension = Object.freeze({
AIOHTTP: 'aiohttp',
APACHE_HTTPCLIENT: 'Apache-HttpClient',
AXIOS: 'axios',
BUN: 'Bun',
DART: 'Dart',
DENO: 'Deno',
GO_HTTP_CLIENT: 'go-http-client',
GOT: 'got',
GUZZLEHTTP: 'GuzzleHttp',
HACKNEY: 'hackney',
JAVA: 'Java',
JAVA_HTTPCLIENT: 'Java-http-client',
JSDOM: 'jsdom',
@@ -714,16 +724,19 @@ const Extension = Object.freeze({
LUA_RESTY_HTTP: 'lua-resty-http',
NEEDLE: 'Needle',
NUTCH: 'Nutch',
OKHTTP: 'OkHttp',
NODE_FETCH: 'node-fetch',
NODE_JS: 'Node.js',
NODE_SUPERAGENT: 'node-superagent',
OKHTTP: 'OkHttp',
PHP_SOAP: 'PHP-SOAP',
POSTMAN_RUNTIME: 'PostmanRuntime',
PYTHON_HTTPX: 'python-httpx',
PYTHON_URLLIB: 'python-urllib',
PYTHON_URLLIB3: 'python-urllib3',
PYTHON_REQUESTS: 'python-requests',
SCRAPY: 'Scrapy'
REST_CLIENT: 'rest-client',
SCRAPY: 'Scrapy',
UNDICI: 'undici'
}
},
DeviceVendor: {

View File

@@ -3,7 +3,7 @@
// Source: /src/enums/ua-parser-enums.js
///////////////////////////////////////////////
/* Enums for UAParser.js v2.0.6
/* Enums for UAParser.js v2.0.7
https://github.com/faisalman/ua-parser-js
Author: Faisal Salman <f@faisalman.com>
AGPLv3 License */
@@ -19,6 +19,7 @@ const BrowserName = Object.freeze({
AMAYA: 'Amaya',
ANDROID: 'Android Browser',
ARORA: 'Arora',
ATLAS: 'Atlas',
AVANT: 'Avant',
AVAST: 'Avast Secure Browser',
AVG: 'AVG Secure Browser',
@@ -147,6 +148,7 @@ const BrowserName = Object.freeze({
SNAPCHAT: 'Snapchat',
SOGOU_EXPLORER: 'Sogou Explorer',
SOGOU_MOBILE: 'Sogou Mobile',
STEAM: 'Steam',
SURF: 'Surf',
SWIFTFOX: 'Swiftfox',
TESLA: 'Tesla',
@@ -228,8 +230,9 @@ const DeviceVendor = Object.freeze({
ACER: 'Acer',
ADVAN: 'Advan',
ALCATEL: 'Alcatel',
APPLE: 'Apple',
AMAZON: 'Amazon',
ANBERNIC: 'Anbernic',
APPLE: 'Apple',
ARCHOS: 'Archos',
ASUS: 'ASUS',
ATT: 'AT&T',
@@ -258,6 +261,7 @@ const DeviceVendor = Object.freeze({
LAVA: 'Lava',
LENOVO: 'Lenovo',
LG: 'LG',
LOGITECH: 'Logitech',
MEIZU: 'Meizu',
MICROMAX: 'Micromax',
MICROSOFT: 'Microsoft',
@@ -291,6 +295,7 @@ const DeviceVendor = Object.freeze({
TECNO: 'TECNO',
TESLA: 'Tesla',
ULEFONE: 'Ulefone',
VALVE: 'Valve',
VIVO: 'Vivo',
VIZIO: 'Vizio',
VODAFONE: 'Vodafone',
@@ -631,6 +636,7 @@ const Extension = Object.freeze({
},
Fetcher: {
AHREFS_SITEAUDIT: 'AhrefsSiteAudit',
AMAZON_NOVA_ACT: 'NovaAct',
ANTHROPIC_CLAUDE_USER: 'Claude-User',
ASANA: 'Asana',
BETTER_UPTIME_BOT: 'Better Uptime Bot',
@@ -708,9 +714,13 @@ const Extension = Object.freeze({
AIOHTTP: 'aiohttp',
APACHE_HTTPCLIENT: 'Apache-HttpClient',
AXIOS: 'axios',
BUN: 'Bun',
DART: 'Dart',
DENO: 'Deno',
GO_HTTP_CLIENT: 'go-http-client',
GOT: 'got',
GUZZLEHTTP: 'GuzzleHttp',
HACKNEY: 'hackney',
JAVA: 'Java',
JAVA_HTTPCLIENT: 'Java-http-client',
JSDOM: 'jsdom',
@@ -718,16 +728,19 @@ const Extension = Object.freeze({
LUA_RESTY_HTTP: 'lua-resty-http',
NEEDLE: 'Needle',
NUTCH: 'Nutch',
OKHTTP: 'OkHttp',
NODE_FETCH: 'node-fetch',
NODE_JS: 'Node.js',
NODE_SUPERAGENT: 'node-superagent',
OKHTTP: 'OkHttp',
PHP_SOAP: 'PHP-SOAP',
POSTMAN_RUNTIME: 'PostmanRuntime',
PYTHON_HTTPX: 'python-httpx',
PYTHON_URLLIB: 'python-urllib',
PYTHON_URLLIB3: 'python-urllib3',
PYTHON_REQUESTS: 'python-requests',
SCRAPY: 'Scrapy'
REST_CLIENT: 'rest-client',
SCRAPY: 'Scrapy',
UNDICI: 'undici'
}
},
DeviceVendor: {

View File

@@ -1,5 +1,5 @@
///////////////////////////////////////////////
/* Extensions for UAParser.js v2.0.6
/* Extensions for UAParser.js v2.0.7
https://github.com/faisalman/ua-parser-js
Author: Faisal Salman <f@faisalman.com>
AGPLv3 License */
@@ -258,7 +258,7 @@ const Emails = Object.freeze({
const Fetchers = Object.freeze({
browser : [
[
// Asana / Bitlybot / Better Uptime / BingPreview / Blueno / Cohere-AI / HubSpot Page Fetcher / kakaotalk-scrap / Mastodon / MicrosoftPreview / Pinterestbot / Redditbot / Rogerbot / SiteAuditBot / Telegrambot / Twitterbot / UptimeRobot
// Asana / Bitlybot / Better Uptime / BingPreview / Blueno / Cohere-AI / HubSpot Page Fetcher / kakaotalk-scrap / Mastodon / MicrosoftPreview / Pinterestbot / Redditbot / Rogerbot / SiteAuditBot / Telegrambot / Twitterbot / UptimeRobot / WhatsApp
// AhrefsSiteAudit - https://ahrefs.com/robot/site-audit
// Buffer Link Preview Bot - https://scraper.buffer.com/about/bots/link-preview-bot
// ChatGPT-User - https://platform.openai.com/docs/plugins/bot
@@ -268,19 +268,19 @@ const Fetchers = Object.freeze({
// Perplexity-User - https://docs.perplexity.ai/guides/bots
// MistralAI-User - https://docs.mistral.ai/robots/
// Yandex Bots - https://yandex.com/bots
/(asana|ahrefssiteaudit|(?:bing|microsoft)preview|blueno|(?:chatgpt|claude|mistralai|perplexity)-user|cohere-ai|hubspot page fetcher|mastodon|(?:bitly|bufferlinkpreview|discord|duckassist|linkedin|pinterest|reddit|roger|siteaudit|twitter|uptimero|zoom)bot|google-site-verification|iframely|kakaotalk-scrap|meta-externalfetcher|y!?j-dlc|yandex(?:calendar|direct(?:dyn)?|fordomain|pagechecker|searchshop)|yadirectfetcher)\/([\w\.]+)/i,
/(asana|ahrefssiteaudit|(?:bing|microsoft)preview|blueno|(?:chatgpt|claude|mistralai|perplexity)-user|cohere-ai|hubspot page fetcher|mastodon|(?:bitly|bufferlinkpreview|discord|duckassist|linkedin|pinterest|reddit|roger|siteaudit|twitter|uptimero|zoom)bot|google-site-verification|iframely|kakaotalk-scrap|meta-externalfetcher|y!?j-dlc|yandex(?:calendar|direct(?:dyn)?|fordomain|pagechecker|searchshop)|yadirectfetcher|whatsapp)\/([\w\.]+)/i,
// Bluesky
/(bluesky) cardyb\/([\w\.]+)/i,
// Nova Act - https://github.com/aws/nova-act
/agent-(novaact)\/([\w\.]+)/i,
// Skype
/(skypeuripreview) preview\/([\w\.]+)/i,
// Slackbot - https://api.slack.com/robots
/(slack(?:bot)?(?:-imgproxy|-linkexpanding)?) ([\w\.]+)/i,
// WhatsApp
/(whatsapp)\/([\w\.]+)/i
/(slack(?:bot)?(?:-imgproxy|-linkexpanding)?) ([\w\.]+)/i
],
[NAME, VERSION, [TYPE, FETCHER]],
@@ -389,13 +389,15 @@ const MediaPlayers = Object.freeze({
const Libraries = Object.freeze({
browser : [
// Apache-HttpClient/Axios/go-http-client/got/GuzzleHttp/Java[-HttpClient]/jsdom/libwww-perl/lua-resty-http/Needle/node-fetch/OkHttp/PHP-SOAP/PostmanRuntime/python-urllib/python-requests/Scrapy/superagent
[
/^(apache-httpclient|axios|(?:go|java)-http-client|got|guzzlehttp|java|libwww-perl|lua-resty-http|needle|node-(?:fetch|superagent)|okhttp|php-soap|postmanruntime|python-(?:httpx|urllib[23]?|requests)|scrapy)\/([\w\.]+)/i,
// Apache-HttpClient/Axios/Bun/Dart/go-http-client/got/GuzzleHttp/hackney/Java[-HttpClient]/jsdom/libwww-perl/lua-resty-http/Needle/Node.js/node-fetch/OkHttp/PHP-SOAP/PostmanRuntime/python-urllib/python-requests/rest-client/Scrapy/superagent
/^(apache-httpclient|axios|bun|dart|deno|(?:go|java)-http-client|got|guzzlehttp|hackney|java|libwww-perl|lua-resty-http|needle|node(?:\.js|-fetch|-superagent)|okhttp|php-soap|postmanruntime|python-(?:httpx|urllib[23]?|requests)|rest-client|scrapy)\/([\w\.]+)/i,
/(adobeair|aiohttp|jsdom)\/([\w\.]+)/i,
/(nutch)-([\w\.-]+)(\(|$)/i,
/\((java)\/([\w\.]+)/i
], [NAME, VERSION, [TYPE, LIBRARY]]
], [NAME, VERSION, [TYPE, LIBRARY]], [
/(node-fetch|undici)/i
], [NAME, [TYPE, LIBRARY]]
]
});

View File

@@ -3,7 +3,7 @@
// Source: /src/extensions/ua-parser-extensions.js
///////////////////////////////////////////////
/* Extensions for UAParser.js v2.0.6
/* Extensions for UAParser.js v2.0.7
https://github.com/faisalman/ua-parser-js
Author: Faisal Salman <f@faisalman.com>
AGPLv3 License */
@@ -262,7 +262,7 @@ const Emails = Object.freeze({
const Fetchers = Object.freeze({
browser : [
[
// Asana / Bitlybot / Better Uptime / BingPreview / Blueno / Cohere-AI / HubSpot Page Fetcher / kakaotalk-scrap / Mastodon / MicrosoftPreview / Pinterestbot / Redditbot / Rogerbot / SiteAuditBot / Telegrambot / Twitterbot / UptimeRobot
// Asana / Bitlybot / Better Uptime / BingPreview / Blueno / Cohere-AI / HubSpot Page Fetcher / kakaotalk-scrap / Mastodon / MicrosoftPreview / Pinterestbot / Redditbot / Rogerbot / SiteAuditBot / Telegrambot / Twitterbot / UptimeRobot / WhatsApp
// AhrefsSiteAudit - https://ahrefs.com/robot/site-audit
// Buffer Link Preview Bot - https://scraper.buffer.com/about/bots/link-preview-bot
// ChatGPT-User - https://platform.openai.com/docs/plugins/bot
@@ -272,19 +272,19 @@ const Fetchers = Object.freeze({
// Perplexity-User - https://docs.perplexity.ai/guides/bots
// MistralAI-User - https://docs.mistral.ai/robots/
// Yandex Bots - https://yandex.com/bots
/(asana|ahrefssiteaudit|(?:bing|microsoft)preview|blueno|(?:chatgpt|claude|mistralai|perplexity)-user|cohere-ai|hubspot page fetcher|mastodon|(?:bitly|bufferlinkpreview|discord|duckassist|linkedin|pinterest|reddit|roger|siteaudit|twitter|uptimero|zoom)bot|google-site-verification|iframely|kakaotalk-scrap|meta-externalfetcher|y!?j-dlc|yandex(?:calendar|direct(?:dyn)?|fordomain|pagechecker|searchshop)|yadirectfetcher)\/([\w\.]+)/i,
/(asana|ahrefssiteaudit|(?:bing|microsoft)preview|blueno|(?:chatgpt|claude|mistralai|perplexity)-user|cohere-ai|hubspot page fetcher|mastodon|(?:bitly|bufferlinkpreview|discord|duckassist|linkedin|pinterest|reddit|roger|siteaudit|twitter|uptimero|zoom)bot|google-site-verification|iframely|kakaotalk-scrap|meta-externalfetcher|y!?j-dlc|yandex(?:calendar|direct(?:dyn)?|fordomain|pagechecker|searchshop)|yadirectfetcher|whatsapp)\/([\w\.]+)/i,
// Bluesky
/(bluesky) cardyb\/([\w\.]+)/i,
// Nova Act - https://github.com/aws/nova-act
/agent-(novaact)\/([\w\.]+)/i,
// Skype
/(skypeuripreview) preview\/([\w\.]+)/i,
// Slackbot - https://api.slack.com/robots
/(slack(?:bot)?(?:-imgproxy|-linkexpanding)?) ([\w\.]+)/i,
// WhatsApp
/(whatsapp)\/([\w\.]+)/i
/(slack(?:bot)?(?:-imgproxy|-linkexpanding)?) ([\w\.]+)/i
],
[NAME, VERSION, [TYPE, FETCHER]],
@@ -393,13 +393,15 @@ const MediaPlayers = Object.freeze({
const Libraries = Object.freeze({
browser : [
// Apache-HttpClient/Axios/go-http-client/got/GuzzleHttp/Java[-HttpClient]/jsdom/libwww-perl/lua-resty-http/Needle/node-fetch/OkHttp/PHP-SOAP/PostmanRuntime/python-urllib/python-requests/Scrapy/superagent
[
/^(apache-httpclient|axios|(?:go|java)-http-client|got|guzzlehttp|java|libwww-perl|lua-resty-http|needle|node-(?:fetch|superagent)|okhttp|php-soap|postmanruntime|python-(?:httpx|urllib[23]?|requests)|scrapy)\/([\w\.]+)/i,
// Apache-HttpClient/Axios/Bun/Dart/go-http-client/got/GuzzleHttp/hackney/Java[-HttpClient]/jsdom/libwww-perl/lua-resty-http/Needle/Node.js/node-fetch/OkHttp/PHP-SOAP/PostmanRuntime/python-urllib/python-requests/rest-client/Scrapy/superagent
/^(apache-httpclient|axios|bun|dart|deno|(?:go|java)-http-client|got|guzzlehttp|hackney|java|libwww-perl|lua-resty-http|needle|node(?:\.js|-fetch|-superagent)|okhttp|php-soap|postmanruntime|python-(?:httpx|urllib[23]?|requests)|rest-client|scrapy)\/([\w\.]+)/i,
/(adobeair|aiohttp|jsdom)\/([\w\.]+)/i,
/(nutch)-([\w\.-]+)(\(|$)/i,
/\((java)\/([\w\.]+)/i
], [NAME, VERSION, [TYPE, LIBRARY]]
], [NAME, VERSION, [TYPE, LIBRARY]], [
/(node-fetch|undici)/i
], [NAME, [TYPE, LIBRARY]]
]
});

View File

@@ -4,12 +4,37 @@
import type { IResult } from "../main/ua-parser";
export function getDeviceVendor(model: string): string | undefined;
export function isAppleSilicon(resultOrUA: IResult | string): boolean;
export function isAIBot(resultOrUA: IResult | string): boolean;
export function isBot(resultOrUA: IResult | string): boolean;
export function isChromeFamily(resultOrUA: IResult | string): boolean;
export function isElectron(): boolean;
export function isFromEU(): boolean;
export function isFrozenUA(ua: string): boolean;
/**
* @deprecated Moved to `device-detection` submodule
*/
export function getDeviceVendor(model: string): string | undefined;
/**
* @deprecated Moved to `device-detection` submodule
*/
export function isAppleSilicon(resultOrUA: IResult | string): boolean;
/**
* @deprecated Moved to `bot-detection` submodule
*/
export function isAIBot(resultOrUA: IResult | string): boolean;
/**
* @deprecated Moved to `bot-detection` submodule
*/
export function isBot(resultOrUA: IResult | string): boolean;
/**
* @deprecated Moved to `browser-detection` submodule
*/
export function isChromeFamily(resultOrUA: IResult | string): boolean;
/**
* @deprecated Moved to `browser-detection` submodule
*/
export function isElectron(): boolean;
/**
* @deprecated Moved to `browser-detection` submodule
*/
export function isFromEU(): boolean;
/**
* @deprecated Moved to `browser-detection` submodule
*/
export function isStandalonePWA(): boolean;

View File

@@ -1,5 +1,5 @@
///////////////////////////////////////////////
/* Helpers for UAParser.js v2.0.6
/* Helpers for UAParser.js v2.0.7
https://github.com/faisalman/ua-parser-js
Author: Faisal Salman <f@faisalman.com>
AGPLv3 License */
@@ -8,169 +8,52 @@
/*jshint esversion: 6 */
const { UAParser } = require('../main/ua-parser');
const { CPUArch, OSName, EngineName, Extension, BrowserType } = require('../enums/ua-parser-enums');
const { Bots, Crawlers } = require('../extensions/ua-parser-extensions');
const { isFromEU } = require('detect-europe-js');
const { EngineName } = require('../enums/ua-parser-enums');
const { getDeviceVendor: _getDeviceVendor, isAppleSilicon: _isAppleSilicon } = require('../device-detection/device-detection');
const { isBot: _isBot, isAICrawler } = require('../bot-detection/bot-detection');
const { isChromeFamily: _isChromeFamily, isElectron: _isElectron, isStandalonePWA: _isStandalonePWA } = require('../browser-detection/browser-detection');
const { isFromEU: _isFromEU } = require('../browser-detection/browser-detection');
const { isFrozenUA } = require('ua-is-frozen');
const { isStandalonePWA } = require('is-standalone-pwa');
const { Crawler } = Extension.BrowserName;
const toResult = (value, head, ext) => typeof value === 'string' ? UAParser(value, head, ext) : value;
/**
* @deprecated Moved to `device-detection` submodule
*/
const getDeviceVendor = _getDeviceVendor;
const getDeviceVendor = (model) => UAParser(`Mozilla/5.0 (Linux; Android 10; ${model}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36`).device.vendor;
/**
* @deprecated Moved to `device-detection` submodule
*/
const isAppleSilicon = _isAppleSilicon;
const isAppleSilicon = (resultOrUA) => {
const res = toResult(resultOrUA);
if (res.os.is(OSName.MACOS)) {
if (res.cpu.is(CPUArch.ARM)) {
return true;
}
if (typeof resultOrUA !== 'string' && typeof window !== 'undefined') {
try {
const canvas = document.createElement('canvas');
const webgl = canvas.getContext('webgl2') || canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
const debug = webgl.getExtension('WEBGL_debug_renderer_info');
const renderer = webgl.getParameter(debug.UNMASKED_RENDERER_WEBGL);
if (renderer.match(/apple m\d/i)) {
return true;
}
} catch {
return false;
}
}
}
return false;
}
/**
* @deprecated Moved to `bot-detection` submodule
*/
const isAIBot = isAICrawler;
const isAIBot = (resultOrUA) => [
/**
* @deprecated Moved to `bot-detection` submodule
*/
const isBot = _isBot;
// AI2
Crawler.AI2_BOT,
/**
* @deprecated Moved to `browser-detection` submodule
*/
const isChromeFamily = _isChromeFamily;
// Amazon
Crawler.AMAZON_BOT,
/**
* @deprecated Moved to `browser-detection` submodule
*/
const isElectron = () => _isElectron;
// Anthropic
Crawler.ANTHROPIC_AI,
Crawler.ANTHROPIC_CLAUDE_BOT,
Crawler.ANTHROPIC_CLAUDE_SEARCHBOT,
Crawler.ANTHROPIC_CLAUDE_WEB,
/**
* @deprecated Moved to `browser-detection` submodule
*/
const isFromEU = _isFromEU;
// Apple
Crawler.APPLE_BOT,
Crawler.APPLE_BOT_EXTENDED,
// Brave
Crawler.BRAVE_BOT,
// ByteDance
Crawler.BYTEDANCE_BYTESPIDER,
Crawler.BYTEDANCE_TIKTOKSPIDER,
// Cohere
Crawler.COHERE_TRAINING_DATA_CRAWLER,
// Common Crawl
Crawler.COMMON_CRAWL_CCBOT,
// Coveo
Crawler.COVEO_BOT,
// DataForSeo
Crawler.DATAFORSEO_BOT,
// DeepSeek
Crawler.DEEPSEEK_BOT,
// Diffbot
Crawler.DIFFBOT,
// Google
Crawler.GOOGLE_EXTENDED,
Crawler.GOOGLE_OTHER,
Crawler.GOOGLE_OTHER_IMAGE,
Crawler.GOOGLE_OTHER_VIDEO,
Crawler.GOOGLE_CLOUDVERTEXBOT,
// Hive AI
Crawler.HIVE_IMAGESIFTBOT,
// Huawei
Crawler.HUAWEI_PETALBOT,
Crawler.HUAWEI_PANGUBOT,
// Hugging Face
Crawler.HUGGINGFACE_BOT,
// Kangaroo
Crawler.KANGAROO_BOT,
// Mendable.ai
Crawler.FIRECRAWL_AGENT,
// Meta
Crawler.META_FACEBOOKBOT,
Crawler.META_EXTERNALAGENT,
// OpenAI
Crawler.OPENAI_GPTBOT,
Crawler.OPENAI_SEARCH_BOT,
// Perplexity
Crawler.PERPLEXITY_BOT,
// Replicate
Crawler.REPLICATE_BOT,
// Runpod
Crawler.RUNPOD_BOT,
// SB Intuitions
Crawler.SB_INTUITIONS_BOT,
// Semrush
Crawler.SEMRUSH_BOT_CONTENTSHAKE,
// Timpi
Crawler.TIMPI_BOT,
// Together AI
Crawler.TOGETHER_BOT,
// Velen.io
Crawler.HUNTER_VELENPUBLICWEBCRAWLER,
// Vercel
Crawler.VERCEL_V0BOT,
// Webz.io
Crawler.WEBZIO_OMGILI,
Crawler.WEBZIO_OMGILI_BOT,
Crawler.WEBZIO_EXTENDED,
// X
Crawler.XAI_BOT,
// You.com
Crawler.YOU_BOT,
// Zhipu AI
Crawler.ZHIPU_CHATGLM_SPIDER
]
.map((s) => s.toLowerCase())
.includes(String(toResult(resultOrUA, Crawlers).browser.name).toLowerCase());
const isBot = (resultOrUA) => [
BrowserType.CLI,
BrowserType.CRAWLER,
BrowserType.FETCHER,
BrowserType.LIBRARY
].includes(toResult(resultOrUA, Bots).browser.type);
const isChromeFamily = (resultOrUA) => toResult(resultOrUA).engine.is(EngineName.BLINK);
const isElectron = () => !!(process?.versions?.hasOwnProperty('electron') || // node.js
/ electron\//i.test(navigator?.userAgent)); // browser
/**
* @deprecated Moved to `browser-detection` submodule
*/
const isStandalonePWA = _isStandalonePWA;
module.exports = {
getDeviceVendor,

View File

@@ -3,7 +3,7 @@
// Source: /src/helpers/ua-parser-helpers.js
///////////////////////////////////////////////
/* Helpers for UAParser.js v2.0.6
/* Helpers for UAParser.js v2.0.7
https://github.com/faisalman/ua-parser-js
Author: Faisal Salman <f@faisalman.com>
AGPLv3 License */
@@ -12,169 +12,52 @@
/*jshint esversion: 6 */
import { UAParser } from '../main/ua-parser.mjs';
import { CPUArch, OSName, EngineName, Extension, BrowserType } from '../enums/ua-parser-enums.mjs';
import { Bots, Crawlers } from '../extensions/ua-parser-extensions.mjs';
import { isFromEU } from 'detect-europe-js';
import { EngineName } from '../enums/ua-parser-enums.mjs';
import { getDeviceVendor: _getDeviceVendor, isAppleSilicon: _isAppleSilicon } from '../device-detection/device-detection.mjs';
import { isBot: _isBot, isAICrawler } from '../bot-detection/bot-detection.mjs';
import { isChromeFamily: _isChromeFamily, isElectron: _isElectron, isStandalonePWA: _isStandalonePWA } from '../browser-detection/browser-detection.mjs';
import { isFromEU: _isFromEU } from '../browser-detection/browser-detection.mjs';
import { isFrozenUA } from 'ua-is-frozen';
import { isStandalonePWA } from 'is-standalone-pwa';
const { Crawler } = Extension.BrowserName;
const toResult = (value, head, ext) => typeof value === 'string' ? UAParser(value, head, ext) : value;
/**
* @deprecated Moved to `device-detection` submodule
*/
const getDeviceVendor = _getDeviceVendor;
const getDeviceVendor = (model) => UAParser(`Mozilla/5.0 (Linux; Android 10; ${model}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36`).device.vendor;
/**
* @deprecated Moved to `device-detection` submodule
*/
const isAppleSilicon = _isAppleSilicon;
const isAppleSilicon = (resultOrUA) => {
const res = toResult(resultOrUA);
if (res.os.is(OSName.MACOS)) {
if (res.cpu.is(CPUArch.ARM)) {
return true;
}
if (typeof resultOrUA !== 'string' && typeof window !== 'undefined') {
try {
const canvas = document.createElement('canvas');
const webgl = canvas.getContext('webgl2') || canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
const debug = webgl.getExtension('WEBGL_debug_renderer_info');
const renderer = webgl.getParameter(debug.UNMASKED_RENDERER_WEBGL);
if (renderer.match(/apple m\d/i)) {
return true;
}
} catch {
return false;
}
}
}
return false;
}
/**
* @deprecated Moved to `bot-detection` submodule
*/
const isAIBot = isAICrawler;
const isAIBot = (resultOrUA) => [
/**
* @deprecated Moved to `bot-detection` submodule
*/
const isBot = _isBot;
// AI2
Crawler.AI2_BOT,
/**
* @deprecated Moved to `browser-detection` submodule
*/
const isChromeFamily = _isChromeFamily;
// Amazon
Crawler.AMAZON_BOT,
/**
* @deprecated Moved to `browser-detection` submodule
*/
const isElectron = () => _isElectron;
// Anthropic
Crawler.ANTHROPIC_AI,
Crawler.ANTHROPIC_CLAUDE_BOT,
Crawler.ANTHROPIC_CLAUDE_SEARCHBOT,
Crawler.ANTHROPIC_CLAUDE_WEB,
/**
* @deprecated Moved to `browser-detection` submodule
*/
const isFromEU = _isFromEU;
// Apple
Crawler.APPLE_BOT,
Crawler.APPLE_BOT_EXTENDED,
// Brave
Crawler.BRAVE_BOT,
// ByteDance
Crawler.BYTEDANCE_BYTESPIDER,
Crawler.BYTEDANCE_TIKTOKSPIDER,
// Cohere
Crawler.COHERE_TRAINING_DATA_CRAWLER,
// Common Crawl
Crawler.COMMON_CRAWL_CCBOT,
// Coveo
Crawler.COVEO_BOT,
// DataForSeo
Crawler.DATAFORSEO_BOT,
// DeepSeek
Crawler.DEEPSEEK_BOT,
// Diffbot
Crawler.DIFFBOT,
// Google
Crawler.GOOGLE_EXTENDED,
Crawler.GOOGLE_OTHER,
Crawler.GOOGLE_OTHER_IMAGE,
Crawler.GOOGLE_OTHER_VIDEO,
Crawler.GOOGLE_CLOUDVERTEXBOT,
// Hive AI
Crawler.HIVE_IMAGESIFTBOT,
// Huawei
Crawler.HUAWEI_PETALBOT,
Crawler.HUAWEI_PANGUBOT,
// Hugging Face
Crawler.HUGGINGFACE_BOT,
// Kangaroo
Crawler.KANGAROO_BOT,
// Mendable.ai
Crawler.FIRECRAWL_AGENT,
// Meta
Crawler.META_FACEBOOKBOT,
Crawler.META_EXTERNALAGENT,
// OpenAI
Crawler.OPENAI_GPTBOT,
Crawler.OPENAI_SEARCH_BOT,
// Perplexity
Crawler.PERPLEXITY_BOT,
// Replicate
Crawler.REPLICATE_BOT,
// Runpod
Crawler.RUNPOD_BOT,
// SB Intuitions
Crawler.SB_INTUITIONS_BOT,
// Semrush
Crawler.SEMRUSH_BOT_CONTENTSHAKE,
// Timpi
Crawler.TIMPI_BOT,
// Together AI
Crawler.TOGETHER_BOT,
// Velen.io
Crawler.HUNTER_VELENPUBLICWEBCRAWLER,
// Vercel
Crawler.VERCEL_V0BOT,
// Webz.io
Crawler.WEBZIO_OMGILI,
Crawler.WEBZIO_OMGILI_BOT,
Crawler.WEBZIO_EXTENDED,
// X
Crawler.XAI_BOT,
// You.com
Crawler.YOU_BOT,
// Zhipu AI
Crawler.ZHIPU_CHATGLM_SPIDER
]
.map((s) => s.toLowerCase())
.includes(String(toResult(resultOrUA, Crawlers).browser.name).toLowerCase());
const isBot = (resultOrUA) => [
BrowserType.CLI,
BrowserType.CRAWLER,
BrowserType.FETCHER,
BrowserType.LIBRARY
].includes(toResult(resultOrUA, Bots).browser.type);
const isChromeFamily = (resultOrUA) => toResult(resultOrUA).engine.is(EngineName.BLINK);
const isElectron = () => !!(process?.versions?.hasOwnProperty('electron') || // node.js
/ electron\//i.test(navigator?.userAgent)); // browser
/**
* @deprecated Moved to `browser-detection` submodule
*/
const isStandalonePWA = _isStandalonePWA;
export {
getDeviceVendor,

View File

@@ -1,4 +1,4 @@
// Type definitions for UAParser.js v2.0.6
// Type definitions for UAParser.js v2.0.7
// Project: https://github.com/faisalman/ua-parser-js
// Definitions by: Faisal Salman <https://github.com/faisalman>

View File

@@ -1,5 +1,5 @@
/////////////////////////////////////////////////////////////////////////////////
/* UAParser.js v2.0.6
/* UAParser.js v2.0.7
Copyright © 2012-2025 Faisal Salman <f@faisalman.com>
AGPLv3 License *//*
Detect Browser, Engine, OS, CPU, and Device type/model from User-Agent data.
@@ -19,7 +19,7 @@
// Constants
/////////////
var LIBVERSION = '2.0.6',
var LIBVERSION = '2.0.7',
UA_MAX_LENGTH = 500,
USER_AGENT = 'user-agent',
EMPTY = '',
@@ -376,8 +376,8 @@
/(?: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/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
/(atlas|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,
// Atlas/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
/(ecosia|weibo)(?:__| \w+@)([\d\.]+)/i // Ecosia/Weibo
@@ -432,7 +432,7 @@
/(tesla)(?: qtcarbrowser|\/(20\d\d\.[-\w\.]+))/i, // Tesla
/m?(qqbrowser|2345(?=browser|chrome|explorer))\w*[\/ ]?v?([\w\.]+)/i // QQ/2345
], [NAME, VERSION], [
/(lbbrowser|rekonq)/i // LieBao Browser/Rekonq
/(lbbrowser|rekonq|steam(?= (clie|tenf|gameo)))/i // LieBao Browser/Rekonq/Steam
], [NAME], [
/ome\/([\w\.]+) \w* ?(iron) saf/i, // Iron
/ome\/([\w\.]+).+qihu (360)[es]e/i // 360
@@ -598,12 +598,12 @@
/\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
/\b; (\w+) build\/hm\1/i, // Xiaomi Hongmi 'numeric' models
/\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|max|cc)?[_ ]?(?:\d{0,2}\w?)[_ ]?(?:plus|se|lite|pro)?( 5g|lte)?)(?: bui|\))/i, // Xiaomi Mi
// Xiaomi Redmi / POCO / Black Shark / Qin
/oid[^\)]+; (redmi[\-_ ]?(?:note|k)?[\w_ ]+|m?[12]\d[01]\d\w{3,6}|poco[\w ]+|(shark )?\w{3}-[ah]0|qin ?[1-3](s\+|ultra| pro)?)( bui|; wv|\))/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,
/ ([\w ]+) miui\/v?\d/i
], [[MODEL, /_/g, ' '], [VENDOR, XIAOMI], [TYPE, MOBILE]], [
@@ -852,10 +852,10 @@
], [MODEL, [VENDOR, MICROSOFT], [TYPE, CONSOLE]], [
/(ouya)/i, // Ouya
/(nintendo) (\w+)/i, // Nintendo
/(retroid) (pocket ([^\)]+))/i // Retroid Pocket
], [VENDOR, MODEL, [TYPE, CONSOLE]], [
/droid.+; (shield)( bui|\))/i // Nvidia Portable
], [MODEL, [VENDOR, NVIDIA], [TYPE, CONSOLE]], [
/(retroid) (pocket ([^\)]+))/i, // Retroid Pocket
/(valve).+(steam deck)/i,
/droid.+; ((shield|rgcube|gr0006))( bui|\))/i // Nvidia Portable/Anbernic/Logitech
], [[VENDOR, strMapper, { 'Nvidia': 'Shield', 'Anbernic': 'RGCUBE', 'Logitech': 'GR0006' }], MODEL, [TYPE, CONSOLE]], [
///////////////////
// WEARABLES
@@ -864,7 +864,7 @@
/\b(sm-[lr]\d\d[0156][fnuw]?s?|gear live)\b/i // Samsung Galaxy Watch
], [MODEL, [VENDOR, SAMSUNG], [TYPE, WEARABLE]], [
/((pebble))app/i, // Pebble
/(asus|google|lg|oppo) ((pixel |zen)?watch[\w ]*)( bui|\))/i // Asus ZenWatch / LG Watch / Pixel Watch
/(asus|google|lg|oppo|xiaomi) ((pixel |zen)?watch[\w ]*)( bui|\))/i // Asus ZenWatch / LG Watch / Pixel Watch / Xiaomi Watch
], [VENDOR, MODEL, [TYPE, WEARABLE]], [
/(ow(?:19|20)?we?[1-3]{1,3})/i // Oppo Watch
], [MODEL, [VENDOR, OPPO], [TYPE, WEARABLE]], [
@@ -968,7 +968,8 @@
// iOS/macOS
/[adehimnop]{4,7}\b(?:.*os ([\w]+) like mac|; opera)/i, // iOS
/(?:ios;fbsv|ios(?=.+ip(?:ad|hone))|ip(?:ad|hone)(?: |.+i(?:pad)?)os)[\/ ]([\w\.]+)/i,
/(?:ios;fbsv|ios(?=.+ip(?:ad|hone)|.+apple ?tv)|ip(?:ad|hone)(?: |.+i(?:pad)?)os|apple ?tv.+ios)[\/ ]([\w\.]+)/i,
/\btvos ?([\w\.]+)/i,
/cfnetwork\/.+darwin/i
], [[VERSION, /_/g, '.'], [NAME, 'iOS']], [
/(mac os x) ?([\w\. ]*)/i,
@@ -1142,25 +1143,25 @@
};
}
if (!NAVIGATOR_UADATA) {
IData.prototype.then = function (cb) {
var that = this;
var IDataResolve = function () {
for (var prop in that) {
if (that.hasOwnProperty(prop)) {
this[prop] = that[prop];
}
IData.prototype.then = function (cb) {
var that = this;
var IDataResolve = function () {
for (var prop in that) {
if (that.hasOwnProperty(prop)) {
this[prop] = that[prop];
}
};
IDataResolve.prototype = {
is : IData.prototype.is,
toString : IData.prototype.toString
};
var resolveData = new IDataResolve();
cb(resolveData);
return resolveData;
}
};
}
IDataResolve.prototype = {
is : IData.prototype.is,
toString : IData.prototype.toString,
withClientHints : IData.prototype.withClientHints,
withFeatureCheck : IData.prototype.withFeatureCheck
};
var resolveData = new IDataResolve();
cb(resolveData);
return resolveData;
};
return new IData();
};
@@ -1192,189 +1193,6 @@
}
function UAItem (itemType, ua, rgxMap, uaCH) {
this.get = function (prop) {
if (!prop) return this.data;
return this.data.hasOwnProperty(prop) ? this.data[prop] : undefined;
};
this.set = function (prop, val) {
this.data[prop] = val;
return this;
};
this.setCH = function (ch) {
this.uaCH = ch;
return this;
};
this.detectFeature = function () {
if (NAVIGATOR && NAVIGATOR.userAgent == this.ua) {
switch (this.itemType) {
case BROWSER:
// Brave-specific detection
if (NAVIGATOR.brave && typeof NAVIGATOR.brave.isBrave == TYPEOF.FUNCTION) {
this.set(NAME, 'Brave');
}
break;
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 !== TYPEOF.UNDEFINED && NAVIGATOR.maxTouchPoints && NAVIGATOR.maxTouchPoints > 2) {
this.set(MODEL, 'iPad')
.set(TYPE, TABLET);
}
break;
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 RESULT:
var data = this.data;
var detect = function (itemType) {
return data[itemType]
.getItem()
.detectFeature()
.get();
};
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 != RESULT) {
rgxMapper.call(this.data, this.ua, this.rgxMap);
}
switch (this.itemType) {
case BROWSER:
this.set(MAJOR, majorize(this.get(VERSION)));
break;
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
if (realVersion && parseInt(realVersion[1].substring(0,2), 10) >= 26) {
this.set(VERSION, realVersion[1]); // Set as iOS version
}
}
break;
}
return this;
};
this.parseCH = function () {
var uaCH = this.uaCH,
rgxMap = this.rgxMap;
switch (this.itemType) {
case BROWSER:
case ENGINE:
var brands = uaCH[FULLVERLIST] || uaCH[BRANDS], prevName;
if (brands) {
for (var i=0; i<brands.length; i++) {
var brandName = brands[i].brand || brands[i],
brandVersion = brands[i].version;
if (this.itemType == BROWSER &&
!/not.a.brand/i.test(brandName) &&
(!prevName ||
(/Chrom/.test(prevName) && brandName != CHROMIUM) ||
(prevName == EDGE && /WebView2/.test(brandName))
)) {
brandName = strMapper(brandName, browserHintsMap);
prevName = this.get(NAME);
if (!(prevName && !/Chrom/.test(prevName) && /Chrom/.test(brandName))) {
this.set(NAME, brandName)
.set(VERSION, brandVersion)
.set(MAJOR, majorize(brandVersion));
}
prevName = brandName;
}
if (this.itemType == ENGINE && brandName == CHROMIUM) {
this.set(VERSION, brandVersion);
}
}
}
break;
case CPU:
var archName = uaCH[ARCHITECTURE];
if (archName) {
if (archName && uaCH[BITNESS] == '64') archName += '64';
rgxMapper.call(this.data, archName + ';', rgxMap);
}
break;
case DEVICE:
if (uaCH[MOBILE]) {
this.set(TYPE, MOBILE);
}
if (uaCH[MODEL]) {
this.set(MODEL, uaCH[MODEL]);
if (!this.get(TYPE) || !this.get(VENDOR)) {
var reParse = {};
rgxMapper.call(reParse, 'droid 9; ' + uaCH[MODEL] + ')', rgxMap);
if (!this.get(TYPE) && !!reParse.type) {
this.set(TYPE, reParse.type);
}
if (!this.get(VENDOR) && !!reParse.vendor) {
this.set(VENDOR, reParse.vendor);
}
}
}
if (uaCH[FORMFACTORS]) {
var ff;
if (typeof uaCH[FORMFACTORS] !== 'string') {
var idx = 0;
while (!ff && idx < uaCH[FORMFACTORS].length) {
ff = strMapper(uaCH[FORMFACTORS][idx++], formFactorsMap);
}
} else {
ff = strMapper(uaCH[FORMFACTORS], formFactorsMap);
}
this.set(TYPE, ff);
}
break;
case OS:
var osName = uaCH[PLATFORM];
if(osName) {
var osVersion = uaCH[PLATFORMVER];
if (osName == WINDOWS) osVersion = (parseInt(majorize(osVersion), 10) >= 13 ? '11' : '10');
this.set(NAME, osName)
.set(VERSION, osVersion);
}
// Xbox-Specific Detection
if (this.get(NAME) == WINDOWS && uaCH[MODEL] == 'Xbox') {
this.set(NAME, 'Xbox')
.set(VERSION, undefined);
}
break;
case RESULT:
var data = this.data;
var parse = function (itemType) {
return data[itemType]
.getItem()
.setCH(uaCH)
.parseCH()
.get();
};
this.set(BROWSER, parse(BROWSER))
.set(CPU, parse(CPU))
.set(DEVICE, parse(DEVICE))
.set(ENGINE, parse(ENGINE))
.set(OS, parse(OS));
}
return this;
};
setProps.call(this, [
['itemType', itemType],
['ua', ua],
@@ -1382,10 +1200,191 @@
['rgxMap', rgxMap],
['data', createIData(this, itemType)]
]);
return this;
}
UAItem.prototype.get = function (prop) {
if (!prop) return this.data;
return this.data.hasOwnProperty(prop) ? this.data[prop] : undefined;
};
UAItem.prototype.set = function (prop, val) {
this.data[prop] = val;
return this;
};
UAItem.prototype.setCH = function (ch) {
this.uaCH = ch;
return this;
};
UAItem.prototype.detectFeature = function () {
if (NAVIGATOR && NAVIGATOR.userAgent == this.ua) {
switch (this.itemType) {
case BROWSER:
// Brave-specific detection
if (NAVIGATOR.brave && typeof NAVIGATOR.brave.isBrave == TYPEOF.FUNCTION) {
this.set(NAME, 'Brave');
}
break;
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 !== TYPEOF.UNDEFINED && NAVIGATOR.maxTouchPoints && NAVIGATOR.maxTouchPoints > 2) {
this.set(MODEL, 'iPad')
.set(TYPE, TABLET);
}
break;
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 RESULT:
var data = this.data;
var detect = function (itemType) {
return data[itemType]
.getItem()
.detectFeature()
.get();
};
this.set(BROWSER, detect(BROWSER))
.set(CPU, detect(CPU))
.set(DEVICE, detect(DEVICE))
.set(ENGINE, detect(ENGINE))
.set(OS, detect(OS));
}
}
return this;
};
UAItem.prototype.parseUA = function () {
if (this.itemType != RESULT) {
rgxMapper.call(this.data, this.ua, this.rgxMap);
}
switch (this.itemType) {
case BROWSER:
this.set(MAJOR, majorize(this.get(VERSION)));
break;
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
if (realVersion && parseInt(realVersion[1].substring(0,2), 10) >= 26) {
this.set(VERSION, realVersion[1]); // Set as iOS version
}
}
break;
}
return this;
};
UAItem.prototype.parseCH = function () {
var uaCH = this.uaCH,
rgxMap = this.rgxMap;
switch (this.itemType) {
case BROWSER:
case ENGINE:
var brands = uaCH[FULLVERLIST] || uaCH[BRANDS], prevName;
if (brands) {
for (var i=0; i<brands.length; i++) {
var brandName = brands[i].brand || brands[i],
brandVersion = brands[i].version;
if (this.itemType == BROWSER &&
!/not.a.brand/i.test(brandName) &&
(!prevName ||
(/Chrom/.test(prevName) && brandName != CHROMIUM) ||
(prevName == EDGE && /WebView2/.test(brandName))
)) {
brandName = strMapper(brandName, browserHintsMap);
prevName = this.get(NAME);
if (!(prevName && !/Chrom/.test(prevName) && /Chrom/.test(brandName))) {
this.set(NAME, brandName)
.set(VERSION, brandVersion)
.set(MAJOR, majorize(brandVersion));
}
prevName = brandName;
}
if (this.itemType == ENGINE && brandName == CHROMIUM) {
this.set(VERSION, brandVersion);
}
}
}
break;
case CPU:
var archName = uaCH[ARCHITECTURE];
if (archName) {
if (archName && uaCH[BITNESS] == '64') archName += '64';
rgxMapper.call(this.data, archName + ';', rgxMap);
}
break;
case DEVICE:
if (uaCH[MOBILE]) {
this.set(TYPE, MOBILE);
}
if (uaCH[MODEL]) {
this.set(MODEL, uaCH[MODEL]);
if (!this.get(TYPE) || !this.get(VENDOR)) {
var reParse = {};
rgxMapper.call(reParse, 'droid 9; ' + uaCH[MODEL] + ')', rgxMap);
if (!this.get(TYPE) && !!reParse.type) {
this.set(TYPE, reParse.type);
}
if (!this.get(VENDOR) && !!reParse.vendor) {
this.set(VENDOR, reParse.vendor);
}
}
}
if (uaCH[FORMFACTORS]) {
var ff;
if (typeof uaCH[FORMFACTORS] !== 'string') {
var idx = 0;
while (!ff && idx < uaCH[FORMFACTORS].length) {
ff = strMapper(uaCH[FORMFACTORS][idx++], formFactorsMap);
}
} else {
ff = strMapper(uaCH[FORMFACTORS], formFactorsMap);
}
this.set(TYPE, ff);
}
break;
case OS:
var osName = uaCH[PLATFORM];
if(osName) {
var osVersion = uaCH[PLATFORMVER];
if (osName == WINDOWS) osVersion = (parseInt(majorize(osVersion), 10) >= 13 ? '11' : '10');
this.set(NAME, osName)
.set(VERSION, osVersion);
}
// Xbox-Specific Detection
if (this.get(NAME) == WINDOWS && uaCH[MODEL] == 'Xbox') {
this.set(NAME, 'Xbox')
.set(VERSION, undefined);
}
break;
case RESULT:
var data = this.data;
var parse = function (itemType) {
return data[itemType]
.getItem()
.setCH(uaCH)
.parseCH()
.get();
};
this.set(BROWSER, parse(BROWSER))
.set(CPU, parse(CPU))
.set(DEVICE, parse(DEVICE))
.set(ENGINE, parse(ENGINE))
.set(OS, parse(OS));
}
return this;
};
function UAParser (ua, extensions, headers) {
if (typeof ua === TYPEOF.OBJECT) {

View File

@@ -3,7 +3,7 @@
// Source: /src/main/ua-parser.js
/////////////////////////////////////////////////////////////////////////////////
/* UAParser.js v2.0.6
/* UAParser.js v2.0.7
Copyright © 2012-2025 Faisal Salman <f@faisalman.com>
AGPLv3 License *//*
Detect Browser, Engine, OS, CPU, and Device type/model from User-Agent data.
@@ -21,7 +21,7 @@
// Constants
/////////////
var LIBVERSION = '2.0.6',
var LIBVERSION = '2.0.7',
UA_MAX_LENGTH = 500,
USER_AGENT = 'user-agent',
EMPTY = '',
@@ -378,8 +378,8 @@
/(?: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/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
/(atlas|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,
// Atlas/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
/(ecosia|weibo)(?:__| \w+@)([\d\.]+)/i // Ecosia/Weibo
@@ -434,7 +434,7 @@
/(tesla)(?: qtcarbrowser|\/(20\d\d\.[-\w\.]+))/i, // Tesla
/m?(qqbrowser|2345(?=browser|chrome|explorer))\w*[\/ ]?v?([\w\.]+)/i // QQ/2345
], [NAME, VERSION], [
/(lbbrowser|rekonq)/i // LieBao Browser/Rekonq
/(lbbrowser|rekonq|steam(?= (clie|tenf|gameo)))/i // LieBao Browser/Rekonq/Steam
], [NAME], [
/ome\/([\w\.]+) \w* ?(iron) saf/i, // Iron
/ome\/([\w\.]+).+qihu (360)[es]e/i // 360
@@ -600,12 +600,12 @@
/\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
/\b; (\w+) build\/hm\1/i, // Xiaomi Hongmi 'numeric' models
/\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|max|cc)?[_ ]?(?:\d{0,2}\w?)[_ ]?(?:plus|se|lite|pro)?( 5g|lte)?)(?: bui|\))/i, // Xiaomi Mi
// Xiaomi Redmi / POCO / Black Shark / Qin
/oid[^\)]+; (redmi[\-_ ]?(?:note|k)?[\w_ ]+|m?[12]\d[01]\d\w{3,6}|poco[\w ]+|(shark )?\w{3}-[ah]0|qin ?[1-3](s\+|ultra| pro)?)( bui|; wv|\))/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,
/ ([\w ]+) miui\/v?\d/i
], [[MODEL, /_/g, ' '], [VENDOR, XIAOMI], [TYPE, MOBILE]], [
@@ -854,10 +854,10 @@
], [MODEL, [VENDOR, MICROSOFT], [TYPE, CONSOLE]], [
/(ouya)/i, // Ouya
/(nintendo) (\w+)/i, // Nintendo
/(retroid) (pocket ([^\)]+))/i // Retroid Pocket
], [VENDOR, MODEL, [TYPE, CONSOLE]], [
/droid.+; (shield)( bui|\))/i // Nvidia Portable
], [MODEL, [VENDOR, NVIDIA], [TYPE, CONSOLE]], [
/(retroid) (pocket ([^\)]+))/i, // Retroid Pocket
/(valve).+(steam deck)/i,
/droid.+; ((shield|rgcube|gr0006))( bui|\))/i // Nvidia Portable/Anbernic/Logitech
], [[VENDOR, strMapper, { 'Nvidia': 'Shield', 'Anbernic': 'RGCUBE', 'Logitech': 'GR0006' }], MODEL, [TYPE, CONSOLE]], [
///////////////////
// WEARABLES
@@ -866,7 +866,7 @@
/\b(sm-[lr]\d\d[0156][fnuw]?s?|gear live)\b/i // Samsung Galaxy Watch
], [MODEL, [VENDOR, SAMSUNG], [TYPE, WEARABLE]], [
/((pebble))app/i, // Pebble
/(asus|google|lg|oppo) ((pixel |zen)?watch[\w ]*)( bui|\))/i // Asus ZenWatch / LG Watch / Pixel Watch
/(asus|google|lg|oppo|xiaomi) ((pixel |zen)?watch[\w ]*)( bui|\))/i // Asus ZenWatch / LG Watch / Pixel Watch / Xiaomi Watch
], [VENDOR, MODEL, [TYPE, WEARABLE]], [
/(ow(?:19|20)?we?[1-3]{1,3})/i // Oppo Watch
], [MODEL, [VENDOR, OPPO], [TYPE, WEARABLE]], [
@@ -970,7 +970,8 @@
// iOS/macOS
/[adehimnop]{4,7}\b(?:.*os ([\w]+) like mac|; opera)/i, // iOS
/(?:ios;fbsv|ios(?=.+ip(?:ad|hone))|ip(?:ad|hone)(?: |.+i(?:pad)?)os)[\/ ]([\w\.]+)/i,
/(?:ios;fbsv|ios(?=.+ip(?:ad|hone)|.+apple ?tv)|ip(?:ad|hone)(?: |.+i(?:pad)?)os|apple ?tv.+ios)[\/ ]([\w\.]+)/i,
/\btvos ?([\w\.]+)/i,
/cfnetwork\/.+darwin/i
], [[VERSION, /_/g, '.'], [NAME, 'iOS']], [
/(mac os x) ?([\w\. ]*)/i,
@@ -1144,25 +1145,25 @@
};
}
if (!NAVIGATOR_UADATA) {
IData.prototype.then = function (cb) {
var that = this;
var IDataResolve = function () {
for (var prop in that) {
if (that.hasOwnProperty(prop)) {
this[prop] = that[prop];
}
IData.prototype.then = function (cb) {
var that = this;
var IDataResolve = function () {
for (var prop in that) {
if (that.hasOwnProperty(prop)) {
this[prop] = that[prop];
}
};
IDataResolve.prototype = {
is : IData.prototype.is,
toString : IData.prototype.toString
};
var resolveData = new IDataResolve();
cb(resolveData);
return resolveData;
}
};
}
IDataResolve.prototype = {
is : IData.prototype.is,
toString : IData.prototype.toString,
withClientHints : IData.prototype.withClientHints,
withFeatureCheck : IData.prototype.withFeatureCheck
};
var resolveData = new IDataResolve();
cb(resolveData);
return resolveData;
};
return new IData();
};
@@ -1194,189 +1195,6 @@
}
function UAItem (itemType, ua, rgxMap, uaCH) {
this.get = function (prop) {
if (!prop) return this.data;
return this.data.hasOwnProperty(prop) ? this.data[prop] : undefined;
};
this.set = function (prop, val) {
this.data[prop] = val;
return this;
};
this.setCH = function (ch) {
this.uaCH = ch;
return this;
};
this.detectFeature = function () {
if (NAVIGATOR && NAVIGATOR.userAgent == this.ua) {
switch (this.itemType) {
case BROWSER:
// Brave-specific detection
if (NAVIGATOR.brave && typeof NAVIGATOR.brave.isBrave == TYPEOF.FUNCTION) {
this.set(NAME, 'Brave');
}
break;
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 !== TYPEOF.UNDEFINED && NAVIGATOR.maxTouchPoints && NAVIGATOR.maxTouchPoints > 2) {
this.set(MODEL, 'iPad')
.set(TYPE, TABLET);
}
break;
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 RESULT:
var data = this.data;
var detect = function (itemType) {
return data[itemType]
.getItem()
.detectFeature()
.get();
};
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 != RESULT) {
rgxMapper.call(this.data, this.ua, this.rgxMap);
}
switch (this.itemType) {
case BROWSER:
this.set(MAJOR, majorize(this.get(VERSION)));
break;
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
if (realVersion && parseInt(realVersion[1].substring(0,2), 10) >= 26) {
this.set(VERSION, realVersion[1]); // Set as iOS version
}
}
break;
}
return this;
};
this.parseCH = function () {
var uaCH = this.uaCH,
rgxMap = this.rgxMap;
switch (this.itemType) {
case BROWSER:
case ENGINE:
var brands = uaCH[FULLVERLIST] || uaCH[BRANDS], prevName;
if (brands) {
for (var i=0; i<brands.length; i++) {
var brandName = brands[i].brand || brands[i],
brandVersion = brands[i].version;
if (this.itemType == BROWSER &&
!/not.a.brand/i.test(brandName) &&
(!prevName ||
(/Chrom/.test(prevName) && brandName != CHROMIUM) ||
(prevName == EDGE && /WebView2/.test(brandName))
)) {
brandName = strMapper(brandName, browserHintsMap);
prevName = this.get(NAME);
if (!(prevName && !/Chrom/.test(prevName) && /Chrom/.test(brandName))) {
this.set(NAME, brandName)
.set(VERSION, brandVersion)
.set(MAJOR, majorize(brandVersion));
}
prevName = brandName;
}
if (this.itemType == ENGINE && brandName == CHROMIUM) {
this.set(VERSION, brandVersion);
}
}
}
break;
case CPU:
var archName = uaCH[ARCHITECTURE];
if (archName) {
if (archName && uaCH[BITNESS] == '64') archName += '64';
rgxMapper.call(this.data, archName + ';', rgxMap);
}
break;
case DEVICE:
if (uaCH[MOBILE]) {
this.set(TYPE, MOBILE);
}
if (uaCH[MODEL]) {
this.set(MODEL, uaCH[MODEL]);
if (!this.get(TYPE) || !this.get(VENDOR)) {
var reParse = {};
rgxMapper.call(reParse, 'droid 9; ' + uaCH[MODEL] + ')', rgxMap);
if (!this.get(TYPE) && !!reParse.type) {
this.set(TYPE, reParse.type);
}
if (!this.get(VENDOR) && !!reParse.vendor) {
this.set(VENDOR, reParse.vendor);
}
}
}
if (uaCH[FORMFACTORS]) {
var ff;
if (typeof uaCH[FORMFACTORS] !== 'string') {
var idx = 0;
while (!ff && idx < uaCH[FORMFACTORS].length) {
ff = strMapper(uaCH[FORMFACTORS][idx++], formFactorsMap);
}
} else {
ff = strMapper(uaCH[FORMFACTORS], formFactorsMap);
}
this.set(TYPE, ff);
}
break;
case OS:
var osName = uaCH[PLATFORM];
if(osName) {
var osVersion = uaCH[PLATFORMVER];
if (osName == WINDOWS) osVersion = (parseInt(majorize(osVersion), 10) >= 13 ? '11' : '10');
this.set(NAME, osName)
.set(VERSION, osVersion);
}
// Xbox-Specific Detection
if (this.get(NAME) == WINDOWS && uaCH[MODEL] == 'Xbox') {
this.set(NAME, 'Xbox')
.set(VERSION, undefined);
}
break;
case RESULT:
var data = this.data;
var parse = function (itemType) {
return data[itemType]
.getItem()
.setCH(uaCH)
.parseCH()
.get();
};
this.set(BROWSER, parse(BROWSER))
.set(CPU, parse(CPU))
.set(DEVICE, parse(DEVICE))
.set(ENGINE, parse(ENGINE))
.set(OS, parse(OS));
}
return this;
};
setProps.call(this, [
['itemType', itemType],
['ua', ua],
@@ -1384,10 +1202,191 @@
['rgxMap', rgxMap],
['data', createIData(this, itemType)]
]);
return this;
}
UAItem.prototype.get = function (prop) {
if (!prop) return this.data;
return this.data.hasOwnProperty(prop) ? this.data[prop] : undefined;
};
UAItem.prototype.set = function (prop, val) {
this.data[prop] = val;
return this;
};
UAItem.prototype.setCH = function (ch) {
this.uaCH = ch;
return this;
};
UAItem.prototype.detectFeature = function () {
if (NAVIGATOR && NAVIGATOR.userAgent == this.ua) {
switch (this.itemType) {
case BROWSER:
// Brave-specific detection
if (NAVIGATOR.brave && typeof NAVIGATOR.brave.isBrave == TYPEOF.FUNCTION) {
this.set(NAME, 'Brave');
}
break;
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 !== TYPEOF.UNDEFINED && NAVIGATOR.maxTouchPoints && NAVIGATOR.maxTouchPoints > 2) {
this.set(MODEL, 'iPad')
.set(TYPE, TABLET);
}
break;
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 RESULT:
var data = this.data;
var detect = function (itemType) {
return data[itemType]
.getItem()
.detectFeature()
.get();
};
this.set(BROWSER, detect(BROWSER))
.set(CPU, detect(CPU))
.set(DEVICE, detect(DEVICE))
.set(ENGINE, detect(ENGINE))
.set(OS, detect(OS));
}
}
return this;
};
UAItem.prototype.parseUA = function () {
if (this.itemType != RESULT) {
rgxMapper.call(this.data, this.ua, this.rgxMap);
}
switch (this.itemType) {
case BROWSER:
this.set(MAJOR, majorize(this.get(VERSION)));
break;
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
if (realVersion && parseInt(realVersion[1].substring(0,2), 10) >= 26) {
this.set(VERSION, realVersion[1]); // Set as iOS version
}
}
break;
}
return this;
};
UAItem.prototype.parseCH = function () {
var uaCH = this.uaCH,
rgxMap = this.rgxMap;
switch (this.itemType) {
case BROWSER:
case ENGINE:
var brands = uaCH[FULLVERLIST] || uaCH[BRANDS], prevName;
if (brands) {
for (var i=0; i<brands.length; i++) {
var brandName = brands[i].brand || brands[i],
brandVersion = brands[i].version;
if (this.itemType == BROWSER &&
!/not.a.brand/i.test(brandName) &&
(!prevName ||
(/Chrom/.test(prevName) && brandName != CHROMIUM) ||
(prevName == EDGE && /WebView2/.test(brandName))
)) {
brandName = strMapper(brandName, browserHintsMap);
prevName = this.get(NAME);
if (!(prevName && !/Chrom/.test(prevName) && /Chrom/.test(brandName))) {
this.set(NAME, brandName)
.set(VERSION, brandVersion)
.set(MAJOR, majorize(brandVersion));
}
prevName = brandName;
}
if (this.itemType == ENGINE && brandName == CHROMIUM) {
this.set(VERSION, brandVersion);
}
}
}
break;
case CPU:
var archName = uaCH[ARCHITECTURE];
if (archName) {
if (archName && uaCH[BITNESS] == '64') archName += '64';
rgxMapper.call(this.data, archName + ';', rgxMap);
}
break;
case DEVICE:
if (uaCH[MOBILE]) {
this.set(TYPE, MOBILE);
}
if (uaCH[MODEL]) {
this.set(MODEL, uaCH[MODEL]);
if (!this.get(TYPE) || !this.get(VENDOR)) {
var reParse = {};
rgxMapper.call(reParse, 'droid 9; ' + uaCH[MODEL] + ')', rgxMap);
if (!this.get(TYPE) && !!reParse.type) {
this.set(TYPE, reParse.type);
}
if (!this.get(VENDOR) && !!reParse.vendor) {
this.set(VENDOR, reParse.vendor);
}
}
}
if (uaCH[FORMFACTORS]) {
var ff;
if (typeof uaCH[FORMFACTORS] !== 'string') {
var idx = 0;
while (!ff && idx < uaCH[FORMFACTORS].length) {
ff = strMapper(uaCH[FORMFACTORS][idx++], formFactorsMap);
}
} else {
ff = strMapper(uaCH[FORMFACTORS], formFactorsMap);
}
this.set(TYPE, ff);
}
break;
case OS:
var osName = uaCH[PLATFORM];
if(osName) {
var osVersion = uaCH[PLATFORMVER];
if (osName == WINDOWS) osVersion = (parseInt(majorize(osVersion), 10) >= 13 ? '11' : '10');
this.set(NAME, osName)
.set(VERSION, osVersion);
}
// Xbox-Specific Detection
if (this.get(NAME) == WINDOWS && uaCH[MODEL] == 'Xbox') {
this.set(NAME, 'Xbox')
.set(VERSION, undefined);
}
break;
case RESULT:
var data = this.data;
var parse = function (itemType) {
return data[itemType]
.getItem()
.setCH(uaCH)
.parseCH()
.get();
};
this.set(BROWSER, parse(BROWSER))
.set(CPU, parse(CPU))
.set(DEVICE, parse(DEVICE))
.set(ENGINE, parse(ENGINE))
.set(OS, parse(OS));
}
return this;
};
function UAParser (ua, extensions, headers) {
if (typeof ua === TYPEOF.OBJECT) {

View File

@@ -171,6 +171,16 @@
"major" : "0"
}
},
{
"desc" : "Atlas",
"ua" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Atlas/1.0",
"expect" :
{
"name" : "Atlas",
"version" : "1.0",
"major" : "1"
}
},
{
"desc" : "Avant",
"ua" : "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; GTB5; Avant Browser; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",
@@ -1949,6 +1959,36 @@
"major" : "20"
}
},
{
"desc" : "Steam Client",
"ua" : "Mozilla/5.0 (X11; Linux x86_64; Valve Steam Client/default/1705108172) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36",
"expect" :
{
"name" : "Steam",
"version" : "undefined",
"major" : "undefined"
}
},
{
"desc" : "Steam Big Picture",
"ua" : "Mozilla/5.0 (Linux; U; X11; en-US; Valve Steam Tenfoot/1660688177; ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36",
"expect" :
{
"name" : "Steam",
"version" : "undefined",
"major" : "undefined"
}
},
{
"desc" : "Steam Overlay",
"ua" : "Mozilla/5.0 (Windows NT 10.0; Win64; x64; Valve Steam GameOverlay/default/1741737356) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.183 Safari/537.36",
"expect" :
{
"name" : "Steam",
"version" : "undefined",
"major" : "undefined"
}
},
{
"desc" : "Swiftfox",
"ua" : "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1) Gecko/20061024 Firefox/2.0 (Swiftfox)",

View File

@@ -0,0 +1,11 @@
[
{
"desc": "Anbernic",
"ua": "Mozilla/5.0 (Linux; Android 13; RGCUBE) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36",
"expect": {
"vendor": "Anbernic",
"model": "RGCUBE",
"type": "console"
}
}
]

View File

@@ -0,0 +1,11 @@
[
{
"desc": "Logitech",
"ua": "Dalvik/2.1.0 (Linux; U; Android 11; GR0006 Build/SQ02K.00.0018)",
"expect": {
"vendor": "Logitech",
"model": "GR0006",
"type": "console"
}
}
]

View File

@@ -0,0 +1,11 @@
[
{
"desc": "Valve",
"ua": "Mozilla/5.0 (X11; Linux x86_64; Valve Steam Client/Steam Deck [Steam Deck Beta]/default/0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.183 Safari/537.36",
"expect": {
"vendor": "Valve",
"model": "Steam Deck",
"type": "console"
}
}
]

View File

@@ -71,6 +71,51 @@
"type": "mobile"
}
},
{
"desc": "XiaoMi Black Shark 3",
"ua": "Mozilla/5.0 (Linux; U; Android 11; zh-CN; SHARK KLE-A0 Build/KLEN2211210CN00MR6) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.58 UCBrowser/17.3.8.1369 Mobile Safari/537.36",
"expect": {
"vendor": "Xiaomi",
"model": "SHARK KLE-A0",
"type": "mobile"
}
},
{
"desc": "XiaoMi Black Shark 4",
"ua": "Mozilla/5.0 (Linux; U; Android 11; en-US; SHARK PRS-H0 Build/PROS2203060OS00MP5) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/78.0.3904.108 UCBrowser/13.4.0.1306 Mobile Safari/537.36",
"expect": {
"vendor": "Xiaomi",
"model": "SHARK PRS-H0",
"type": "mobile"
}
},
{
"desc": "XiaoMi Black Shark 4 Pro",
"ua": "Mozilla/5.0 (Linux; U; Android 11; es-us; SHARK KSR-H0 Build/KASE2208050OS00MP4) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Mobile Safari/537.36 XiaoMi/MiuiBrowser/13.22.1-gn",
"expect": {
"vendor": "Xiaomi",
"model": "SHARK KSR-H0",
"type": "mobile"
}
},
{
"desc": "XiaoMi Black Shark 5",
"ua": "Mozilla/5.0 (Linux; arm_64; Android 12; SHARK PAR-H0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 YaBrowser/23.5.0.256.00 SA/3 Mobile Safari/537.36",
"expect": {
"vendor": "Xiaomi",
"model": "SHARK PAR-H0",
"type": "mobile"
}
},
{
"desc": "XiaoMi Civi 5 Pro",
"ua": "Mozilla/5.0 (Linux; U; Android 15; zh-CN; 25067PYE3C Build/AQ3A.250129.001) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.58 UCBrowser/18.0.4.1430 Mobile Safari/537.36",
"expect": {
"vendor": "Xiaomi",
"model": "25067PYE3C",
"type": "mobile"
}
},
{
"desc": "Xiaomi Hongmi Note 1W",
"ua": "Mozilla/5.0 (Linux; U; Android 4.2.2; zh-CN; HM NOTE 1W Build/JDQ39) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 UCBrowser/9.7.9.439 U3/0.8.0 Mobile Safari/533.1",
@@ -413,6 +458,42 @@
"type": "mobile"
}
},
{
"desc": "Xiaomi Qin 1s+",
"ua": "Mozilla/5.0 (Linux; U; Android 4.4.4; Qin 1s+ Build/KTU84P; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/33.0.0.0 Mobile Safari/537.36 OPR/46.0.2254.145391",
"expect": {
"vendor": "Xiaomi",
"model": "Qin 1s+",
"type": "mobile"
}
},
{
"desc": "Xiaomi Qin 2",
"ua": "Mozilla/5.0 (Linux; Android 9; Qin 2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.162 Mobile Safari/537.36",
"expect": {
"vendor": "Xiaomi",
"model": "Qin 2",
"type": "mobile"
}
},
{
"desc": "Xiaomi Qin 2 Pro",
"ua": "Mozilla/5.0 (Linux; Android 9.0; QIN2 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4215.0 Mobile Safari/537.36 EdgA/86.0.597.0",
"expect": {
"vendor": "Xiaomi",
"model": "QIN2 Pro",
"type": "mobile"
}
},
{
"desc": "Xiaomi Qin 3 Ultra",
"ua": "Mozilla/5.0 (Linux; Android 12; QIN3ULTRA Build/SP1A.210812.016; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.120 Mobile Safari/537.36",
"expect": {
"vendor": "Xiaomi",
"model": "QIN3ULTRA",
"type": "mobile"
}
},
{
"desc": "Xiaomi Redmi 4A",
"ua": "Mozilla/5.0 (Linux; Android 6.0; Redmi 4A Build/MMB29M; xx-xx) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/56.0.2924.87 Mobile Safari/537.36",
@@ -431,6 +512,15 @@
"type": "mobile"
}
},
{
"desc": "Xiaomi Redmi 15",
"ua": "Mozilla/5.0 (Linux; Android 15; 25062RN2DA Build/AQ3A.250226.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.212 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/541.0.0.49.79;IABMV/1;]",
"expect": {
"vendor": "Xiaomi",
"model": "25062RN2DA",
"type": "mobile"
}
},
{
"desc": "Xiaomi Redmi K30 5G",
"ua": "Mozilla/5.0 (Linux; Android 10; Redmi K30 5G) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.96 Mobile Safari/537.36",
@@ -601,5 +691,14 @@
"model": "MIBOX3",
"type": "smarttv"
}
},
{
"desc": "Xiaomi Watch 2 Pro",
"ua": "Dalvik/2.1.0 (Linux; U; Android 13; Xiaomi Watch 2 Pro Build/TWR7.231031.001.XM064S)",
"expect": {
"vendor": "Xiaomi",
"model": "Watch 2 Pro",
"type": "wearable"
}
}
]

View File

@@ -289,6 +289,16 @@
"type" : "fetcher"
}
},
{
"desc" : "NovaAct",
"ua" : "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Agent-NovaAct/0.9",
"expect" :
{
"name" : "NovaAct",
"version" : "0.9",
"type" : "fetcher"
}
},
{
"desc" : "Perplexity-User",
"ua" : "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Perplexity-User/1.0; +https://perplexity.ai/perplexity-user)",

View File

@@ -39,6 +39,36 @@
"type" : "library"
}
},
{
"desc" : "Bun",
"ua" : "Bun/1.0.6",
"expect" :
{
"name" : "Bun",
"version" : "1.0.6",
"type" : "library"
}
},
{
"desc" : "Dart",
"ua" : "Dart/3.5 (dart:io)",
"expect" :
{
"name" : "Dart",
"version" : "3.5",
"type" : "library"
}
},
{
"desc" : "Deno",
"ua" : "Deno/2.1.7",
"expect" :
{
"name" : "Deno",
"version" : "2.1.7",
"type" : "library"
}
},
{
"desc" : "go-http-client",
"ua" : "go-http-client/1.1",
@@ -59,6 +89,16 @@
"type" : "library"
}
},
{
"desc" : "hackney",
"ua" : "hackney/1.20.1",
"expect" :
{
"name" : "hackney",
"version" : "1.20.1",
"type" : "library"
}
},
{
"desc" : "GuzzleHttp",
"ua" : "GuzzleHttp/6.5.5 curl/7.70.0 PHP/7.4.22",
@@ -129,6 +169,26 @@
"type" : "library"
}
},
{
"desc" : "Node.js",
"ua" : "Node.js/22",
"expect" :
{
"name" : "Node.js",
"version" : "22",
"type" : "library"
}
},
{
"desc" : "node-fetch",
"ua" : "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)",
"expect" :
{
"name" : "node-fetch",
"version" : "1.0",
"type" : "library"
}
},
{
"desc" : "Nutch",
"ua" : "AliyunSecBot/Nutch-1.21-SNAPSHOT",
@@ -149,16 +209,6 @@
"type" : "library"
}
},
{
"desc" : "node-fetch",
"ua" : "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)",
"expect" :
{
"name" : "node-fetch",
"version" : "1.0",
"type" : "library"
}
},
{
"desc" : "PHP-SOAP",
"ua" : "PHP-SOAP/7.4.33",
@@ -219,6 +269,16 @@
"type" : "library"
}
},
{
"desc" : "rest-client",
"ua" : "rest-client/2.1.0 (linux-gnu x86_64) ruby/2.7.2p137",
"expect" :
{
"name" : "rest-client",
"version" : "2.1.0",
"type" : "library"
}
},
{
"desc" : "Scrapy",
"ua" : "Scrapy/1.5.0 (+https://scrapy.org)",
@@ -238,5 +298,15 @@
"version" : "5.0.2",
"type" : "library"
}
},
{
"desc" : "undici",
"ua" : "undici",
"expect" :
{
"name" : "undici",
"version" : "undefined",
"type" : "library"
}
}
]

View File

@@ -142,5 +142,41 @@
"name" : "iOS",
"version" : "11.2.5"
}
},
{
"desc": "iOS",
"ua": "iPlayTV/3.3.9 (Apple TV; iOS 16.1; Scale/1.00)",
"expect" :
{
"name" : "iOS",
"version" : "16.1"
}
},
{
"desc": "iOS",
"ua": "itunesstored/1.0 iOS/8.4.4 AppleTV/7.8 model/AppleTV3,2 build/12H937 (3; dt:12)",
"expect" :
{
"name" : "iOS",
"version" : "8.4.4"
}
},
{
"desc": "tvOS",
"ua": "iMPlayer/1.6.1 (tvOS 26.0.1)",
"expect" :
{
"name" : "iOS",
"version" : "26.0.1"
}
},
{
"desc": "tvOS",
"ua": "otg/1.5.1 (AppleTv Apple TV 4; tvOS16.2; appletv.client) libcurl/7.58.0 OpenSSL/1.0.2o zlib/1.2.11 clib/1.8.56",
"expect" :
{
"name" : "iOS",
"version" : "16.2"
}
}
]

View File

@@ -152,3 +152,45 @@ test.describe('request.headers can be passed in form of a Headers object', () =>
expect(uap.ua).toBe('myBrowser/1.0');
});
});
test.describe('Chaining withFeatureCheck() & withClientHints() in client-side development', () => {
test('Chain', async ({ page, browserName }) => {
await page.addInitScript((browserName) => {
Object.defineProperty(navigator, 'userAgent', {
value: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15'
});
Object.defineProperty(navigator, 'standalone', {
value: true
});
Object.defineProperty(navigator, 'maxTouchPoints', {
value: 3
});
if (browserName == 'chromium') {
Object.defineProperty(navigator, 'userAgentData', {
value: {
brands: [],
platform: '',
mobile: false,
getHighEntropyValues: () => {
return Promise.resolve({
formFactors: 'VR'
});
}
}
});
}
}, browserName);
await page.goto(localHtml);
// @ts-ignore
const fc2ch = await page.evaluate(async () => await UAParser().withFeatureCheck().then(res => res.withClientHints()));
const ch2fc = await page.evaluate(async () => await UAParser().withClientHints().then(res => res.withFeatureCheck()));
if (browserName == 'chromium') {
expect(fc2ch).toHaveProperty('device.type', 'xr'); // overwrite by client hints
expect(ch2fc).toHaveProperty('device.type', 'tablet'); // overwrite by feature check
} else {
expect(fc2ch).toHaveProperty('device.type', 'tablet'); // no client hints found
expect(ch2fc).toHaveProperty('device.type', 'tablet');
}
});
});

View File

@@ -1,85 +0,0 @@
const assert = require('assert');
const { UAParser } = require('../../src/main/ua-parser');
const { getDeviceVendor, isAppleSilicon, isAIBot, isBot, isChromeFamily } = require('../../src/helpers/ua-parser-helpers');
const { Bots, Emails } = require('../../src/extensions/ua-parser-extensions');
const { DeviceVendor } = require('../../src/enums/ua-parser-enums');
describe('getDeviceVendor', () => {
it('Can guess the device vendor from a model name', () => {
const modelSM = 'SM-A605G';
const modelRedmi = 'Redmi Note 8';
const modelNexus = 'Nexus 6P';
const modelAquos = 'AQUOS-TVX19B';
assert.equal(getDeviceVendor(modelSM), DeviceVendor.SAMSUNG);
assert.equal(getDeviceVendor(modelRedmi), DeviceVendor.XIAOMI);
assert.equal(getDeviceVendor(modelNexus), DeviceVendor.HUAWEI);
assert.equal(getDeviceVendor(modelAquos), DeviceVendor.SHARP);
});
});
describe('isAppleSilicon', () => {
it('Can detect Apple Silicon device', () => {
// non-real ua
const macARM = 'Mozilla/5.0 (Macintosh; ARM; Mac OS X 10.15; rv:97.0) Gecko/20100101 Firefox/97.0';
const macIntel = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:97.0) Gecko/20100101 Firefox/97.0';
assert.equal(isAppleSilicon(UAParser(macIntel)), false);
assert.equal(isAppleSilicon(macIntel), false);
assert.equal(isAppleSilicon(UAParser(macARM)), true);
assert.equal(isAppleSilicon(macARM), true);
});
});
describe('isAIBot', () => {
it('Can detect AI Bots', () => {
const claudeBot = 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; ClaudeBot/1.0; +claudebot@anthropic.com)';
const firefox = 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/111.0';
const searchGPT = 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; OAI-SearchBot/1.0; +https://openai.com/searchbot';
const semrushAI = 'Mozilla/5.0 (compatible; SemrushBot-OCOB/1; +https://www.semrush.com/bot/)';
assert.equal(isAIBot(UAParser(claudeBot, Bots)), true);
assert.equal(isAIBot(claudeBot), true);
assert.equal(isAIBot(firefox), false);
assert.equal(isAIBot(searchGPT), true);
assert.equal(isAIBot(semrushAI), true);
});
});
describe('isBot', () => {
it('Can detect Bots', () => {
// non-real ua
const ahrefsBot = 'Mozilla/5.0 (compatible; AhrefsBot/7.0; +http://ahrefs.com/robot/)';
const firefox = 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/111.0';
const scrapy = 'Scrapy/1.5.0 (+https://scrapy.org)';
const thunderbird = 'Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Thunderbird/78.13.0';
const botParser = new UAParser(firefox, { Bots, Emails });
assert.equal(isBot(botParser.getResult()), false);
assert.equal(isBot(botParser.setUA(ahrefsBot).getResult()), true);
assert.equal(isBot(botParser.setUA(scrapy).getResult()), true);
assert.equal(isBot(botParser.setUA(thunderbird).getResult()), false);
assert.equal(isBot(ahrefsBot), true);
assert.equal(isBot(firefox), false);
assert.equal(isBot(scrapy), true);
assert.equal(isBot(thunderbird), false);
});
});
describe('isChromeFamily', () => {
it('Can detect Chromium-based browser', () => {
const edge = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.2151.58';
const firefox = 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/111.0';
assert.equal(isChromeFamily(UAParser(edge)), true);
assert.equal(isChromeFamily(UAParser(firefox)), false);
assert.equal(isChromeFamily(edge), true);
assert.equal(isChromeFamily(firefox), false);
});
});

View File

@@ -0,0 +1,42 @@
const assert = require('assert');
const { UAParser } = require('../../../src/main/ua-parser');
const { isAICrawler, isBot } = require('../../../src/bot-detection/bot-detection');
const { Bots, Emails } = require('../../../src/extensions/ua-parser-extensions');
describe('isAICrawler()', () => {
it('Can detect AI Crawlers', () => {
// AI Crawlers
const claudeBot = 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; ClaudeBot/1.0; +claudebot@anthropic.com)';
const searchGPT = 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; OAI-SearchBot/1.0; +https://openai.com/searchbot';
const semrushAI = 'Mozilla/5.0 (compatible; SemrushBot-OCOB/1; +https://www.semrush.com/bot/)';
assert.equal(isAICrawler(claudeBot), true);
assert.equal(isAICrawler(searchGPT), true);
assert.equal(isAICrawler(semrushAI), true);
// Non-AI Crawlers
const firefox = 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/111.0';
assert.equal(isAICrawler(firefox), false);
});
});
describe('isBot()', () => {
it('Can detect general Bots', () => {
// Bots
const ahrefsBot = 'Mozilla/5.0 (compatible; AhrefsBot/7.0; +http://ahrefs.com/robot/)';
const scrapy = 'Scrapy/1.5.0 (+https://scrapy.org)';
assert.equal(isBot(ahrefsBot), true);
assert.equal(isBot(scrapy), true);
// Non-bots
const firefox = 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/111.0';
const thunderbird = 'Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Thunderbird/78.13.0';
assert.equal(isBot(firefox), false);
assert.equal(isBot(thunderbird), false);
});
});

View File

@@ -0,0 +1,16 @@
const assert = require('assert');
const { UAParser } = require('../../../src/main/ua-parser');
const { isChromeFamily } = require('../../../src/browser-detection/browser-detection');
describe('isChromeFamily()', () => {
it('Can detect Chromium-based browser', () => {
const edge = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.2151.58';
const firefox = 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/111.0';
assert.equal(isChromeFamily(UAParser(edge)), true);
assert.equal(isChromeFamily(edge), true);
assert.equal(isChromeFamily(UAParser(firefox)), false);
assert.equal(isChromeFamily(firefox), false);
});
});

View File

@@ -0,0 +1,32 @@
const assert = require('assert');
const { UAParser } = require('../../../src/main/ua-parser');
const { getDeviceVendor, isAppleSilicon } = require('../../../src/device-detection/device-detection');
const { DeviceVendor } = require('../../../src/enums/ua-parser-enums');
describe('getDeviceVendor()', () => {
it('Can guess the device vendor from a model name', () => {
const sm = 'SM-A605G';
const redmi = 'Redmi Note 8';
const nexus = 'Nexus 6P';
const aquos = 'AQUOS-TVX19B';
assert.equal(getDeviceVendor(sm), DeviceVendor.SAMSUNG);
assert.equal(getDeviceVendor(redmi), DeviceVendor.XIAOMI);
assert.equal(getDeviceVendor(nexus), DeviceVendor.HUAWEI);
assert.equal(getDeviceVendor(aquos), DeviceVendor.SHARP);
});
});
describe('isAppleSilicon()', () => {
it('Can detect Apple Silicon device', () => {
const macARM = 'Mozilla/5.0 (Macintosh; ARM; Mac OS X 10.15; rv:97.0) Gecko/20100101 Firefox/97.0';
const macIntel = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:97.0) Gecko/20100101 Firefox/97.0';
assert.equal(isAppleSilicon(UAParser(macIntel)), false);
assert.equal(isAppleSilicon(macIntel), false);
assert.equal(isAppleSilicon(UAParser(macARM)), true);
assert.equal(isAppleSilicon(macARM), true);
});
});

View File

@@ -3,9 +3,9 @@ const assert = require('assert');
const parseJS = require('@babel/parser').parse;
const traverse = require('@babel/traverse').default;
const safe = require('safe-regex');
const { UAParser } = require('../../src/main/ua-parser');
const { Bots, CLIs, Crawlers, Emails, Fetchers, InApps, Libraries, Vehicles } = require('../../src/extensions/ua-parser-extensions');
const { BrowserType, OSName, Extension } = require('../../src/enums/ua-parser-enums');
const { UAParser } = require('../../../src/main/ua-parser');
const { Bots, CLIs, Crawlers, Emails, Fetchers, InApps, Libraries, Vehicles } = require('../../../src/extensions/ua-parser-extensions');
const { BrowserType, OSName, Extension } = require('../../../src/enums/ua-parser-enums');
const { CLI, Crawler, Email, Fetcher, Library } = Extension.BrowserName;
describe('Extensions', () => {
@@ -19,7 +19,7 @@ describe('Extensions', () => {
['Vehicles', 'vehicle', Vehicles]
]
.forEach(([desc, path, ext]) => {
const tests = require(`../data/ua/extension/${path}.json`);
const tests = require(`../../data/ua/extension/${path}.json`);
describe(desc, () => {
tests.forEach((test) => {
it(`Can detect ${test.desc}: "${test.ua}"`, () => {

View File

@@ -0,0 +1,13 @@
const assert = require('assert');
const { isFrozenUA } = require('../../../src/helpers/ua-parser-helpers');
describe('isFrozenUA()', () => {
it('matches supplied user-agent string with known frozen user-agent pattern', () => {
const regularMobileUA = "Mozilla/5.0 (Linux; Android 9; SM-A205U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.1234.56 Mobile Safari/537.36";
const frozenMobileUA = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.0.0 Mobile Safari/537.36";
assert.equal(isFrozenUA(regularMobileUA), false);
assert.equal(isFrozenUA(frozenMobileUA), true);
});
});

View File

@@ -414,3 +414,19 @@ describe('Identify vendor & type of device from given model name', () => {
});
});
});
describe('Chaining withClientHints() & withFeatureCheck() in server-side development', () => {
const headers = {
'user-agent' : 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15',
'sec-ch-ua-form-factors' : '"VR"'
};
const device = new UAParser(headers).getDevice();
it('Chain order: withFeatureCheck().withClientHints()', () => {
const fc2ch = device.withFeatureCheck().withClientHints();
assert.strictEqual(fc2ch.type, "xr");
});
it('Chain order: withClientHints().withFeatureCheck()', () => {
const ch2fc = device.withClientHints().withFeatureCheck();
assert.strictEqual(ch2fc.type, "xr");
});
});