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'
1
8
import { 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' )
3
24
4
25
export default defineNuxtPlugin ( {
5
26
name : 'http-client-hints:device-server:plugin' ,
@@ -9,5 +30,125 @@ export default defineNuxtPlugin({
9
30
dependsOn : [ 'http-client-hints:init-server:plugin' ] ,
10
31
setup ( ) {
11
32
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
12
46
} ,
13
47
} )
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