Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support client hints #114

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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)

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.

### 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.

## 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')
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]}
})
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 })
}

function deleteUndefinedProperties(obj) {
for (const key of Object.keys(obj)) {
if (typeof obj[key] === 'undefined') {
delete obj[key]
}
}
return obj
}

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