From 3f105fe93b8761a7d048f1d3f812c4f94bcf3c2e Mon Sep 17 00:00:00 2001 From: Faisal Salman Date: Wed, 23 Aug 2023 14:38:34 +0700 Subject: [PATCH] [helpers] split `helpers` into 2 new packages: `user-agent-helpers` & `client-hints-helpers` --- package-lock.json | 24 +++- script/build-module.js | 89 ++++++------ .../client-hints-helpers.d.ts} | 2 - .../client-hints-helpers.js | 88 ++++++++++++ .../client-hints-helpers.mjs | 92 ++++++++++++ .../package.json | 14 +- src/client-hints-helpers/readme.md | 75 ++++++++++ src/helpers/readme.md | 131 ------------------ src/helpers/ua-parser-helpers.mjs | 44 ------ src/user-agent-helpers/package.json | 32 +++++ src/user-agent-helpers/readme.md | 67 +++++++++ .../user-agent-helpers.d.ts | 4 + .../user-agent-helpers.js} | 87 +----------- src/user-agent-helpers/user-agent-helpers.mjs | 101 ++++++++++++++ test/mocha-test-helpers.js | 5 +- test/playwright-test-helpers.spec.mjs | 47 ------- 16 files changed, 543 insertions(+), 359 deletions(-) rename src/{helpers/ua-parser-helpers.d.ts => client-hints-helpers/client-hints-helpers.d.ts} (90%) create mode 100644 src/client-hints-helpers/client-hints-helpers.js create mode 100644 src/client-hints-helpers/client-hints-helpers.mjs rename src/{helpers => client-hints-helpers}/package.json (62%) create mode 100644 src/client-hints-helpers/readme.md delete mode 100644 src/helpers/readme.md delete mode 100644 src/helpers/ua-parser-helpers.mjs create mode 100644 src/user-agent-helpers/package.json create mode 100644 src/user-agent-helpers/readme.md create mode 100644 src/user-agent-helpers/user-agent-helpers.d.ts rename src/{helpers/ua-parser-helpers.js => user-agent-helpers/user-agent-helpers.js} (63%) create mode 100644 src/user-agent-helpers/user-agent-helpers.mjs delete mode 100644 test/playwright-test-helpers.spec.mjs diff --git a/package-lock.json b/package-lock.json index 9d1ca35ff..613ed7399 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,8 @@ ], "license": "MIT", "workspaces": [ - "src/helpers" + "src/client-hints-helpers", + "src/user-agent-helpers" ], "devDependencies": { "@babel/parser": "7.15.8", @@ -763,8 +764,12 @@ "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==", "dev": true }, - "node_modules/@ua-parser-js/helpers": { - "resolved": "src/helpers", + "node_modules/@ua-parser-js/client-hints-helpers": { + "resolved": "src/client-hints-helpers", + "link": true + }, + "node_modules/@ua-parser-js/user-agent-helpers": { + "resolved": "src/user-agent-helpers", "link": true }, "node_modules/@ungap/promise-all-settled": { @@ -3778,9 +3783,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "src/client-hints-helpers": { + "name": "@ua-parser-js/client-hints-helpers", + "version": "0.0.1", + "license": "MIT" + }, "src/helpers": { "name": "@ua-parser-js/helpers", - "version": "2.0.0-alpha.3", + "version": "0.0.3", + "extraneous": true, + "license": "MIT" + }, + "src/user-agent-helpers": { + "name": "@ua-parser-js/user-agent-helpers", + "version": "0.0.1", "license": "MIT" } } diff --git a/script/build-module.js b/script/build-module.js index d7f5a59ae..3b00317f6 100755 --- a/script/build-module.js +++ b/script/build-module.js @@ -1,57 +1,64 @@ #!/usr/bin/env node /* jshint esversion: 6 */ const fs = require('fs'); -const PATH = { - main : { - src : 'src/main/ua-parser.js', - dest : 'src/main/ua-parser.mjs', - title : '' - }, - enums : { - src : 'src/enums/ua-parser-enums.js', - dest :'src/enums/ua-parser-enums.mjs', - title : 'enums' - }, - extensions : { - src : 'src/extensions/ua-parser-extensions.js', - dest : 'src/extensions/ua-parser-extensions.mjs', - title : 'extensions' - }, - helpers : { - src : 'src/helpers/ua-parser-helpers.js', - dest : 'src/helpers/ua-parser-helpers.mjs', - title : 'helpers' - } -}; -const generateMJS = (module, replacers) => { - const { src, dest, title } = PATH[module]; + +const generateMJS = (module) => { + let { src, dest, title, replacements } = module; let text = fs.readFileSync(src, 'utf-8'); - replacers.forEach(replacer => { - text = text.replace(replacer[0], replacer[1]); + + replacements.push( + [/const (.+?)\s*=\s*require\((.+)\)/ig, 'import $1 from $2'], + [/module\.exports =/ig, 'export'] + ); + replacements.forEach(rep => { + text = text.replace(rep[0], rep[1]); }); console.log(`Generate ${dest}`); fs.writeFileSync(dest, -`// Generated ESM version of UAParser.js ${title} +`// Generated ESM version of ${title} // DO NOT EDIT THIS FILE! // Source: /${src} ${text}`, 'utf-8'); -}; - -// ua-parser.mjs -generateMJS('main', [ - [/\(func[\s\S]+strict\';/ig, ''], - [/esversion\: 3/ig, 'esversion: 6'], - [/\/[\/\s]+export[\s\S]+/ig,'export {UAParser};'] -]); -// ua-parser-enum.mjs -generateMJS('enums', [[/module\.exports =/ig, 'export']]); +}; -// ua-parser-extension.mjs -generateMJS('extensions', [[/module\.exports =/ig, 'export']]); +const modules = [ + { + src : 'src/main/ua-parser.js', + dest : 'src/main/ua-parser.mjs', + title : 'ua-parser-js', + replacements : [ + [/\(func[\s\S]+strict\';/ig, ''], + [/esversion\: 3/ig, 'esversion: 6'], + [/\/[\/\s]+export[\s\S]+/ig,'export {UAParser};'] + ] + },{ + src : 'src/enums/ua-parser-enums.js', + dest :'src/enums/ua-parser-enums.mjs', + title : 'ua-parser-js/enums', + replacements : [] + }, + { + src : 'src/extensions/ua-parser-extensions.js', + dest : 'src/extensions/ua-parser-extensions.mjs', + title : 'ua-parser-js/extensions', + replacements : [] + }, + { + src : 'src/user-agent-helpers/user-agent-helpers.js', + dest : 'src/user-agent-helpers/user-agent-helpers.mjs', + title : '@ua-parser-js/user-agent-helpers', + replacements : [] + }, + { + src : 'src/client-hints-helpers/client-hints-helpers.js', + dest : 'src/client-hints-helpers/client-hints-helpers.mjs', + title : '@ua-parser-js/client-hints-helpers', + replacements : [] + } +]; -// ua-parser-helpers.mjs -generateMJS('helpers', [[/module\.exports =/ig, 'export']]); \ No newline at end of file +modules.forEach(module => generateMJS(module)); \ No newline at end of file diff --git a/src/helpers/ua-parser-helpers.d.ts b/src/client-hints-helpers/client-hints-helpers.d.ts similarity index 90% rename from src/helpers/ua-parser-helpers.d.ts rename to src/client-hints-helpers/client-hints-helpers.d.ts index 836de09d4..00f98f84b 100644 --- a/src/helpers/ua-parser-helpers.d.ts +++ b/src/client-hints-helpers/client-hints-helpers.d.ts @@ -32,6 +32,4 @@ export interface ClientHintsHTTPHeaders { 'sec-ch-ua-wow64'?: string; } -export function isFrozenUA(ua: string): boolean; -export function unfreezeUA(): Promise; export function UACHParser(headers: ClientHintsHTTPHeaders): ClientHintsJSHighEntropy; \ No newline at end of file diff --git a/src/client-hints-helpers/client-hints-helpers.js b/src/client-hints-helpers/client-hints-helpers.js new file mode 100644 index 000000000..5fa3bbc77 --- /dev/null +++ b/src/client-hints-helpers/client-hints-helpers.js @@ -0,0 +1,88 @@ +//////////////////////////////////////////////////// +/* A collection of utility methods for client-hints + https://github.com/faisalman/ua-parser-js + Author: Faisal Salman + MIT License */ +/////////////////////////////////////////////////// + +/*jshint esversion: 11 */ + +const UACHMap = { + 'sec-ch-ua-arch' : { + prop : 'architecture', + type : 'sf-string' + }, + 'sec-ch-ua-bitness' : { + prop : 'bitness', + type : 'sf-string' + }, + 'sec-ch-ua' : { + prop : 'brands', + type : 'sf-list' + }, + 'sec-ch-ua-form-factor' : { + prop : 'formFactor', + type : 'sf-string' + }, + 'sec-ch-ua-full-version-list' : { + prop : 'fullVersionList', + type : 'sf-list' + }, + 'sec-ch-ua-mobile' : { + prop : 'mobile', + type : 'sf-boolean', + }, + 'sec-ch-ua-model' : { + prop : 'model', + type : 'sf-string', + }, + 'sec-ch-ua-platform' : { + prop : 'platform', + type : 'sf-string' + }, + 'sec-ch-ua-platform-version' : { + prop : 'platformVersion', + type : 'sf-string' + }, + 'sec-ch-ua-wow64' : { + prop : 'wow64', + type : 'sf-boolean' + } +}; + +const UACHParser = (headers) => { + const parse = (str, type) => { + if (!str) { + return ''; + } + switch (type) { + case 'sf-boolean': + return /\?1/.test(str); + case 'sf-list': + return str.replace(/\\?\"/g, '') + .split(', ') + .map(brands => { + const [brand, version] = brands.split(';v='); + return { + brand : brand, + version : version + }; + }); + case 'sf-string': + default: + return str.replace(/\\?\"/g, ''); + } + }; + let ch = {}; + Object.keys(UACHMap).forEach(field => { + if (headers.hasOwnProperty(field)) { + const { prop, type } = UACHMap[field]; + ch[prop] = parse(headers[field], type); + } + }); + return ch; +}; + +module.exports = { + UACHParser +}; \ No newline at end of file diff --git a/src/client-hints-helpers/client-hints-helpers.mjs b/src/client-hints-helpers/client-hints-helpers.mjs new file mode 100644 index 000000000..184e0002d --- /dev/null +++ b/src/client-hints-helpers/client-hints-helpers.mjs @@ -0,0 +1,92 @@ +// Generated ESM version of @ua-parser-js/client-hints-helpers +// DO NOT EDIT THIS FILE! +// Source: /src/client-hints-helpers/client-hints-helpers.js + +//////////////////////////////////////////////////// +/* A collection of utility methods for client-hints + https://github.com/faisalman/ua-parser-js + Author: Faisal Salman + MIT License */ +/////////////////////////////////////////////////// + +/*jshint esversion: 11 */ + +const UACHMap = { + 'sec-ch-ua-arch' : { + prop : 'architecture', + type : 'sf-string' + }, + 'sec-ch-ua-bitness' : { + prop : 'bitness', + type : 'sf-string' + }, + 'sec-ch-ua' : { + prop : 'brands', + type : 'sf-list' + }, + 'sec-ch-ua-form-factor' : { + prop : 'formFactor', + type : 'sf-string' + }, + 'sec-ch-ua-full-version-list' : { + prop : 'fullVersionList', + type : 'sf-list' + }, + 'sec-ch-ua-mobile' : { + prop : 'mobile', + type : 'sf-boolean', + }, + 'sec-ch-ua-model' : { + prop : 'model', + type : 'sf-string', + }, + 'sec-ch-ua-platform' : { + prop : 'platform', + type : 'sf-string' + }, + 'sec-ch-ua-platform-version' : { + prop : 'platformVersion', + type : 'sf-string' + }, + 'sec-ch-ua-wow64' : { + prop : 'wow64', + type : 'sf-boolean' + } +}; + +const UACHParser = (headers) => { + const parse = (str, type) => { + if (!str) { + return ''; + } + switch (type) { + case 'sf-boolean': + return /\?1/.test(str); + case 'sf-list': + return str.replace(/\\?\"/g, '') + .split(', ') + .map(brands => { + const [brand, version] = brands.split(';v='); + return { + brand : brand, + version : version + }; + }); + case 'sf-string': + default: + return str.replace(/\\?\"/g, ''); + } + }; + let ch = {}; + Object.keys(UACHMap).forEach(field => { + if (headers.hasOwnProperty(field)) { + const { prop, type } = UACHMap[field]; + ch[prop] = parse(headers[field], type); + } + }); + return ch; +}; + +export { + UACHParser +}; \ No newline at end of file diff --git a/src/helpers/package.json b/src/client-hints-helpers/package.json similarity index 62% rename from src/helpers/package.json rename to src/client-hints-helpers/package.json index b5167cfaa..39a2dc2e6 100644 --- a/src/helpers/package.json +++ b/src/client-hints-helpers/package.json @@ -1,13 +1,13 @@ { - "title": "UAParser.js Helpers", - "name": "@ua-parser-js/helpers", - "version": "0.0.3", + "title": "Client-Hints Helpers", + "name": "@ua-parser-js/client-hints-helpers", + "version": "0.0.1", "author": "Faisal Salman ", - "description": "A collection of utility methods for UAParser.js", - "main": "ua-parser-helpers.js", - "module": "ua-parser-helpers.mjs", + "description": "A collection of utility methods for working with client-hints", + "main": "client-hints-helpers.js", + "module": "client-hints-helpers.mjs", "scripts": { - "test": "echo 1" + "test": "mocha ../../test/mocha-test-helpers" }, "repository": { "type": "git", diff --git a/src/client-hints-helpers/readme.md b/src/client-hints-helpers/readme.md new file mode 100644 index 000000000..b1f569e38 --- /dev/null +++ b/src/client-hints-helpers/readme.md @@ -0,0 +1,75 @@ +# @ua-parser-js/client-hints-helpers + +This is a [UAParser.js](https://github.com/faisalman/ua-parser-js) module that contains a collection of utility methods for working with user-agent client-hints. + +```sh +npm i @ua-parser-js/client-hints-helpers +``` + +### * `UACHParser(headers:object):object` + +Parse user-agent client-hints HTTP headers (sec-ch-ua) into its JS API equivalent + +## Code Example + +```js +import { UACHParser } from '@ua-parser-js/client-hints-helpers'; + +/* + Suppose we're in a server having this client hints data: + + const headers = { + 'sec-ch-ua' : '"Chromium";v="93", "Google Chrome";v="93", " Not;A Brand";v="99"', + 'sec-ch-ua-full-version-list' : '"Chromium";v="93.0.1.2", "Google Chrome";v="93.0.1.2", " Not;A Brand";v="99.0.1.2"', + 'sec-ch-ua-arch' : 'arm', + 'sec-ch-ua-bitness' : '64', + 'sec-ch-ua-mobile' : '?1', + 'sec-ch-ua-model' : 'Pixel 99', + 'sec-ch-ua-platform' : 'Linux', + 'sec-ch-ua-platform-version' : '13', + 'user-agent' : 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36' + }; +*/ + +const userAgentData = UACHParser(headers); + +console.log(userAgentData); +/* + { + "architecture": "arm", + "bitness": "64", + "brands": [ + { + "brand": "Chromium", + "version": "93" + }, + { + "brand": "Google Chrome", + "version": "93" + }, + { + "brand": " Not;A Brand", + "version": "99" + } + ], + "fullVersionList": [ + { + "brand": "Chromium", + "version": "93.0.1.2" + }, + { + "brand": "Google Chrome", + "version": "93.0.1.2" + }, + { + "brand": " Not;A Brand", + "version": "99.0.1.2" + } + ], + "mobile": true, + "model": "Pixel 99", + "platform": "Linux", + "platformVersion": "13" + } +*/ +``` \ No newline at end of file diff --git a/src/helpers/readme.md b/src/helpers/readme.md deleted file mode 100644 index 5f1efd2c8..000000000 --- a/src/helpers/readme.md +++ /dev/null @@ -1,131 +0,0 @@ -# @ua-parser-js/helpers - -This package contains a collection of utility methods for [UAParser.js](https://github.com/faisalman/ua-parser-js) - -```sh -npm i @ua-parser-js/helpers -``` - -### * `isFrozenUA(ua:string):boolean` - -Check whether a user-agent string match with [frozen user-agent pattern](https://www.chromium.org/updates/ua-reduction/) - -### * `unfreezeUA():Promise` - -Construct new unfreezed user-agent string using real data from client hints - -### * `UACHParser(headers: object): object` - -Parse client hints HTTP headers (sec-ch-ua) into its JS API equivalent - -## Code Example - -```js -import { isFrozenUA } from '@ua-parser-js/helpers'; - -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 freezedMobileUA = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.0.0 Mobile Safari/537.36"; - -console.log(isFrozenUA(regularMobileUA)); -// false - -console.log(isFrozenUA(freezedMobileUA)); -// true -``` - -```js -import { unfreezeUA } from '@ua-parser-js/helpers'; - -/* - Suppose we're in a browser having this client hints data: - - { - fullVersionList: [ - { - brand: 'New Browser', - version: '110.1.2.3' - }, - { - brand: 'Chromium', - version: '110.1.2.3' - }, - { - brand: 'Not(A:Brand', - version: '110' - } - ], - platform: 'Windows', - platformVersion: '13.0.0' - } - - And a freezed user-agent: - - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.0.0 Safari/537.36' -*/ - -unfreezeUA() - .then(ua => console.log(ua)); -// 'Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) New Browser/110.1.2.3 Chromium/110.1.2.3 Safari/537.36' -``` - -```js -import { UACHParser } from '@ua-parser-js/helpers'; - -/* - Suppose we're in a server having this client hints data: - - const headers = { - 'sec-ch-ua' : '"Chromium";v="93", "Google Chrome";v="93", " Not;A Brand";v="99"', - 'sec-ch-ua-full-version-list' : '"Chromium";v="93.0.1.2", "Google Chrome";v="93.0.1.2", " Not;A Brand";v="99.0.1.2"', - 'sec-ch-ua-arch' : 'arm', - 'sec-ch-ua-bitness' : '64', - 'sec-ch-ua-mobile' : '?1', - 'sec-ch-ua-model' : 'Pixel 99', - 'sec-ch-ua-platform' : 'Linux', - 'sec-ch-ua-platform-version' : '13', - 'user-agent' : 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36' - }; -*/ - -const userAgentData = UACHParser(headers); - -console.log(userAgentData); -/* - { - "architecture": "arm", - "bitness": "64", - "brands": [ - { - "brand": "Chromium", - "version": "93" - }, - { - "brand": "Google Chrome", - "version": "93" - }, - { - "brand": " Not;A Brand", - "version": "99" - } - ], - "fullVersionList": [ - { - "brand": "Chromium", - "version": "93.0.1.2" - }, - { - "brand": "Google Chrome", - "version": "93.0.1.2" - }, - { - "brand": " Not;A Brand", - "version": "99.0.1.2" - } - ], - "mobile": true, - "model": "Pixel 99", - "platform": "Linux", - "platformVersion": "13" - } -*/ -``` \ No newline at end of file diff --git a/src/helpers/ua-parser-helpers.mjs b/src/helpers/ua-parser-helpers.mjs deleted file mode 100644 index bf0dd6650..000000000 --- a/src/helpers/ua-parser-helpers.mjs +++ /dev/null @@ -1,44 +0,0 @@ -// Generated ESM version of UAParser.js helpers -// DO NOT EDIT THIS FILE! -// Source: /src/helpers/ua-parser-helpers.js - -/////////////////////////////////////////////// -/* Helpers for UAParser.js - https://github.com/faisalman/ua-parser-js - Author: Faisal Salman - MIT License */ -////////////////////////////////////////////// - -/*jshint esversion: 6 */ - -/* - # Reference: - https://www.chromium.org/updates/ua-reduction/ - - # Desktop - --- - Format: - Mozilla/5.0 () AppleWebKit/537.36 (KHTML, like Gecko) Chrome/.0.0.0 Safari/537.36 - - Possible values: - - Windows NT 10.0; Win64; x64 - - Macintosh; Intel Mac OS X 10_15_7 - - X11; Linux x86_64 - - X11; CrOS x86_64 14541.0.0 - - Fuchsia - - # Mobile & Tablet: (except iOS/Android WebView) - --- - Format: - Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/.0.0.0 Safari/537.36 - - Possible values: - - "Mobile" - - "" (empty string for Tablets & Desktop) -*/ - -const isFrozenUA = ua => /Mozilla\/5\.0 \((Windows NT 10\.0; Win64; x64|Macintosh; Intel Mac OS X 10_15_7|X11; Linux x86_64|X11; CrOS x86_64 14541\.0\.0|Fuchsia|Linux; Android 10; K)\) AppleWebKit\/537\.36 \(KHTML, like Gecko\) Chrome\/\d+\.0\.0\.0 (Mobile )?Safari\/537\.36/.test(ua); - -export { - isFrozenUA -}; \ No newline at end of file diff --git a/src/user-agent-helpers/package.json b/src/user-agent-helpers/package.json new file mode 100644 index 000000000..aef3208e2 --- /dev/null +++ b/src/user-agent-helpers/package.json @@ -0,0 +1,32 @@ +{ + "title": "User-Agent Helpers", + "name": "@ua-parser-js/user-agent-helpers", + "version": "0.0.1", + "author": "Faisal Salman ", + "description": "A collection of utility methods for working with user-agent", + "main": "user-agent-helpers.js", + "module": "user-agent-helpers.mjs", + "scripts": { + "test": "mocha ../../test/mocha-test-helpers" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/faisalman/ua-parser-js.git" + }, + "keywords": [ + "ua-parser-js", + "browser-detection", + "device-detection", + "os-detection", + "user-agent", + "client-hints" + ], + "license": "MIT", + "bugs": { + "url": "https://github.com/faisalman/ua-parser-js/issues" + }, + "homepage": "https://github.com/faisalman/ua-parser-js#readme", + "dependencies": { + "@ua-parser-js/client-hints-helpers": "*" + } +} diff --git a/src/user-agent-helpers/readme.md b/src/user-agent-helpers/readme.md new file mode 100644 index 000000000..b2eefd25b --- /dev/null +++ b/src/user-agent-helpers/readme.md @@ -0,0 +1,67 @@ +# @ua-parser-js/user-agent-helpers + +This is a [UAParser.js](https://github.com/faisalman/ua-parser-js) module that contains a collection of utility methods for working with user-agent. + +```sh +npm i @ua-parser-js/user-agent-helpers +``` + +### * `isFrozenUA(ua:string):boolean` + +Check whether a user-agent string match with [frozen user-agent pattern](https://www.chromium.org/updates/ua-reduction/) + +### * `unfreezeUA([ua:string,ch:object]|[headers:object]):Promise` + +Construct new unfreezed user-agent string using real data from client hints + +## Code Example + +```js +import { isFrozenUA } from '@ua-parser-js/user-agent-helpers'; + +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 freezedMobileUA = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.0.0 Mobile Safari/537.36"; + +console.log(isFrozenUA(regularMobileUA)); +// false + +console.log(isFrozenUA(freezedMobileUA)); +// true +``` + +```js +import { unfreezeUA } from '@ua-parser-js/user-agent-helpers'; + +/* + Suppose we're in a browser having this client hints data: + + { + fullVersionList: [ + { + brand: 'New Browser', + version: '110.1.2.3' + }, + { + brand: 'Chromium', + version: '110.1.2.3' + }, + { + brand: 'Not(A:Brand', + version: '110' + } + ], + platform: 'Windows', + platformVersion: '13.0.0', + architecture: 'arm' + } + + With a frozen user-agent: + + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.0.0 Safari/537.36' +*/ + +// Now let's generate a complete user-agent: +unfreezeUA() + .then(newUA => console.log(newUA)); +// 'Mozilla/5.0 (Windows NT 11.0; ARM) AppleWebKit/537.36 (KHTML, like Gecko) New Browser/110.1.2.3 Chromium/110.1.2.3 Safari/537.36' +``` \ No newline at end of file diff --git a/src/user-agent-helpers/user-agent-helpers.d.ts b/src/user-agent-helpers/user-agent-helpers.d.ts new file mode 100644 index 000000000..ae63f9fe7 --- /dev/null +++ b/src/user-agent-helpers/user-agent-helpers.d.ts @@ -0,0 +1,4 @@ +export function isFrozenUA(ua: string): boolean; +export function unfreezeUA(): Promise; +export function unfreezeUA(ua: string, ch: ClientHintsJSHighEntropy): Promise; +export function unfreezeUA(headers: ClientHintsHTTPHeaders): Promise; \ No newline at end of file diff --git a/src/helpers/ua-parser-helpers.js b/src/user-agent-helpers/user-agent-helpers.js similarity index 63% rename from src/helpers/ua-parser-helpers.js rename to src/user-agent-helpers/user-agent-helpers.js index 26d28f726..1b74e1d8e 100644 --- a/src/helpers/ua-parser-helpers.js +++ b/src/user-agent-helpers/user-agent-helpers.js @@ -1,12 +1,14 @@ -/////////////////////////////////////////////// -/* A collection of utility methods for UAParser.js +//////////////////////////////////////////////////// +/* A collection of utility methods for user-agent https://github.com/faisalman/ua-parser-js Author: Faisal Salman MIT License */ -////////////////////////////////////////////// +/////////////////////////////////////////////////// /*jshint esversion: 11 */ +const { UACHParser } = require('@ua-parser-js/client-hints-helpers'); + /* # Reference: https://www.chromium.org/updates/ua-reduction/ @@ -89,84 +91,7 @@ const unfreezeUA = async (ua, ch) => { return ua; }; -const UACHMap = { - 'sec-ch-ua-arch' : { - prop : 'architecture', - type : 'sf-string' - }, - 'sec-ch-ua-bitness' : { - prop : 'bitness', - type : 'sf-string' - }, - 'sec-ch-ua' : { - prop : 'brands', - type : 'sf-list' - }, - 'sec-ch-ua-form-factor' : { - prop : 'formFactor', - type : 'sf-string' - }, - 'sec-ch-ua-full-version-list' : { - prop : 'fullVersionList', - type : 'sf-list' - }, - 'sec-ch-ua-mobile' : { - prop : 'mobile', - type : 'sf-boolean', - }, - 'sec-ch-ua-model' : { - prop : 'model', - type : 'sf-string', - }, - 'sec-ch-ua-platform' : { - prop : 'platform', - type : 'sf-string' - }, - 'sec-ch-ua-platform-version' : { - prop : 'platformVersion', - type : 'sf-string' - }, - 'sec-ch-ua-wow64' : { - prop : 'wow64', - type : 'sf-boolean' - } -}; - -const UACHParser = (headers) => { - const parse = (str, type) => { - if (!str) { - return ''; - } - switch (type) { - case 'sf-boolean': - return /\?1/.test(str); - case 'sf-list': - return str.replace(/\\?\"/g, '') - .split(', ') - .map(brands => { - const [brand, version] = brands.split(';v='); - return { - brand : brand, - version : version - }; - }); - case 'sf-string': - default: - return str.replace(/\\?\"/g, ''); - } - }; - let ch = {}; - Object.keys(UACHMap).forEach(field => { - if (headers.hasOwnProperty(field)) { - const { prop, type } = UACHMap[field]; - ch[prop] = parse(headers[field], type); - } - }); - return ch; -}; - module.exports = { isFrozenUA, - unfreezeUA, - UACHParser + unfreezeUA }; \ No newline at end of file diff --git a/src/user-agent-helpers/user-agent-helpers.mjs b/src/user-agent-helpers/user-agent-helpers.mjs new file mode 100644 index 000000000..a125a49ae --- /dev/null +++ b/src/user-agent-helpers/user-agent-helpers.mjs @@ -0,0 +1,101 @@ +// Generated ESM version of @ua-parser-js/user-agent-helpers +// DO NOT EDIT THIS FILE! +// Source: /src/user-agent-helpers/user-agent-helpers.js + +//////////////////////////////////////////////////// +/* A collection of utility methods for user-agent + https://github.com/faisalman/ua-parser-js + Author: Faisal Salman + MIT License */ +/////////////////////////////////////////////////// + +/*jshint esversion: 11 */ + +import { UACHParser } from '@ua-parser-js/client-hints-helpers'; + +/* + # Reference: + https://www.chromium.org/updates/ua-reduction/ + + # Desktop + --- + Format: + Mozilla/5.0 () AppleWebKit/537.36 (KHTML, like Gecko) Chrome/.0.0.0 Safari/537.36 + + Possible values: + - Windows NT 10.0; Win64; x64 + - Macintosh; Intel Mac OS X 10_15_7 + - X11; Linux x86_64 + - X11; CrOS x86_64 14541.0.0 + - Fuchsia + + # Mobile & Tablet: (except iOS/Android WebView) + --- + Format: + Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/.0.0.0 Safari/537.36 + + Possible values: + - "Mobile" + - "" (empty string for Tablets & Desktop) +*/ + +const isFrozenUA = ua => /^Mozilla\/5\.0 \((Windows NT 10\.0; Win64; x64|Macintosh; Intel Mac OS X 10_15_7|X11; Linux x86_64|X11; CrOS x86_64 14541\.0\.0|Fuchsia|Linux; Android 10; K)\) AppleWebKit\/537\.36 \(KHTML, like Gecko\) Chrome\/\d+\.0\.0\.0 (Mobile )?Safari\/537\.36$/.test(ua); + +const unfreezeUA = async (ua, ch) => { + const env = typeof navigator == 'undefined' ? 'node' : 'browser'; + if (env == 'node') { + if (!ua['user-agent']) { + throw new Error('User-Agent header not found'); + } + ch = UACHParser(ua); + ua = ua['user-agent']; + } else { + ua = ua || navigator.userAgent; + ch = ch || await navigator.userAgentData?.getHighEntropyValues(['arch', 'bitness', 'fullVersionList', 'model', 'platform', 'platformVersion', 'wow64']); + } + if (isFrozenUA(ua) && ch) { + switch (ch.platform) { + case 'Windows': + let [major, minor] = ch.platformVersion + .split('.') + .map(num => parseInt(num, 10)); + major = (major < 1) ? '6' : (major >= 13) ? '11' : '10'; + ua = ua .replace(/(?Windows NT) 10\.0/, `$ ${major}.${minor}`) + .replace(/; (?Win64; x64)/, + (ch.architecture == 'arm') ? + '; ARM' : + (ch.wow64) ? + '; WOW64' : + (ch.architecture == 'x86' && ch.bitness != '64') ? + '' : '; $'); + break; + case 'Android': + ua = ua.replace(/(?Android) 10; K/, `$ ${ch.platformVersion}; ${ch.model}`); + break; + case 'Linux': + case 'Chrome OS': + ua = ua.replace(/(?x86_64)/, + (ch.architecture == 'arm') ? + ((ch.bitness == '64') ? 'arm64' : 'arm') : + (ch.architecture == 'x86' && ch.bitness != '64') ? + 'x86' : '$'); + break; + case 'macOS': + ua = ua.replace(/(?Mac OS X) 10_15_7/, `$ ${ch.platformVersion.replace(/\./, '_')}`); + break; + } + if (ch.fullVersionList) { + ua = ua.replace(/Chrome\/\d+\.0\.0\.0 /, + ch.fullVersionList + .filter(browser => !/not.a.brand/i.test(browser.brand)) + .map(browser => `${browser.brand.replace(/^google /i,'')}/${browser.version} `) + .join('')); + } + } + return ua; +}; + +export { + isFrozenUA, + unfreezeUA +}; \ No newline at end of file diff --git a/test/mocha-test-helpers.js b/test/mocha-test-helpers.js index 07ba15c6c..526887dd0 100644 --- a/test/mocha-test-helpers.js +++ b/test/mocha-test-helpers.js @@ -1,4 +1,5 @@ -const { isFrozenUA, unfreezeUA, UACHParser } = require('@ua-parser-js/helpers'); +const { isFrozenUA, unfreezeUA } = require('@ua-parser-js/user-agent-helpers'); +const { UACHParser } = require('@ua-parser-js/client-hints-helpers'); const assert = require('assert'); describe('isFrozenUA()', () => { @@ -54,7 +55,7 @@ describe('unfreezeUA()', () => { }); describe('UACHParser()', () => { - it('parse client hints HTTP headers (sec-ch-ua) into a JavaScript object', () => { + it('parse client hints HTTP headers (sec-ch-ua) into a client hints-like JavaScript object', () => { assert.deepEqual(UACHParser(headers), { "architecture": "arm", "bitness": "64", diff --git a/test/playwright-test-helpers.spec.mjs b/test/playwright-test-helpers.spec.mjs deleted file mode 100644 index c5bc65e70..000000000 --- a/test/playwright-test-helpers.spec.mjs +++ /dev/null @@ -1,47 +0,0 @@ -// @ts-check -import { test, expect } from '@playwright/test'; -import { unfreezeUA } from '@ua-parser-js/helpers'; - -test('test for unfreezeUA() method', async ({ page }) => { - - - await page.addInitScript(() => { - Object.defineProperty(navigator, 'userAgentData', { - value: { - brands: [], - platform: 'Windows', - mobile: false, - getHighEntropyValues: () => { - return Promise.resolve({ - architecture: 'x86', - bitness: '64', - fullVersionList: [ - { - brand: 'New Browser', - version: '110.1.2.3' - }, - { - brand: 'Chromium', - version: '110.1.2.3' - }, - { - brand: 'Not(A:Brand', - version: '110' - } - ], - platform: 'Windows', - platformVersion: '0.3' - }); - } - } - }); - Object.defineProperty(navigator, 'userAgent', { - value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.0.0 Safari/537.36' - }); - }); - await page.goto('about:blank'); - await page.addScriptTag({ path: './src/helpers/ua-parser-helpers.js' }); - // @ts-ignore - const ua = await page.evaluate(async () => await unfreezeUA()); - expect(ua).toBe('Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) New Browser/110.1.2.3 Chromium/110.1.2.3 Safari/537.36'); -}); \ No newline at end of file