1+ import type { Browser } from 'detect-browser-es'
2+ import { parseUserAgent } from 'detect-browser-es'
3+ import type {
4+ DeviceClientHints ,
5+ DeviceHints ,
6+ ResolvedHttpClientHintsOptions ,
7+ } from '../shared-types/types'
18import { useHttpClientHintsState } from './state'
2- import { defineNuxtPlugin } from '#imports'
9+ import { writeClientHintHeaders , writeHeaders } from './headers'
10+ import { defineNuxtPlugin , useRequestHeaders , useRuntimeConfig } from '#imports'
11+
12+ const DeviceClientHintsHeaders : Record < DeviceHints , string > = {
13+ memory : 'Device-Memory' ,
14+ }
15+
16+ type DeviceClientHintsHeadersKey = keyof typeof DeviceClientHintsHeaders
17+
18+ const AcceptClientHintsRequestHeaders = Object . entries ( DeviceClientHintsHeaders ) . reduce ( ( acc , [ key , value ] ) => {
19+ acc [ key as DeviceClientHintsHeadersKey ] = value . toLowerCase ( ) as Lowercase < string >
20+ return acc
21+ } , { } as Record < DeviceClientHintsHeadersKey , Lowercase < string > > )
22+
23+ const HttpRequestHeaders = Array . from ( Object . values ( DeviceClientHintsHeaders ) ) . concat ( 'user-agent' )
324
425export default defineNuxtPlugin ( {
526 name : 'http-client-hints:device-server:plugin' ,
@@ -9,5 +30,125 @@ export default defineNuxtPlugin({
930 dependsOn : [ 'http-client-hints:init-server:plugin' ] ,
1031 setup ( ) {
1132 const state = useHttpClientHintsState ( )
33+ const httpClientHints = useRuntimeConfig ( ) . public . httpClientHints as ResolvedHttpClientHintsOptions
34+ const requestHeaders = useRequestHeaders < string > ( HttpRequestHeaders )
35+ const userAgentHeader = requestHeaders [ 'user-agent' ]
36+
37+ // 1. extract browser info
38+ const userAgent = userAgentHeader
39+ ? parseUserAgent ( userAgentHeader )
40+ : null
41+ // 2. prepare client hints request
42+ const clientHintsRequest = collectClientHints ( userAgent , httpClientHints . device ! , requestHeaders )
43+ // 3. write client hints response headers
44+ writeClientHintsResponseHeaders ( clientHintsRequest , httpClientHints . device ! )
45+ state . value . deviceInfo = clientHintsRequest
1246 } ,
1347} )
48+
49+ type BrowserFeatureAvailable = ( android : boolean , versions : number [ ] ) => boolean
50+ type BrowserFeatures = Record < DeviceClientHintsHeadersKey , BrowserFeatureAvailable >
51+
52+ // Tests for Browser compatibility
53+ // https://developer.mozilla.org/en-US/docs/Web/API/Device_Memory_API
54+ const chromiumBasedBrowserFeatures : BrowserFeatures = {
55+ memory : ( _ , v ) => v [ 0 ] >= 63 ,
56+ }
57+ const allowedBrowsers : [ browser : Browser , features : BrowserFeatures ] [ ] = [
58+ [ 'chrome' , chromiumBasedBrowserFeatures ] ,
59+ [ 'edge-chromium' , {
60+ memory : ( _ , v ) => v [ 0 ] >= 79 ,
61+ } ] ,
62+ [ 'chromium-webview' , chromiumBasedBrowserFeatures ] ,
63+ [ 'opera' , {
64+ memory : ( android , v ) => v [ 0 ] >= ( android ? 50 : 46 ) ,
65+ } ] ,
66+ ]
67+
68+ const ClientHeaders = [ 'Accept-CH' ]
69+
70+ function browserFeatureAvailable ( userAgent : ReturnType < typeof parseUserAgent > , feature : DeviceClientHintsHeadersKey ) {
71+ if ( userAgent == null || userAgent . type !== 'browser' )
72+ return false
73+
74+ try {
75+ const browserName = userAgent . name
76+ const android = userAgent . os ?. toLowerCase ( ) . startsWith ( 'android' ) ?? false
77+ const versions = userAgent . version . split ( '.' ) . map ( v => Number . parseInt ( v ) )
78+ return allowedBrowsers . some ( ( [ name , check ] ) => {
79+ if ( browserName !== name )
80+ return false
81+
82+ try {
83+ return check [ feature ] ( android , versions )
84+ }
85+ catch {
86+ return false
87+ }
88+ } )
89+ }
90+ catch {
91+ return false
92+ }
93+ }
94+
95+ function lookupClientHints (
96+ userAgent : ReturnType < typeof parseUserAgent > ,
97+ deviceHints : DeviceHints [ ] ,
98+ ) {
99+ const features : DeviceClientHints = {
100+ memoryAvailable : false ,
101+ }
102+
103+ if ( userAgent == null || userAgent . type !== 'browser' )
104+ return features
105+
106+ for ( const hint of deviceHints ) {
107+ features [ `${ hint } Available` ] = browserFeatureAvailable ( userAgent , hint )
108+ }
109+
110+ return features
111+ }
112+
113+ function collectClientHints (
114+ userAgent : ReturnType < typeof parseUserAgent > ,
115+ deviceHints : DeviceHints [ ] ,
116+ headers : { [ key in Lowercase < string > ] ?: string | undefined } ,
117+ ) {
118+ // collect client hints
119+ const hints = lookupClientHints ( userAgent , deviceHints )
120+
121+ for ( const hint of deviceHints ) {
122+ // TODO: review this logic, we need some helpers to parse headers
123+ if ( hint === 'memory' ) {
124+ if ( hints . memoryAvailable ) {
125+ const header = headers [ AcceptClientHintsRequestHeaders . memory ]
126+ if ( header ) {
127+ try {
128+ hints . memory = Number . parseFloat ( header )
129+ }
130+ catch {
131+ // just ignore
132+ }
133+ }
134+ }
135+ }
136+ }
137+
138+ return hints
139+ }
140+
141+ function writeClientHintsResponseHeaders (
142+ deviceClientHints : DeviceClientHints ,
143+ deviceHints : DeviceHints [ ] ,
144+ ) {
145+ const headers : Record < string , string [ ] > = { }
146+
147+ for ( const hint of deviceHints ) {
148+ if ( deviceClientHints [ `${ hint } Available` ] ) {
149+ writeClientHintHeaders ( ClientHeaders , DeviceClientHintsHeaders [ hint ] , headers )
150+ }
151+ }
152+
153+ writeHeaders ( headers )
154+ }
0 commit comments