1
+ import type { Browser } from 'detect-browser-es'
2
+ import { parseUserAgent } from 'detect-browser-es'
3
+ import type { NetworkInfo , NetworkHints , ResolvedHttpClientHintsOptions } from '../shared-types/types'
1
4
import { useHttpClientHintsState } from './state'
2
- import { defineNuxtPlugin } from '#imports'
5
+ import type { GetHeaderType } from './headers'
6
+ import { lookupHeader , writeClientHintHeaders , writeHeaders } from './headers'
7
+ import { defineNuxtPlugin , useRequestHeaders , useRuntimeConfig } from '#imports'
8
+
9
+ const NetworkClientHintsHeaders : Record < NetworkHints , string > = {
10
+ savedata : 'Save-Data' ,
11
+ downlink : 'Downlink' ,
12
+ ect : 'ECT' ,
13
+ rtt : 'RTT' ,
14
+ }
15
+
16
+ const NetworkClientHintsHeadersTypes : Record < NetworkHints , GetHeaderType > = {
17
+ savedata : 'string' ,
18
+ downlink : 'float' ,
19
+ ect : 'string' ,
20
+ rtt : 'int' ,
21
+ }
22
+
23
+ type NetworkClientHintsHeadersKey = keyof typeof NetworkClientHintsHeaders
24
+
25
+ const AcceptClientHintsRequestHeaders = Object . entries ( NetworkClientHintsHeaders ) . reduce ( ( acc , [ key , value ] ) => {
26
+ acc [ key as NetworkClientHintsHeadersKey ] = value . toLowerCase ( ) as Lowercase < string >
27
+ return acc
28
+ } , { } as Record < NetworkClientHintsHeadersKey , Lowercase < string > > )
29
+
30
+ const HttpRequestHeaders = Array . from ( Object . values ( NetworkClientHintsHeaders ) ) . concat ( 'user-agent' )
3
31
4
32
export default defineNuxtPlugin ( {
5
33
name : 'http-client-hints:network-server:plugin' ,
@@ -9,5 +37,138 @@ export default defineNuxtPlugin({
9
37
dependsOn : [ 'http-client-hints:init-server:plugin' ] ,
10
38
setup ( ) {
11
39
const state = useHttpClientHintsState ( )
40
+ const httpClientHints = useRuntimeConfig ( ) . public . httpClientHints as ResolvedHttpClientHintsOptions
41
+ const requestHeaders = useRequestHeaders < string > ( HttpRequestHeaders )
42
+ const userAgentHeader = requestHeaders [ 'user-agent' ]
43
+
44
+ // 1. extract browser info
45
+ const userAgent = userAgentHeader
46
+ ? parseUserAgent ( userAgentHeader )
47
+ : null
48
+ // 2. prepare client hints request
49
+ const clientHintsRequest = collectClientHints ( userAgent , httpClientHints . network ! , requestHeaders )
50
+ // 3. write client hints response headers
51
+ writeClientHintsResponseHeaders ( clientHintsRequest , httpClientHints . network ! )
52
+ state . value . network = clientHintsRequest
12
53
} ,
13
54
} )
55
+
56
+ type BrowserFeatureAvailable = ( android : boolean , versions : number [ ] ) => boolean
57
+ type BrowserFeatures = Record < NetworkClientHintsHeadersKey , BrowserFeatureAvailable >
58
+
59
+ // Tests for Browser compatibility
60
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Save-Data
61
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Downlink
62
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ECT
63
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/RTT
64
+ const chromiumBasedBrowserFeatures : BrowserFeatures = {
65
+ savedata : ( android , v ) => v [ 0 ] >= 49 ,
66
+ downlink : ( _ , v ) => v [ 0 ] >= 67 ,
67
+ ect : ( _ , v ) => v [ 0 ] >= 67 ,
68
+ rtt : ( _ , v ) => v [ 0 ] >= 67 ,
69
+ }
70
+ const allowedBrowsers : [ browser : Browser , features : BrowserFeatures ] [ ] = [
71
+ [ 'chrome' , chromiumBasedBrowserFeatures ] ,
72
+ [ 'edge-chromium' , {
73
+ savedata : ( _ , v ) => v [ 0 ] >= 79 ,
74
+ downlink : ( _ , v ) => v [ 0 ] >= 79 ,
75
+ ect : ( _ , v ) => v [ 0 ] >= 79 ,
76
+ rtt : ( _ , v ) => v [ 0 ] >= 79 ,
77
+ } ] ,
78
+ [ 'chromium-webview' , chromiumBasedBrowserFeatures ] ,
79
+ [ 'opera' , {
80
+ savedata : ( _ , v ) => v [ 0 ] >= 35 ,
81
+ downlink : ( android , v ) => v [ 0 ] >= ( android ? 48 : 54 ) ,
82
+ ect : ( android , v ) => v [ 0 ] >= ( android ? 48 : 54 ) ,
83
+ rtt : ( android , v ) => v [ 0 ] >= ( android ? 48 : 54 ) ,
84
+ } ] ,
85
+ ]
86
+
87
+ const ClientHeaders = [ 'Accept-CH' , 'Vary' ]
88
+
89
+ function browserFeatureAvailable ( userAgent : ReturnType < typeof parseUserAgent > , feature : NetworkClientHintsHeadersKey ) {
90
+ if ( userAgent == null || userAgent . type !== 'browser' )
91
+ return false
92
+
93
+ try {
94
+ const browserName = userAgent . name
95
+ const android = userAgent . os ?. toLowerCase ( ) . startsWith ( 'android' ) ?? false
96
+ const versions = userAgent . version . split ( '.' ) . map ( v => Number . parseInt ( v ) )
97
+ return allowedBrowsers . some ( ( [ name , check ] ) => {
98
+ if ( browserName !== name )
99
+ return false
100
+
101
+ try {
102
+ return check [ feature ] ( android , versions )
103
+ }
104
+ catch {
105
+ return false
106
+ }
107
+ } )
108
+ }
109
+ catch {
110
+ return false
111
+ }
112
+ }
113
+
114
+ function lookupClientHints (
115
+ userAgent : ReturnType < typeof parseUserAgent > ,
116
+ networkHints : NetworkHints [ ] ,
117
+ ) {
118
+ const features : NetworkInfo = {
119
+ savedataAvailable : false ,
120
+ downlinkAvailable : false ,
121
+ ectAvailable : false ,
122
+ rttAvailable : false ,
123
+ }
124
+
125
+ if ( userAgent == null || userAgent . type !== 'browser' )
126
+ return features
127
+
128
+ for ( const hint of networkHints ) {
129
+ features [ `${ hint } Available` ] = browserFeatureAvailable ( userAgent , hint )
130
+ }
131
+
132
+ return features
133
+ }
134
+
135
+ function collectClientHints (
136
+ userAgent : ReturnType < typeof parseUserAgent > ,
137
+ networkHints : NetworkHints [ ] ,
138
+ headers : { [ key in Lowercase < string > ] ?: string | undefined } ,
139
+ ) {
140
+ // collect client hints
141
+ const hints = lookupClientHints ( userAgent , networkHints )
142
+
143
+ for ( const hint of networkHints ) {
144
+ if ( hints [ `${ hint } Available` ] ) {
145
+ const value = lookupHeader (
146
+ NetworkClientHintsHeadersTypes [ hint ] ,
147
+ AcceptClientHintsRequestHeaders [ hint ] ,
148
+ headers ,
149
+ )
150
+ console . log ( { hint, value } )
151
+ if ( typeof value !== 'undefined' ) {
152
+ // @ts -expect-error Type 'number | "on" | NetworkECT | undefined' is not assignable to type 'undefined'.
153
+ hints [ hint ] = value as typeof hints [ typeof hint ]
154
+ }
155
+ }
156
+ }
157
+
158
+ return hints
159
+ }
160
+
161
+ function writeClientHintsResponseHeaders (
162
+ networkInfo : NetworkInfo ,
163
+ networkHints : NetworkHints [ ] ,
164
+ ) {
165
+ const headers : Record < string , string [ ] > = { }
166
+
167
+ for ( const hint of networkHints ) {
168
+ if ( networkInfo [ `${ hint } Available` ] ) {
169
+ writeClientHintHeaders ( ClientHeaders , NetworkClientHintsHeaders [ hint ] , headers )
170
+ }
171
+ }
172
+
173
+ writeHeaders ( headers )
174
+ }
0 commit comments