forked from deco-cx/deco
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathuseSection.ts
More file actions
175 lines (153 loc) · 4.83 KB
/
Copy pathuseSection.ts
File metadata and controls
175 lines (153 loc) · 4.83 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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
import type { ComponentType } from "preact";
import { useContext } from "preact/hooks";
import { SectionContext } from "../components/section.tsx";
import { FieldResolver } from "../engine/core/resolver.ts";
import { Murmurhash3 } from "../deps.ts";
const hasher = new Murmurhash3();
// Seed list adapted from Cloudflare APO
// (https://developers.cloudflare.com/automatic-platform-optimization/reference/query-parameters),
// extended with ad-network families seen in production traffic on commerce sites.
/** Exact-match querystring names that should not vary cache. */
const BLOCKED_QS = new Set<string>([
"ref",
"fbclid",
"fb_action_ids",
"fb_action_types",
"fb_source",
"mc_cid",
"mc_eid",
"gclid",
"dclid",
"msclkid",
"ttclid",
"yclid",
"_ga",
"_gl",
"campaignid",
"adgroupid",
"_ke",
"cn-reloaded",
"age-verified",
"ao_noptimize",
"usqp",
"mkt_tok",
"epik",
"ck_subscriber_id",
"_hsenc",
"_hsmi",
]);
/**
* Prefix-matched querystring names that should not vary cache. Used for
* families where individual params are open-ended (e.g. utm_source, utm_medium,
* utm_id, utm_campaign, utm_content, utm_term — enumerating every variant is
* impractical, and ad platforms keep inventing new ones).
*/
const BLOCKED_QS_PREFIXES: string[] = [
"utm_",
"gad_",
"dgen_",
];
const ALLOWED_QS = new Set<string>();
export const addBlockedQS = (queryStrings: string[]): void => {
queryStrings.forEach((qs) => BLOCKED_QS.add(qs));
};
export const addBlockedQSPrefix = (prefixes: string[]): void => {
for (const p of prefixes) {
// Empty prefix would make startsWith("") match every param, silently
// stripping the entire querystring. Skip without throwing so a single bad
// entry doesn't break the caller.
if (!p) continue;
if (!BLOCKED_QS_PREFIXES.includes(p)) {
BLOCKED_QS_PREFIXES.push(p);
}
}
};
export const addAllowedQS = (queryStrings: string[]): void => {
queryStrings.forEach((qs) => ALLOWED_QS.add(qs));
};
/** Returns new props object with prop __cb with `pathname?querystring` from href */
const createStableHref = (href: string): string => {
const hrefUrl = new URL(href!, "http://localhost:8000");
const qsList = [...hrefUrl.searchParams.keys()];
qsList.forEach((qsName: string) => {
const shouldRemove = ALLOWED_QS.size > 0
? !ALLOWED_QS.has(qsName)
: (BLOCKED_QS.has(qsName) ||
BLOCKED_QS_PREFIXES.some((prefix) => qsName.startsWith(prefix)));
if (shouldRemove) {
hrefUrl.searchParams.delete(qsName);
}
});
hrefUrl.searchParams.sort();
return hrefUrl.href;
};
export interface RenderCbInput {
revision: unknown;
vary: unknown;
href: string;
deploymentId?: string;
}
/**
* Cache-bust value for `/deco/render` URLs. Single source of truth shared by
* `useSection` (web partials) and JSON serialization consumers (e.g. the
* website app's ?renderJson handler) — both sides MUST produce identical
* values or web/JSON cache invalidation diverges.
*
* `revision`/`vary` join as-is (undefined → "undefined") to preserve the
* historical recipe byte-for-byte. Shared `hasher` is safe ONLY because this
* function is fully synchronous — no await between hash() and reset().
*/
export const computeRenderCb = (input: RenderCbInput): string => {
const cbString = [
input.revision,
input.vary,
createStableHref(input.href),
input.deploymentId,
].join("|");
hasher.hash(cbString);
const cb = `${hasher.result()}`;
hasher.reset();
return cb;
};
export type Options<P> = {
/** Section props partially applied */
props?: Partial<P extends ComponentType<infer K> ? K : P>;
/** Path where section is to be found */
href?: string;
};
export const useSection = <P>(
{ props = {}, href }: Pick<Options<P>, "href" | "props"> = {},
): string => {
const ctx = useContext(SectionContext);
if (typeof document !== "undefined") {
throw new Error("Partials cannot be used inside an Island!");
}
if (!ctx) {
throw new Error("Missing context in rendering tree");
}
const revisionId = ctx?.revision;
const vary = ctx?.context.state.vary.build();
const { request, renderSalt, context: { state: { pathTemplate } } } = ctx;
const hrefParam = href ?? request.url;
const stableHref = createStableHref(hrefParam);
const cb = computeRenderCb({
revision: revisionId,
vary,
href: hrefParam,
deploymentId: ctx?.deploymentId,
});
const params = new URLSearchParams([
["props", JSON.stringify(props)],
["href", stableHref],
["pathTemplate", pathTemplate],
["renderSalt", `${renderSalt ?? crypto.randomUUID()}`],
["__cb", `${cb}`],
]);
if ((props as { __resolveType?: string })?.__resolveType === undefined) {
params.set(
"resolveChain",
JSON.stringify(FieldResolver.minify(ctx.resolveChain.slice(0, -1))),
);
}
return `/deco/render?${params}`;
};