Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,30 @@ export default function ({ $device }) {
}
```

`clientHints.enabled` enables client hints feature.(default by false)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`clientHints.enabled` enables client hints feature.(default by false)
`clientHints.enabled` enables client hints feature (false by default).

😄


Note that the default user agent value is set to `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.39 Safari/537.36`.

## User-Agent Client Hints Support

To enable Client Hints, set clientHints.enabled options to true.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
To enable Client Hints, set clientHints.enabled options to true.
To enable Client Hints, set `clientHints.enabled` options to `true`.


### Client Side

`navigator.userAgentData` are referred to detect a device and a platform.

results from `navigator.userAgent` are overridden.

### Server Side

the following request headers are referred to detect a device and a platform.

- sec-ch-ua
- sec-ch-mobile
- sec-ch-platform

results from user-agent header are overridden.
Comment on lines +150 to +160
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
results from `navigator.userAgent` are overridden.
### Server Side
the following request headers are referred to detect a device and a platform.
- sec-ch-ua
- sec-ch-mobile
- sec-ch-platform
results from user-agent header are overridden.
Results from `navigator.userAgent` are overridden.
### Server Side
The following request headers are referred to detect a device and a platform.
- `sec-ch-ua`
- `sec-ch-mobile`
- `sec-ch-platform`
Results from user-agent header are overridden.


## CloudFront Support

If a user-agent is `Amazon CloudFront`, this module checks
Expand Down
3 changes: 3 additions & 0 deletions lib/module.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ const { defu } = require('defu')
module.exports = function (moduleOptions) {
const options = defu(moduleOptions, this.options.device, {
defaultUserAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.39 Safari/537.36',
clientHints: {
enabled: false
},
refreshOnResize: false
})
// Register plugin
Expand Down
94 changes: 88 additions & 6 deletions lib/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,24 +60,25 @@ function getBrowserName(a) {

const DEFAULT_USER_AGENT = '<%= options.defaultUserAgent %>'
const REFRESH_ON_RESIZE = <%= options.refreshOnResize %>
const USE_CLIENT_HINT = <%= options.clientHints.enabled %>

function extractDevices (ctx, userAgent = DEFAULT_USER_AGENT) {
function extractDevices (headers, userAgent = DEFAULT_USER_AGENT) {
let mobile = null
let mobileOrTablet = null
let ios = null
let android = null

if (userAgent === 'Amazon CloudFront') {
if (ctx.req.headers['cloudfront-is-mobile-viewer'] === 'true') {
if (headers['cloudfront-is-mobile-viewer'] === 'true') {
mobile = true
mobileOrTablet = true
}
if (ctx.req.headers['cloudfront-is-tablet-viewer'] === 'true') {
if (headers['cloudfront-is-tablet-viewer'] === 'true') {
mobile = false
mobileOrTablet = true
}
} else if (ctx.req && ctx.req.headers['cf-device-type']) { // Cloudflare
switch (ctx.req.headers['cf-device-type']) {
} else if (headers['cf-device-type']) { // Cloudflare
switch (headers['cf-device-type']) {
case 'mobile':
mobile = true
mobileOrTablet = true
Expand Down Expand Up @@ -110,6 +111,73 @@ function extractDevices (ctx, userAgent = DEFAULT_USER_AGENT) {
return { mobile, mobileOrTablet, ios, android, windows, macOS, isSafari, isFirefox, isEdge, isChrome, isSamsung, isCrawler }
}

function extractFromUserAgentData(userAgentData) {
const hasBrand = (brandName) => userAgentData.brands.some(b => b.brand === brandName)
const platform = userAgentData.platform

const mobile = userAgentData.mobile
let mobileOrTablet = undefined
if (mobile) {
mobileOrTablet = mobile
}
const ios = undefined
const android = undefined
const windows = platform === 'Windows'
const macOS = platform === 'macOS'
const isSafari = undefined
const isFirefox = undefined
const isEdge = hasBrand('Microsoft Edge')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these brands' list would benefit from being stored in a kind of enum

const isChrome = hasBrand('Google Chrome')
const isSamsung = undefined
const isCrawler = undefined
return deleteUndefinedProperties({ mobile, mobileOrTablet, ios, android, windows, macOS, isSafari, isFirefox, isEdge, isChrome, isSamsung, isCrawler })
}

const REGEX_CLIENT_HINT_BRAND = /"([^"]*)";v="([^"]*)"/
function extractFromUserHint(headers) {
const uaHeader = headers['sec-ch-ua']
const mobileHeader = headers['sec-ch-ua-mobile']
const platform = headers['sec-ch-ua-platform']
if (typeof uaHeader === 'undefined') {
return {}
}
const brands = uaHeader.split(',').map(b => b.trim()).map(brandStr => {
const parsed = brandStr.match(REGEX_CLIENT_HINT_BRAND)
console.log(brandStr, parsed)
return {brand: parsed[1], version: parsed[2]}
})
Comment on lines +144 to +148
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const brands = uaHeader.split(',').map(b => b.trim()).map(brandStr => {
const parsed = brandStr.match(REGEX_CLIENT_HINT_BRAND)
console.log(brandStr, parsed)
return {brand: parsed[1], version: parsed[2]}
})
const brands = uaHeader.split(',').map(brandStr => {
const [,brand,version] = brandStr.trim().match(REGEX_CLIENT_HINT_BRAND)
return { brand, version }
})
  • Removed console.log()
  • Removed map() dedicated to trimming
  • Destructured parsed array

this part isn't defensive enough imo

const hasBrand = (brandName) => brands.some(b => b.brand === brandName)

let mobile = undefined
if (mobileHeader) {
mobile = mobileHeader === '?1'
}
let mobileOrTablet = undefined
if (mobile) {
mobileOrTablet = mobile
}
const ios = undefined
const android = undefined
const windows = platform === 'Windows'
const macOS = platform === 'macOS'
const isSafari = undefined
const isFirefox = undefined
const isEdge = hasBrand('Microsoft Edge')
const isChrome = hasBrand('Google Chrome')
const isSamsung = undefined
const isCrawler = undefined
return deleteUndefinedProperties({ mobile, mobileOrTablet, ios, android, windows, macOS, isSafari, isFirefox, isEdge, isChrome, isSamsung, isCrawler })
Comment on lines +159 to +169
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why setting these to undefined if we're deleting them right away ?

}

function deleteUndefinedProperties(obj) {
for (const key of Object.keys(obj)) {
if (typeof obj[key] === 'undefined') {
delete obj[key]
}
}
return obj
}
Comment on lines +172 to +179
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function deleteUndefinedProperties(obj) {
for (const key of Object.keys(obj)) {
if (typeof obj[key] === 'undefined') {
delete obj[key]
}
}
return obj
}
function deleteUndefinedProperties(obj) {
return Object.fromEntries(
Object.entries(obj).filter(([_, value]) => typeof value !== 'undefined')
)
}

💅


export default async function (ctx, inject) {
const makeFlags = () => {
let userAgent = ''
Expand All @@ -118,10 +186,24 @@ export default async function (ctx, inject) {
} else if (typeof navigator !== 'undefined') {
userAgent = navigator.userAgent
}
const { mobile, mobileOrTablet, ios, android, windows, macOS, isSafari, isFirefox, isEdge, isChrome, isSamsung, isCrawler } = extractDevices(ctx, userAgent)
let headers = {}
if (ctx && ctx.req) {
headers = ctx.req.headers
}
const uaResult = extractDevices(headers, userAgent)
let result = uaResult
if (USE_CLIENT_HINT) {
if (typeof navigator !== 'undefined' && typeof navigator.userAgentData !== 'undefined') {
Object.assign(result, extractFromUserAgentData(navigator.userAgentData))
}
Object.assign(result, extractFromUserHint(headers))
}
const { mobile, mobileOrTablet, ios, android, windows, macOS, isSafari, isFirefox, isEdge, isChrome, isSamsung, isCrawler } = result
return {
<% if (options.test) { %>
extractDevices,
extractFromUserAgentData,
extractFromUserHint,
<% } %>
userAgent,
isMobile: mobile,
Expand Down
Loading