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'
14import { 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' )
331
432export default defineNuxtPlugin ( {
533 name : 'http-client-hints:network-server:plugin' ,
@@ -9,5 +37,138 @@ export default defineNuxtPlugin({
937 dependsOn : [ 'http-client-hints:init-server:plugin' ] ,
1038 setup ( ) {
1139 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
1253 } ,
1354} )
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