-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathindex.ts
More file actions
137 lines (119 loc) · 4.26 KB
/
index.ts
File metadata and controls
137 lines (119 loc) · 4.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import { ClientHint, ClientHintsValue } from './utils.js'
export type { ClientHint, ClientHintsValue }
export function getHintUtils<Hints extends Record<string, ClientHint<any>>>(
hints: Hints,
) {
function getCookieValue(cookieString: string, name: string) {
const hint = hints[name]
if (!hint) {
throw new Error(
`Unknown client hint: ${typeof name === 'string' ? name : 'Unknown'}`,
)
}
const value = cookieString
.split(';')
.map((c: string) => c.trim())
.find((c: string) => c.startsWith(hint.cookieName + '='))
?.split('=')[1]
if (!value) return null
try {
return decodeURIComponent(value)
} catch (error) {
// Handle malformed URI gracefully by falling back to null
// This prevents crashes and allows the hint's fallback value to be used
console.warn(
`Failed to decode cookie value for ${hint.cookieName}: ${error}`,
)
return null
}
}
function getHints(request?: Request): ClientHintsValue<Hints> {
const cookieString =
typeof document !== 'undefined'
? document.cookie
: typeof request !== 'undefined'
? request.headers.get('Cookie') ?? ''
: ''
return Object.entries(hints).reduce((acc, [name, hint]) => {
const hintName = name
if ('transform' in hint) {
// @ts-expect-error - this is fine (PRs welcome though)
acc[hintName] = hint.transform(
getCookieValue(cookieString, hintName) ?? hint.fallback,
)
} else {
// @ts-expect-error - this is fine (PRs welcome though)
acc[hintName] = getCookieValue(cookieString, hintName) ?? hint.fallback
}
return acc
}, {} as ClientHintsValue<Hints>)
}
/**
* This returns a string of JavaScript that can be used to check if the client
* hints have changed and will reload the page if they have.
*/
function getClientHintCheckScript() {
return `
// This block of code allows us to check if the client hints have changed and
// force a reload of the page with updated hints if they have so you don't get
// a flash of incorrect content.
function checkClientHints() {
if (!navigator.cookieEnabled) return;
// set a short-lived cookie to make sure we can set cookies
document.cookie = "canSetCookies=1; Max-Age=60; SameSite=Lax; path=/";
const canSetCookies = document.cookie.includes("canSetCookies=1");
document.cookie = "canSetCookies=; Max-Age=-1; path=/";
if (!canSetCookies) return;
const cookies = document.cookie.split(';').map(c => c.trim()).reduce((acc, cur) => {
const [key, value] = cur.split('=');
acc[key] = value;
return acc;
}, {});
let cookieChanged = false;
const hints = [
${Object.values(hints)
.map((hint) => {
const cookieName = JSON.stringify(hint.cookieName)
return `{ name: ${cookieName}, actual: String(${hint.getValueCode}), value: cookies[${cookieName}] != null ? cookies[${cookieName}] : encodeURIComponent("${hint.fallback}") }`
})
.join(',\n')}
];
// Add safety check to prevent infinite refresh scenarios
let reloadAttempts = parseInt(sessionStorage.getItem('clientHintReloadAttempts') || '0');
if (reloadAttempts > 3) {
console.warn('Too many client hint reload attempts, skipping reload to prevent infinite loop');
return;
}
for (const hint of hints) {
document.cookie = encodeURIComponent(hint.name) + '=' + encodeURIComponent(hint.actual) + '; Max-Age=31536000; SameSite=Lax; path=/';
try {
const decodedValue = decodeURIComponent(hint.value);
if (decodedValue !== hint.actual) {
cookieChanged = true;
}
} catch (error) {
// Handle malformed URI gracefully
console.warn('Failed to decode cookie value during client hint check:', error);
// If we can't decode the value, assume it's different to be safe
cookieChanged = true;
}
}
if (cookieChanged) {
// Increment reload attempts counter
sessionStorage.setItem('clientHintReloadAttempts', String(reloadAttempts + 1));
// Hide the page content immediately to prevent visual flicker
const style = document.createElement('style');
style.textContent = 'html { visibility: hidden !important; }';
document.head.appendChild(style);
// Trigger the reload
window.location.reload();
} else {
// Reset reload attempts counter if no reload was needed
sessionStorage.removeItem('clientHintReloadAttempts');
}
}
checkClientHints();
`
}
return { getHints, getClientHintCheckScript }
}