@fayz-ai/storefront 0.81.0 → 0.82.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analytics/bus.d.ts +3 -1
- package/dist/analytics/bus.d.ts.map +1 -1
- package/dist/analytics/posthog.d.ts.map +1 -1
- package/dist/app/router.d.ts +1 -1
- package/dist/blocks/registry.d.ts.map +1 -1
- package/dist/chunk-746BEU7H.js +160 -0
- package/dist/chunk-746BEU7H.js.map +1 -0
- package/dist/{chunk-OIPNINZQ.js → chunk-GI5BTJK6.js} +4 -4
- package/dist/{chunk-OIPNINZQ.js.map → chunk-GI5BTJK6.js.map} +1 -1
- package/dist/{chunk-CWHGBPRP.js → chunk-RYWQGQ4M.js} +307 -178
- package/dist/chunk-RYWQGQ4M.js.map +1 -0
- package/dist/{chunk-2KCORAVB.js → chunk-UJHCVRK3.js} +3 -2
- package/dist/chunk-UJHCVRK3.js.map +1 -0
- package/dist/components/delivery/StoreCover.d.ts.map +1 -1
- package/dist/document/config.d.ts +22 -0
- package/dist/document/config.d.ts.map +1 -1
- package/dist/edge.d.ts +2 -1
- package/dist/edge.d.ts.map +1 -1
- package/dist/edge.js +67 -10
- package/dist/edge.js.map +1 -1
- package/dist/experiments/assign.d.ts +113 -0
- package/dist/experiments/assign.d.ts.map +1 -0
- package/dist/experiments/react.d.ts +98 -0
- package/dist/experiments/react.d.ts.map +1 -0
- package/dist/host/edge-handler.d.ts +29 -2
- package/dist/host/edge-handler.d.ts.map +1 -1
- package/dist/host/store-host.d.ts +11 -1
- package/dist/host/store-host.d.ts.map +1 -1
- package/dist/host.d.ts +2 -0
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +10 -4
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -6
- package/dist/index.js.map +1 -1
- package/dist/render.js +4 -3
- package/dist/render.js.map +1 -1
- package/dist/tanstack.d.ts.map +1 -1
- package/dist/tanstack.js +4 -3
- package/dist/tanstack.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-2KCORAVB.js.map +0 -1
- package/dist/chunk-CWHGBPRP.js.map +0 -1
package/dist/edge.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { NO_ASSIGNMENTS, isBotUserAgent, readVisitorId, visitorCookie, readAssignmentCookie, experimentCacheSuffix, evaluateExperiments, assignmentCookie, EXPERIMENT_HEADER, encodeExperimentHeader } from './chunk-746BEU7H.js';
|
|
2
|
+
export { CONTROL, EXPERIMENT_HEADER, VISITOR_COOKIE, decodeExperimentHeader, encodeExperimentHeader, evaluateExperiments, experimentCacheSuffix, isBotUserAgent, readVisitorId, validateExperiments, visitorCookie } from './chunk-746BEU7H.js';
|
|
3
|
+
|
|
1
4
|
// src/analytics/edge.ts
|
|
2
5
|
function isPostHogProxyPath(pathname, prefix = "/ingest") {
|
|
3
6
|
return pathname === prefix || pathname.startsWith(`${prefix}/`);
|
|
@@ -58,7 +61,7 @@ async function serveStorageImage(request, url, supabaseUrl) {
|
|
|
58
61
|
headers.set("vary", "Accept");
|
|
59
62
|
return new Response(response.body, { status: 200, headers });
|
|
60
63
|
}
|
|
61
|
-
function edgeCacheKey(request, buildId) {
|
|
64
|
+
function edgeCacheKey(request, buildId, variant = "") {
|
|
62
65
|
if (request.method !== "GET") return null;
|
|
63
66
|
const url = new URL(request.url);
|
|
64
67
|
for (const key of [...url.searchParams.keys()]) {
|
|
@@ -67,6 +70,7 @@ function edgeCacheKey(request, buildId) {
|
|
|
67
70
|
url.hash = "";
|
|
68
71
|
if (isStorageRequest(url)) url.searchParams.set("_fmt", imageFormatKey(request));
|
|
69
72
|
url.searchParams.set("_build", buildId);
|
|
73
|
+
if (variant) url.searchParams.set("_exp", variant);
|
|
70
74
|
return new Request(url.toString(), { method: "GET" });
|
|
71
75
|
}
|
|
72
76
|
function edgeCacheable(response) {
|
|
@@ -79,6 +83,55 @@ function withHeader(response, name, value) {
|
|
|
79
83
|
headers.set(name, value);
|
|
80
84
|
return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
|
|
81
85
|
}
|
|
86
|
+
function withCookies(response, cookies) {
|
|
87
|
+
if (!cookies.length) return response;
|
|
88
|
+
const headers = new Headers(response.headers);
|
|
89
|
+
for (const cookie of cookies) headers.append("set-cookie", cookie);
|
|
90
|
+
return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
|
|
91
|
+
}
|
|
92
|
+
function isPageRequest(request, url) {
|
|
93
|
+
if (request.method !== "GET") return false;
|
|
94
|
+
if (isStorageRequest(url) || isPostHogProxyPath(url.pathname)) return false;
|
|
95
|
+
if (/\.[a-z0-9]{1,8}$/i.test(url.pathname)) return false;
|
|
96
|
+
if (url.pathname.startsWith("/_serverFn") || url.pathname.startsWith("/_server")) return false;
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
async function assignForRequest(request, options, fetchImpl) {
|
|
100
|
+
const declared = options.declared ?? {};
|
|
101
|
+
const keys = Object.keys(declared);
|
|
102
|
+
const none = { assignments: NO_ASSIGNMENTS, variant: "", cookies: [] };
|
|
103
|
+
if (!keys.length || !options.posthogKey) return none;
|
|
104
|
+
if (isBotUserAgent(request.headers.get("user-agent"))) return none;
|
|
105
|
+
const cookieHeader = request.headers.get("cookie");
|
|
106
|
+
const secure = new URL(request.url).protocol === "https:";
|
|
107
|
+
const known = readVisitorId(cookieHeader, options.posthogKey);
|
|
108
|
+
const distinctId = known ?? crypto.randomUUID();
|
|
109
|
+
const cookies = known ? [] : [visitorCookie(distinctId, secure)];
|
|
110
|
+
const remembered = known ? readAssignmentCookie(cookieHeader, { experiments: declared, distinctId }) : null;
|
|
111
|
+
if (remembered) return { assignments: remembered, variant: experimentCacheSuffix(remembered, keys), cookies };
|
|
112
|
+
const assignments = await evaluateExperiments({
|
|
113
|
+
experiments: declared,
|
|
114
|
+
distinctId,
|
|
115
|
+
posthogKey: options.posthogKey,
|
|
116
|
+
apiHost: `https://${options.region ?? "us"}.i.posthog.com`,
|
|
117
|
+
timeoutMs: options.timeoutMs,
|
|
118
|
+
fetch: fetchImpl
|
|
119
|
+
});
|
|
120
|
+
const remember = assignmentCookie(assignments, declared, secure);
|
|
121
|
+
return {
|
|
122
|
+
assignments,
|
|
123
|
+
variant: experimentCacheSuffix(assignments, keys),
|
|
124
|
+
cookies: remember ? [...cookies, remember] : cookies
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function withAssignment(request, assignments) {
|
|
128
|
+
const headers = new Headers(request.headers);
|
|
129
|
+
headers.delete(EXPERIMENT_HEADER);
|
|
130
|
+
if (Object.keys(assignments.flags).length) {
|
|
131
|
+
headers.set(EXPERIMENT_HEADER, encodeExperimentHeader({ flags: assignments.flags }));
|
|
132
|
+
}
|
|
133
|
+
return new Request(request, { headers });
|
|
134
|
+
}
|
|
82
135
|
function createStorefrontEdge(options) {
|
|
83
136
|
async function respond(request) {
|
|
84
137
|
const url = new URL(request.url);
|
|
@@ -87,20 +140,24 @@ function createStorefrontEdge(options) {
|
|
|
87
140
|
if (options.isCanonicalHost(url)) return response;
|
|
88
141
|
return withHeader(response, "x-robots-tag", "noindex, nofollow");
|
|
89
142
|
}
|
|
90
|
-
return async (
|
|
91
|
-
|
|
92
|
-
if (
|
|
93
|
-
const
|
|
94
|
-
|
|
143
|
+
return async (incoming) => {
|
|
144
|
+
const url = new URL(incoming.url);
|
|
145
|
+
if (isPostHogProxyPath(url.pathname)) return posthogProxy(incoming);
|
|
146
|
+
const page = options.experiments && isPageRequest(incoming, url) ? await assignForRequest(incoming, options.experiments) : null;
|
|
147
|
+
const request = page ? withAssignment(incoming, page.assignments) : incoming;
|
|
148
|
+
const finish = (response2) => withCookies(response2, page?.cookies ?? []);
|
|
149
|
+
if (options.dev || !options.cache) return finish(await respond(request));
|
|
150
|
+
const key = edgeCacheKey(request, options.buildId, page?.variant ?? "");
|
|
151
|
+
if (!key) return finish(await respond(request));
|
|
95
152
|
const hit = await options.cache.match(key);
|
|
96
|
-
if (hit) return withHeader(hit, "x-edge-cache", "HIT");
|
|
153
|
+
if (hit) return finish(withHeader(hit, "x-edge-cache", "HIT"));
|
|
97
154
|
const response = await respond(request);
|
|
98
|
-
if (!edgeCacheable(response)) return withHeader(response, "x-edge-cache", "BYPASS");
|
|
155
|
+
if (!edgeCacheable(response)) return finish(withHeader(response, "x-edge-cache", "BYPASS"));
|
|
99
156
|
options.waitUntil(options.cache.put(key, response.clone()));
|
|
100
|
-
return withHeader(response, "x-edge-cache", "MISS");
|
|
157
|
+
return finish(withHeader(response, "x-edge-cache", "MISS"));
|
|
101
158
|
};
|
|
102
159
|
}
|
|
103
160
|
|
|
104
|
-
export { createStorefrontEdge, edgeCacheKey, edgeCacheable, imageFormatKey, isPostHogProxyPath, isStorageRequest, posthogProxy, serveStorageImage };
|
|
161
|
+
export { assignForRequest, createStorefrontEdge, edgeCacheKey, edgeCacheable, imageFormatKey, isPostHogProxyPath, isStorageRequest, posthogProxy, serveStorageImage };
|
|
105
162
|
//# sourceMappingURL=edge.js.map
|
|
106
163
|
//# sourceMappingURL=edge.js.map
|
package/dist/edge.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/analytics/edge.ts","../src/host/edge-handler.ts"],"names":[],"mappings":"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","file":"edge.js","sourcesContent":["/**\n * The store's own door to PostHog, for a Worker (or any fetch-style) host.\n *\n * Events sent to the store's domain under `/ingest` are not dropped by ad\n * blockers the way requests to a known analytics host are, and the store needs\n * no PostHog setting of its own: every store forwards to the same project.\n *\n * if (url.pathname.startsWith('/ingest')) return posthogProxy(request)\n */\nexport interface PostHogProxyOptions {\n /** The path the storefront's analytics client sends to. Default `/ingest`. */\n prefix?: string\n /** PostHog's region. Default `us`. */\n region?: 'us' | 'eu'\n}\n\nexport function isPostHogProxyPath(pathname: string, prefix = '/ingest'): boolean {\n return pathname === prefix || pathname.startsWith(`${prefix}/`)\n}\n\nexport async function posthogProxy(request: Request, options: PostHogProxyOptions = {}): Promise<Response> {\n const prefix = options.prefix ?? '/ingest'\n const region = options.region ?? 'us'\n const url = new URL(request.url)\n const path = url.pathname.slice(prefix.length) || '/'\n // The SDK's script and its remote config live on the assets host.\n const host = path.startsWith('/static/') || path.startsWith('/array/')\n ? `${region}-assets.i.posthog.com`\n : `${region}.i.posthog.com`\n const target = new URL(`https://${host}${path}${url.search}`)\n\n const headers = new Headers(request.headers)\n headers.delete('cookie')\n headers.set('host', host)\n // PostHog geolocates from the visitor, not from the Worker.\n const ip = request.headers.get('cf-connecting-ip') ?? request.headers.get('x-forwarded-for')\n if (ip) headers.set('x-forwarded-for', ip)\n\n const init: RequestInit = { method: request.method, headers, redirect: 'manual' }\n if (request.method !== 'GET' && request.method !== 'HEAD') init.body = await request.arrayBuffer()\n const response = await fetch(target, init)\n const out = new Response(response.body, response)\n out.headers.delete('set-cookie')\n return out\n}\n","import { isPostHogProxyPath, posthogProxy } from '../analytics/edge'\n\n// ---------------------------------------------------------------------------\n// The Worker in front of a server-rendered store.\n//\n// Four jobs a store's request handler has before the framework renders\n// anything, first written for artorious-shop and the same for every store:\n//\n// 1. Analytics through the store's own domain (`/ingest` → PostHog), so ad\n// blockers that drop requests to a known analytics host do not drop it.\n// 2. The store's photos from the store's own origin. The bytes live in\n// Supabase Storage; a photo addressed at the storage project is a second\n// origin the browser pays DNS + TLS for (measured 156–241 ms TTFB against\n// 112–141 ms same-origin). Only public objects of the `shop-images`\n// bucket are forwarded — this is not a general proxy.\n// 3. A real edge cache. Public pages already leave with `Cache-Control:\n// public, s-maxage=…`, but on workers.dev nothing in front honours it, so\n// every view paid the full render. The Workers Cache API makes the header\n// mean what it says. Marketing parameters (utm_*, fbclid…) are dropped\n// from the key: ad traffic hits the same entry as everyone else.\n// 4. `X-Robots-Tag: noindex` on any host that is not the store's address, so\n// the deployment URL never competes with the domain.\n// ---------------------------------------------------------------------------\n\n/** `/storage/v1/object/public/shop-images/…` or the `render/image` variant of it. */\nconst STORAGE_PATH = /^\\/storage\\/v1\\/(object|render\\/image)\\/public\\/(shop-images\\/[^?#]+)$/\n\n/** Query parameters the renderer understands. Anything else is dropped rather\n * than forwarded, so a crawler cannot mint unlimited distinct variants. */\nconst RENDER_PARAMS = new Set(['width', 'height', 'quality', 'format', 'resize'])\n\n/** A year: the object path carries a timestamp and is never overwritten. */\nconst IMMUTABLE = 'public, max-age=31536000, s-maxage=31536000, immutable'\n\nconst TRACKING_PARAM = /^(utm_[a-z0-9_]+|fbclid|gclid|gbraid|wbraid|msclkid|ttclid|igshid|mc_cid|mc_eid|_ga|ref)$/i\n\nexport function isStorageRequest(url: URL): boolean {\n return STORAGE_PATH.test(url.pathname)\n}\n\n/**\n * Which format this visitor gets — the only part of the request, besides the\n * URL, that changes the bytes. The renderer answers WebP or the original\n * depending on `accept` and declares no `Vary`, so every cache in front of it\n * has to put the answer in the key itself.\n */\nexport function imageFormatKey(request: Request): 'webp' | 'orig' {\n return /image\\/webp/.test(request.headers.get('accept') ?? '') ? 'webp' : 'orig'\n}\n\n/** Answer a storage request from Supabase, on the store's own origin. */\nexport async function serveStorageImage(request: Request, url: URL, supabaseUrl: string | undefined): Promise<Response> {\n const match = STORAGE_PATH.exec(url.pathname)\n if (!match) return new Response('Not found', { status: 404 })\n const base = supabaseUrl?.replace(/\\/$/, '')\n if (!base) return new Response('Storage is not configured', { status: 502 })\n\n const [, kind, objectPath] = match\n const upstream = new URL(`${base}/storage/v1/${kind}/public/${objectPath}`)\n for (const [key, value] of url.searchParams) {\n if (RENDER_PARAMS.has(key)) upstream.searchParams.set(key, value)\n }\n // A canonical `accept`, not the visitor's verbatim: two browsers that both\n // take WebP must produce the same upstream request.\n const webp = imageFormatKey(request) === 'webp'\n const response = await fetch(upstream.toString(), { headers: { accept: webp ? 'image/webp,image/*;q=0.8' : 'image/*' } })\n if (!response.ok) {\n // Upstream's status is passed through (a deleted photo must not look like\n // a 200), never its cache directives.\n return new Response(response.status === 404 ? 'Not found' : 'Upstream error', {\n status: response.status === 404 ? 404 : 502,\n headers: { 'cache-control': 'public, max-age=60' },\n })\n }\n const headers = new Headers()\n headers.set('content-type', response.headers.get('content-type') ?? 'image/jpeg')\n headers.set('cache-control', IMMUTABLE)\n headers.set('x-image-origin', 'supabase')\n headers.set('vary', 'Accept')\n return new Response(response.body, { status: 200, headers })\n}\n\n/** The cache key for a request, or null when it is not cacheable. */\nexport function edgeCacheKey(request: Request, buildId: string): Request | null {\n if (request.method !== 'GET') return null\n const url = new URL(request.url)\n for (const key of [...url.searchParams.keys()]) {\n if (TRACKING_PARAM.test(key)) url.searchParams.delete(key)\n }\n url.hash = ''\n if (isStorageRequest(url)) url.searchParams.set('_fmt', imageFormatKey(request))\n // Cached HTML names this build's hashed chunks, and a deploy deletes them.\n // Keyed by build, old pages stop matching the moment the new Worker serves.\n url.searchParams.set('_build', buildId)\n return new Request(url.toString(), { method: 'GET' })\n}\n\n/** Only what the response itself declares shareable and fresh for a while. */\nexport function edgeCacheable(response: Response): boolean {\n if (response.status !== 200) return false\n const cc = response.headers.get('cache-control') ?? ''\n return /\\bpublic\\b/.test(cc) && /\\bs-maxage=[1-9]\\d*/.test(cc) && !/\\b(no-store|private)\\b/.test(cc)\n}\n\nfunction withHeader(response: Response, name: string, value: string): Response {\n const headers = new Headers(response.headers)\n headers.set(name, value)\n return new Response(response.body, { status: response.status, statusText: response.statusText, headers })\n}\n\nexport interface StorefrontEdgeOptions {\n /** The framework's own handler (TanStack Start's `handler.fetch`). */\n render(request: Request): Response | Promise<Response>\n /** From the store host: `host.isCanonicalHost`. */\n isCanonicalHost(url: URL): boolean\n /** One id per build, for the cache key. */\n buildId: string\n /** The Supabase project the photos live in (`PUBLIC_SUPABASE_URL`). */\n supabaseUrl?: string\n /** Keeps the Worker alive past the response (`waitUntil` from 'cloudflare:workers'). */\n waitUntil(promise: Promise<unknown>): void\n /** The Workers cache (`caches.default`). Omitted: no edge cache. */\n cache?: Cache\n /**\n * Development: NEVER cache. The Cloudflare vite plugin runs the Worker in\n * workerd locally too, and an entry cannot be purged, only outlived — a\n * price edited and reloaded shows the old one with nothing in the browser\n * able to help.\n */\n dev?: boolean\n}\n\n/** The Worker's `fetch`, from the store's pieces. */\nexport function createStorefrontEdge(options: StorefrontEdgeOptions): (request: Request) => Promise<Response> {\n async function respond(request: Request): Promise<Response> {\n const url = new URL(request.url)\n if (isStorageRequest(url)) return serveStorageImage(request, url, options.supabaseUrl)\n const response = await options.render(request)\n if (options.isCanonicalHost(url)) return response\n return withHeader(response, 'x-robots-tag', 'noindex, nofollow')\n }\n\n return async (request) => {\n // Analytics leaves through the store's own domain, never cached.\n if (isPostHogProxyPath(new URL(request.url).pathname)) return posthogProxy(request)\n if (options.dev || !options.cache) return respond(request)\n\n const key = edgeCacheKey(request, options.buildId)\n if (!key) return respond(request)\n const hit = await options.cache.match(key)\n if (hit) return withHeader(hit, 'x-edge-cache', 'HIT')\n\n const response = await respond(request)\n if (!edgeCacheable(response)) return withHeader(response, 'x-edge-cache', 'BYPASS')\n options.waitUntil(options.cache.put(key, response.clone()))\n return withHeader(response, 'x-edge-cache', 'MISS')\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/analytics/edge.ts","../src/host/edge-handler.ts"],"names":["response"],"mappings":"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","file":"edge.js","sourcesContent":["/**\n * The store's own door to PostHog, for a Worker (or any fetch-style) host.\n *\n * Events sent to the store's domain under `/ingest` are not dropped by ad\n * blockers the way requests to a known analytics host are, and the store needs\n * no PostHog setting of its own: every store forwards to the same project.\n *\n * if (url.pathname.startsWith('/ingest')) return posthogProxy(request)\n */\nexport interface PostHogProxyOptions {\n /** The path the storefront's analytics client sends to. Default `/ingest`. */\n prefix?: string\n /** PostHog's region. Default `us`. */\n region?: 'us' | 'eu'\n}\n\nexport function isPostHogProxyPath(pathname: string, prefix = '/ingest'): boolean {\n return pathname === prefix || pathname.startsWith(`${prefix}/`)\n}\n\nexport async function posthogProxy(request: Request, options: PostHogProxyOptions = {}): Promise<Response> {\n const prefix = options.prefix ?? '/ingest'\n const region = options.region ?? 'us'\n const url = new URL(request.url)\n const path = url.pathname.slice(prefix.length) || '/'\n // The SDK's script and its remote config live on the assets host.\n const host = path.startsWith('/static/') || path.startsWith('/array/')\n ? `${region}-assets.i.posthog.com`\n : `${region}.i.posthog.com`\n const target = new URL(`https://${host}${path}${url.search}`)\n\n const headers = new Headers(request.headers)\n headers.delete('cookie')\n headers.set('host', host)\n // PostHog geolocates from the visitor, not from the Worker.\n const ip = request.headers.get('cf-connecting-ip') ?? request.headers.get('x-forwarded-for')\n if (ip) headers.set('x-forwarded-for', ip)\n\n const init: RequestInit = { method: request.method, headers, redirect: 'manual' }\n if (request.method !== 'GET' && request.method !== 'HEAD') init.body = await request.arrayBuffer()\n const response = await fetch(target, init)\n const out = new Response(response.body, response)\n out.headers.delete('set-cookie')\n return out\n}\n","import { isPostHogProxyPath, posthogProxy } from '../analytics/edge'\nimport {\n EXPERIMENT_HEADER,\n NO_ASSIGNMENTS,\n assignmentCookie,\n readAssignmentCookie,\n encodeExperimentHeader,\n evaluateExperiments,\n experimentCacheSuffix,\n isBotUserAgent,\n readVisitorId,\n visitorCookie,\n type ExperimentAssignments,\n type StorefrontExperiment,\n} from '../experiments/assign'\n\n// ---------------------------------------------------------------------------\n// The Worker in front of a server-rendered store.\n//\n// Four jobs a store's request handler has before the framework renders\n// anything, first written for artorious-shop and the same for every store:\n//\n// 1. Analytics through the store's own domain (`/ingest` → PostHog), so ad\n// blockers that drop requests to a known analytics host do not drop it.\n// 2. The store's photos from the store's own origin. The bytes live in\n// Supabase Storage; a photo addressed at the storage project is a second\n// origin the browser pays DNS + TLS for (measured 156–241 ms TTFB against\n// 112–141 ms same-origin). Only public objects of the `shop-images`\n// bucket are forwarded — this is not a general proxy.\n// 3. A real edge cache. Public pages already leave with `Cache-Control:\n// public, s-maxage=…`, but on workers.dev nothing in front honours it, so\n// every view paid the full render. The Workers Cache API makes the header\n// mean what it says. Marketing parameters (utm_*, fbclid…) are dropped\n// from the key: ad traffic hits the same entry as everyone else.\n// 4. `X-Robots-Tag: noindex` on any host that is not the store's address, so\n// the deployment URL never competes with the domain.\n// 5. A/B assignment. With `experiments` set, every page request is assigned\n// its variants HERE, before render (see experiments/assign.ts), and the\n// edge cache is keyed by them — one entry per combination of variants,\n// so a cached page is never one visitor's variant served to another.\n// ---------------------------------------------------------------------------\n\n/** `/storage/v1/object/public/shop-images/…` or the `render/image` variant of it. */\nconst STORAGE_PATH = /^\\/storage\\/v1\\/(object|render\\/image)\\/public\\/(shop-images\\/[^?#]+)$/\n\n/** Query parameters the renderer understands. Anything else is dropped rather\n * than forwarded, so a crawler cannot mint unlimited distinct variants. */\nconst RENDER_PARAMS = new Set(['width', 'height', 'quality', 'format', 'resize'])\n\n/** A year: the object path carries a timestamp and is never overwritten. */\nconst IMMUTABLE = 'public, max-age=31536000, s-maxage=31536000, immutable'\n\nconst TRACKING_PARAM = /^(utm_[a-z0-9_]+|fbclid|gclid|gbraid|wbraid|msclkid|ttclid|igshid|mc_cid|mc_eid|_ga|ref)$/i\n\nexport function isStorageRequest(url: URL): boolean {\n return STORAGE_PATH.test(url.pathname)\n}\n\n/**\n * Which format this visitor gets — the only part of the request, besides the\n * URL, that changes the bytes. The renderer answers WebP or the original\n * depending on `accept` and declares no `Vary`, so every cache in front of it\n * has to put the answer in the key itself.\n */\nexport function imageFormatKey(request: Request): 'webp' | 'orig' {\n return /image\\/webp/.test(request.headers.get('accept') ?? '') ? 'webp' : 'orig'\n}\n\n/** Answer a storage request from Supabase, on the store's own origin. */\nexport async function serveStorageImage(request: Request, url: URL, supabaseUrl: string | undefined): Promise<Response> {\n const match = STORAGE_PATH.exec(url.pathname)\n if (!match) return new Response('Not found', { status: 404 })\n const base = supabaseUrl?.replace(/\\/$/, '')\n if (!base) return new Response('Storage is not configured', { status: 502 })\n\n const [, kind, objectPath] = match\n const upstream = new URL(`${base}/storage/v1/${kind}/public/${objectPath}`)\n for (const [key, value] of url.searchParams) {\n if (RENDER_PARAMS.has(key)) upstream.searchParams.set(key, value)\n }\n // A canonical `accept`, not the visitor's verbatim: two browsers that both\n // take WebP must produce the same upstream request.\n const webp = imageFormatKey(request) === 'webp'\n const response = await fetch(upstream.toString(), { headers: { accept: webp ? 'image/webp,image/*;q=0.8' : 'image/*' } })\n if (!response.ok) {\n // Upstream's status is passed through (a deleted photo must not look like\n // a 200), never its cache directives.\n return new Response(response.status === 404 ? 'Not found' : 'Upstream error', {\n status: response.status === 404 ? 404 : 502,\n headers: { 'cache-control': 'public, max-age=60' },\n })\n }\n const headers = new Headers()\n headers.set('content-type', response.headers.get('content-type') ?? 'image/jpeg')\n headers.set('cache-control', IMMUTABLE)\n headers.set('x-image-origin', 'supabase')\n headers.set('vary', 'Accept')\n return new Response(response.body, { status: 200, headers })\n}\n\n/** The cache key for a request, or null when it is not cacheable. `variant`\n * is the experiment part (`experimentCacheSuffix`); empty means control. */\nexport function edgeCacheKey(request: Request, buildId: string, variant = ''): Request | null {\n if (request.method !== 'GET') return null\n const url = new URL(request.url)\n for (const key of [...url.searchParams.keys()]) {\n if (TRACKING_PARAM.test(key)) url.searchParams.delete(key)\n }\n url.hash = ''\n if (isStorageRequest(url)) url.searchParams.set('_fmt', imageFormatKey(request))\n // Cached HTML names this build's hashed chunks, and a deploy deletes them.\n // Keyed by build, old pages stop matching the moment the new Worker serves.\n url.searchParams.set('_build', buildId)\n if (variant) url.searchParams.set('_exp', variant)\n return new Request(url.toString(), { method: 'GET' })\n}\n\n/** Only what the response itself declares shareable and fresh for a while. */\nexport function edgeCacheable(response: Response): boolean {\n if (response.status !== 200) return false\n const cc = response.headers.get('cache-control') ?? ''\n return /\\bpublic\\b/.test(cc) && /\\bs-maxage=[1-9]\\d*/.test(cc) && !/\\b(no-store|private)\\b/.test(cc)\n}\n\nfunction withHeader(response: Response, name: string, value: string): Response {\n const headers = new Headers(response.headers)\n headers.set(name, value)\n return new Response(response.body, { status: response.status, statusText: response.statusText, headers })\n}\n\n/** Adds cookies without touching the ones the page already sets. */\nfunction withCookies(response: Response, cookies: readonly string[]): Response {\n if (!cookies.length) return response\n const headers = new Headers(response.headers)\n for (const cookie of cookies) headers.append('set-cookie', cookie)\n return new Response(response.body, { status: response.status, statusText: response.statusText, headers })\n}\n\nexport interface StorefrontEdgeOptions {\n /** The framework's own handler (TanStack Start's `handler.fetch`). */\n render(request: Request): Response | Promise<Response>\n /** From the store host: `host.isCanonicalHost`. */\n isCanonicalHost(url: URL): boolean\n /** One id per build, for the cache key. */\n buildId: string\n /** The Supabase project the photos live in (`PUBLIC_SUPABASE_URL`). */\n supabaseUrl?: string\n /** Keeps the Worker alive past the response (`waitUntil` from 'cloudflare:workers'). */\n waitUntil(promise: Promise<unknown>): void\n /** The Workers cache (`caches.default`). Omitted: no edge cache. */\n cache?: Cache\n /**\n * Development: NEVER cache. The Cloudflare vite plugin runs the Worker in\n * workerd locally too, and an entry cannot be purged, only outlived — a\n * price edited and reloaded shows the old one with nothing in the browser\n * able to help.\n */\n dev?: boolean\n /**\n * A/B tests: the store's declared experiments (`config.experiments`) and the\n * PostHog project they run in. Omitted or empty: no assignment, no cookie,\n * the cache key is the page's alone — exactly the handler without tests.\n */\n experiments?: StorefrontEdgeExperiments\n}\n\nexport interface StorefrontEdgeExperiments {\n declared: Record<string, StorefrontExperiment> | undefined\n /** The PostHog project token (`phc_…`), the same the storefront sends with. */\n posthogKey: string | undefined\n /** PostHog's region. Default `us`. */\n region?: 'us' | 'eu'\n /** The flags request's deadline; past it every experiment is control. Default 400 ms. */\n timeoutMs?: number\n}\n\n/** A page a visitor navigates to — not an asset, a photo, a feed or a POST. */\nfunction isPageRequest(request: Request, url: URL): boolean {\n if (request.method !== 'GET') return false\n if (isStorageRequest(url) || isPostHogProxyPath(url.pathname)) return false\n // Hashed bundles, fonts, images, the sitemap: anything with an extension.\n if (/\\.[a-z0-9]{1,8}$/i.test(url.pathname)) return false\n // TanStack's server functions and the router's data requests.\n if (url.pathname.startsWith('/_serverFn') || url.pathname.startsWith('/_server')) return false\n return true\n}\n\ninterface PageAssignment {\n assignments: ExperimentAssignments\n /** The cache-key part; empty for control. */\n variant: string\n /** Cookies for THIS visitor's response: a new id, a fresh assignment. */\n cookies: string[]\n}\n\n/** Decide this request's variants. Never throws; every failure is control. */\nexport async function assignForRequest(\n request: Request,\n options: StorefrontEdgeExperiments,\n fetchImpl?: typeof fetch,\n): Promise<PageAssignment> {\n const declared = options.declared ?? {}\n const keys = Object.keys(declared)\n const none: PageAssignment = { assignments: NO_ASSIGNMENTS, variant: '', cookies: [] }\n if (!keys.length || !options.posthogKey) return none\n // Crawlers and unfurlers see control and get no id: what is indexed and\n // shared must not depend on a coin flip, and a bot is not an exposure.\n if (isBotUserAgent(request.headers.get('user-agent'))) return none\n const cookieHeader = request.headers.get('cookie')\n const secure = new URL(request.url).protocol === 'https:'\n const known = readVisitorId(cookieHeader, options.posthogKey)\n const distinctId = known ?? crypto.randomUUID()\n const cookies = known ? [] : [visitorCookie(distinctId, secure)]\n\n // Asked this half hour already: no round trip in front of the page.\n const remembered = known ? readAssignmentCookie(cookieHeader, { experiments: declared, distinctId }) : null\n if (remembered) return { assignments: remembered, variant: experimentCacheSuffix(remembered, keys), cookies }\n\n const assignments = await evaluateExperiments({\n experiments: declared,\n distinctId,\n posthogKey: options.posthogKey,\n apiHost: `https://${options.region ?? 'us'}.i.posthog.com`,\n timeoutMs: options.timeoutMs,\n fetch: fetchImpl,\n })\n // Only a real answer is remembered: after a timeout the next page asks again.\n const remember = assignmentCookie(assignments, declared, secure)\n return {\n assignments,\n variant: experimentCacheSuffix(assignments, keys),\n cookies: remember ? [...cookies, remember] : cookies,\n }\n}\n\n/**\n * The request the renderer sees: the same, plus the variants — and ONLY the\n * variants. The visitor id stays out of the header on purpose: the rendered\n * HTML is cached and shared by every visitor in the same variant bucket, and\n * an id in it would hand one visitor's identity to the next. The browser\n * reads its own id from the cookie (`readVisitorId(document.cookie, …)`).\n */\nfunction withAssignment(request: Request, assignments: ExperimentAssignments): Request {\n const headers = new Headers(request.headers)\n headers.delete(EXPERIMENT_HEADER) // never trust one the visitor sent\n if (Object.keys(assignments.flags).length) {\n headers.set(EXPERIMENT_HEADER, encodeExperimentHeader({ flags: assignments.flags }))\n }\n return new Request(request, { headers })\n}\n\n/** The Worker's `fetch`, from the store's pieces. */\nexport function createStorefrontEdge(options: StorefrontEdgeOptions): (request: Request) => Promise<Response> {\n async function respond(request: Request): Promise<Response> {\n const url = new URL(request.url)\n if (isStorageRequest(url)) return serveStorageImage(request, url, options.supabaseUrl)\n const response = await options.render(request)\n if (options.isCanonicalHost(url)) return response\n return withHeader(response, 'x-robots-tag', 'noindex, nofollow')\n }\n\n return async (incoming) => {\n const url = new URL(incoming.url)\n // Analytics leaves through the store's own domain, never cached.\n if (isPostHogProxyPath(url.pathname)) return posthogProxy(incoming)\n\n // Variants first: they change the HTML, so they are part of the cache key.\n const page = options.experiments && isPageRequest(incoming, url)\n ? await assignForRequest(incoming, options.experiments)\n : null\n const request = page ? withAssignment(incoming, page.assignments) : incoming\n // Cookies are added to what THIS visitor receives, never to the cached\n // copy (a shared entry carrying one visitor's id would hand it to the\n // next), and appended, so a cookie the page itself sets survives.\n const finish = (response: Response) => withCookies(response, page?.cookies ?? [])\n\n if (options.dev || !options.cache) return finish(await respond(request))\n\n const key = edgeCacheKey(request, options.buildId, page?.variant ?? '')\n if (!key) return finish(await respond(request))\n const hit = await options.cache.match(key)\n if (hit) return finish(withHeader(hit, 'x-edge-cache', 'HIT'))\n\n const response = await respond(request)\n if (!edgeCacheable(response)) return finish(withHeader(response, 'x-edge-cache', 'BYPASS'))\n options.waitUntil(options.cache.put(key, response.clone()))\n return finish(withHeader(response, 'x-edge-cache', 'MISS'))\n }\n}\n"]}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/** The variant every visitor gets when nothing else can be decided. */
|
|
2
|
+
export declare const CONTROL = "control";
|
|
3
|
+
/** Request header the edge hands the assignment to the renderer in. */
|
|
4
|
+
export declare const EXPERIMENT_HEADER = "x-fayz-experiments";
|
|
5
|
+
/** First-party cookie holding the anonymous id when PostHog has not set one yet. */
|
|
6
|
+
export declare const VISITOR_COOKIE = "fz_vid";
|
|
7
|
+
/**
|
|
8
|
+
* First-party cookie remembering what PostHog answered for this visitor, so
|
|
9
|
+
* the edge asks once per visitor per half hour instead of before every page —
|
|
10
|
+
* a flags round trip in front of every navigation, cache hits included, is a
|
|
11
|
+
* tax every shopper pays for a test most of them are not looking at.
|
|
12
|
+
*/
|
|
13
|
+
export declare const ASSIGNMENT_COOKIE = "fz_exp";
|
|
14
|
+
/** How long a remembered assignment is trusted. Stopping or re-rolling an
|
|
15
|
+
* experiment in PostHog reaches every visitor within this window. */
|
|
16
|
+
export declare const ASSIGNMENT_TTL_SECONDS = 1800;
|
|
17
|
+
/** One experiment the store runs. */
|
|
18
|
+
export interface StorefrontExperiment {
|
|
19
|
+
/**
|
|
20
|
+
* The variant keys, as configured on the PostHog flag. Must include
|
|
21
|
+
* 'control'. A variant PostHog answers that is not listed here is treated
|
|
22
|
+
* as control: the page has no rendering for it.
|
|
23
|
+
*/
|
|
24
|
+
variants: readonly string[];
|
|
25
|
+
/** What is being tested and why — for whoever reads the config next. */
|
|
26
|
+
description?: string;
|
|
27
|
+
}
|
|
28
|
+
/** What a visitor sees for one experiment. */
|
|
29
|
+
export interface ExperimentAssignment {
|
|
30
|
+
variant: string;
|
|
31
|
+
/** The flag's JSON payload for this variant, when the flag has one. */
|
|
32
|
+
payload?: unknown;
|
|
33
|
+
}
|
|
34
|
+
/** Everything the edge decided for one request. */
|
|
35
|
+
export interface ExperimentAssignments {
|
|
36
|
+
/** The id the variants were computed for. Kept at the edge: it never
|
|
37
|
+
* travels into the rendered (and cached) page — see `withAssignment`. */
|
|
38
|
+
distinctId?: string;
|
|
39
|
+
/**
|
|
40
|
+
* By flag key, ONLY what PostHog actually assigned — 'control' included when
|
|
41
|
+
* PostHog said control. A key the store declares but that is missing here is
|
|
42
|
+
* rendered as control but is not part of the experiment: it reports no
|
|
43
|
+
* exposure, so a timeout never lands a visitor in the control group.
|
|
44
|
+
*/
|
|
45
|
+
flags: Record<string, ExperimentAssignment>;
|
|
46
|
+
/** PostHog answered (as opposed to: not asked, slow, down, unparsable). */
|
|
47
|
+
answered?: boolean;
|
|
48
|
+
}
|
|
49
|
+
/** The empty decision: every experiment at control. */
|
|
50
|
+
export declare const NO_ASSIGNMENTS: ExperimentAssignments;
|
|
51
|
+
/**
|
|
52
|
+
* Problems with a store's experiment declarations, empty when valid. Checked
|
|
53
|
+
* when the host is created, so a typo fails the build rather than silently
|
|
54
|
+
* running a page with no variants.
|
|
55
|
+
*/
|
|
56
|
+
export declare function validateExperiments(experiments: Record<string, StorefrontExperiment> | undefined): string[];
|
|
57
|
+
export declare function isBotUserAgent(userAgent: string | null | undefined): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* The visitor's stable anonymous id, if the browser already has one:
|
|
60
|
+
* posthog-js's own cookie first (so the edge evaluates flags for the very id
|
|
61
|
+
* the browser reports events under), then the store's first-party cookie.
|
|
62
|
+
*/
|
|
63
|
+
export declare function readVisitorId(cookieHeader: string | null | undefined, posthogKey: string): string | undefined;
|
|
64
|
+
/** `Set-Cookie` for a newly minted visitor id: a year, first-party, not readable cross-site. */
|
|
65
|
+
export declare function visitorCookie(id: string, secure?: boolean): string;
|
|
66
|
+
export interface EvaluateExperimentsInput {
|
|
67
|
+
/** The store's declared experiments. Only these keys are read from the answer. */
|
|
68
|
+
experiments: Record<string, StorefrontExperiment>;
|
|
69
|
+
distinctId: string;
|
|
70
|
+
/** The PostHog project token (`phc_…`). */
|
|
71
|
+
posthogKey: string;
|
|
72
|
+
/** Default `https://us.i.posthog.com`. */
|
|
73
|
+
apiHost?: string;
|
|
74
|
+
/** PostHog is asked with a deadline; past it, every experiment is control. Default 400 ms. */
|
|
75
|
+
timeoutMs?: number;
|
|
76
|
+
fetch?: typeof fetch;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Ask PostHog which variant this visitor is in, for every declared experiment.
|
|
80
|
+
* Never throws and never waits past the deadline.
|
|
81
|
+
*/
|
|
82
|
+
export declare function evaluateExperiments(input: EvaluateExperimentsInput): Promise<ExperimentAssignments>;
|
|
83
|
+
/** The variant of `key`, control when unassigned. */
|
|
84
|
+
export declare function variantOf(assignments: ExperimentAssignments | null | undefined, key: string): string;
|
|
85
|
+
/**
|
|
86
|
+
* The part of the edge cache key that depends on the visitor: one entry per
|
|
87
|
+
* combination of variants, in a fixed order. Control-only visitors (and every
|
|
88
|
+
* crawler) share the plain key with the page as it was before any experiment.
|
|
89
|
+
*/
|
|
90
|
+
export declare function experimentCacheSuffix(assignments: ExperimentAssignments, keys: readonly string[]): string;
|
|
91
|
+
/** The assignment as a header value (URI-encoded JSON: payloads may carry any text). */
|
|
92
|
+
export declare function encodeExperimentHeader(assignments: ExperimentAssignments): string;
|
|
93
|
+
/** The renderer's side: never throws, garbage reads as no assignment. */
|
|
94
|
+
export declare function decodeExperimentHeader(value: string | null | undefined): ExperimentAssignments | null;
|
|
95
|
+
/**
|
|
96
|
+
* A short, stable fingerprint of the declared experiments. A remembered
|
|
97
|
+
* assignment is only trusted for the same declarations: adding an experiment
|
|
98
|
+
* or a variant re-asks PostHog on the next page instead of waiting out the TTL.
|
|
99
|
+
*/
|
|
100
|
+
export declare function experimentsFingerprint(experiments: Record<string, StorefrontExperiment>): string;
|
|
101
|
+
/**
|
|
102
|
+
* The remembered assignment, when it is for this visitor and these
|
|
103
|
+
* declarations. Tampering only lets a visitor pick a variant the store
|
|
104
|
+
* declared — the same page any other visitor may get — so it is validated, not
|
|
105
|
+
* signed; anything that does not validate is ignored and PostHog is asked.
|
|
106
|
+
*/
|
|
107
|
+
export declare function readAssignmentCookie(cookieHeader: string | null | undefined, input: {
|
|
108
|
+
experiments: Record<string, StorefrontExperiment>;
|
|
109
|
+
distinctId: string;
|
|
110
|
+
}): ExperimentAssignments | null;
|
|
111
|
+
/** `Set-Cookie` remembering an assignment PostHog made. Null when there is nothing worth remembering. */
|
|
112
|
+
export declare function assignmentCookie(assignments: ExperimentAssignments, experiments: Record<string, StorefrontExperiment>, secure?: boolean): string | null;
|
|
113
|
+
//# sourceMappingURL=assign.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assign.d.ts","sourceRoot":"","sources":["../../src/experiments/assign.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type BlockNode } from '@fayz-ai/core';
|
|
3
|
+
import { type ExperimentAssignment, type ExperimentAssignments } from './assign';
|
|
4
|
+
/** A setting whose value depends on an experiment's variant. */
|
|
5
|
+
export interface ExperimentBinding<T> {
|
|
6
|
+
/** The PostHog flag key, declared in the store's `experiments`. */
|
|
7
|
+
experiment: string;
|
|
8
|
+
/** The value for each variant; a variant not listed falls back to `control`. */
|
|
9
|
+
variants: {
|
|
10
|
+
control: T;
|
|
11
|
+
} & Record<string, T>;
|
|
12
|
+
}
|
|
13
|
+
/** A plain value, or one bound to an experiment. */
|
|
14
|
+
export type Experimental<T> = T | ExperimentBinding<T>;
|
|
15
|
+
export declare function isExperimentBinding<T>(value: Experimental<T> | undefined): value is ExperimentBinding<T>;
|
|
16
|
+
/** The value of a setting for a given variant (control when unbound or unmatched). */
|
|
17
|
+
export declare function resolveExperimental<T>(value: Experimental<T>, variant: string): T;
|
|
18
|
+
/** The assignment the page was rendered with, for the analytics client. */
|
|
19
|
+
export declare function getExperimentBootstrap(): ExperimentAssignments | null;
|
|
20
|
+
export interface ExperimentProviderProps {
|
|
21
|
+
/** What the edge decided for this request (`StoreRouteData.experiments`). */
|
|
22
|
+
assignments: ExperimentAssignments | null | undefined;
|
|
23
|
+
/** The page being shown. A new path is a new page view: exposures are reported again. */
|
|
24
|
+
path: string;
|
|
25
|
+
children?: React.ReactNode;
|
|
26
|
+
}
|
|
27
|
+
export declare function ExperimentProvider({ assignments, path, children }: ExperimentProviderProps): React.JSX.Element;
|
|
28
|
+
/**
|
|
29
|
+
* The variant this visitor sees for `key`, and its payload.
|
|
30
|
+
*
|
|
31
|
+
* 'control' whenever anything is missing — no provider, an undeclared key, no
|
|
32
|
+
* assignment, PostHog unreachable at the edge. The first render that reads an
|
|
33
|
+
* experiment PostHog assigned reports the exposure (once per page view): that
|
|
34
|
+
* is the event PostHog's experiment analysis counts visitors from.
|
|
35
|
+
*/
|
|
36
|
+
export declare function useExperiment(key: string): ExperimentAssignment;
|
|
37
|
+
/** A setting resolved for this visitor; reads (and reports) the experiment only when bound. */
|
|
38
|
+
export declare function useExperimentValue<T>(value: Experimental<T>): T;
|
|
39
|
+
export declare function useExperimentValue<T>(value: Experimental<T> | undefined): T | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* The `experiment` block: its children are `experimentVariant` blocks, and
|
|
42
|
+
* only the one named after this visitor's variant renders (the `control` one
|
|
43
|
+
* when nothing matches). How a document tests a section without code:
|
|
44
|
+
*
|
|
45
|
+
* { type: 'experiment', props: { experiment: 'home-hero-copy' }, children: [
|
|
46
|
+
* { type: 'experimentVariant', props: { variant: 'control' }, children: [hero A] },
|
|
47
|
+
* { type: 'experimentVariant', props: { variant: 'test' }, children: [hero B] },
|
|
48
|
+
* ] }
|
|
49
|
+
*/
|
|
50
|
+
export declare function ExperimentBlock({ experiment, node }: {
|
|
51
|
+
experiment?: string;
|
|
52
|
+
node?: BlockNode;
|
|
53
|
+
}): React.JSX.Element | null;
|
|
54
|
+
/** One variant's blocks. Renders its children; only meaningful inside `experiment`. */
|
|
55
|
+
export declare function ExperimentVariantBlock({ node }: {
|
|
56
|
+
variant?: string;
|
|
57
|
+
node?: BlockNode;
|
|
58
|
+
}): React.JSX.Element;
|
|
59
|
+
export declare const experimentBlockMeta: {
|
|
60
|
+
label: string;
|
|
61
|
+
description: string;
|
|
62
|
+
settings: {
|
|
63
|
+
id: string;
|
|
64
|
+
type: "text";
|
|
65
|
+
label: string;
|
|
66
|
+
required: boolean;
|
|
67
|
+
help: string;
|
|
68
|
+
}[];
|
|
69
|
+
presets: {
|
|
70
|
+
name: string;
|
|
71
|
+
props: {
|
|
72
|
+
experiment: string;
|
|
73
|
+
};
|
|
74
|
+
}[];
|
|
75
|
+
enabledOn: string[];
|
|
76
|
+
};
|
|
77
|
+
export declare const experimentVariantBlockMeta: {
|
|
78
|
+
label: string;
|
|
79
|
+
description: string;
|
|
80
|
+
parent: string[];
|
|
81
|
+
settings: {
|
|
82
|
+
id: string;
|
|
83
|
+
type: "text";
|
|
84
|
+
label: string;
|
|
85
|
+
required: boolean;
|
|
86
|
+
help: string;
|
|
87
|
+
}[];
|
|
88
|
+
presets: {
|
|
89
|
+
name: string;
|
|
90
|
+
props: {
|
|
91
|
+
variant: string;
|
|
92
|
+
};
|
|
93
|
+
}[];
|
|
94
|
+
enabledOn: string[];
|
|
95
|
+
};
|
|
96
|
+
/** Test seam. */
|
|
97
|
+
export declare function resetExperimentBootstrap(): void;
|
|
98
|
+
//# sourceMappingURL=react.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../../src/experiments/react.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,EAA8C,KAAK,SAAS,EAAE,MAAM,eAAe,CAAA;AAG1F,OAAO,EAAW,KAAK,oBAAoB,EAAE,KAAK,qBAAqB,EAAE,MAAM,UAAU,CAAA;AAazF,gEAAgE;AAChE,MAAM,WAAW,iBAAiB,CAAC,CAAC;IAClC,mEAAmE;IACnE,UAAU,EAAE,MAAM,CAAA;IAClB,gFAAgF;IAChF,QAAQ,EAAE;QAAE,OAAO,EAAE,CAAC,CAAA;KAAE,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAA;CAC7C;AAED,oDAAoD;AACpD,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,CAAC,GAAG,iBAAiB,CAAC,CAAC,CAAC,CAAA;AAEtD,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS,GAAG,KAAK,IAAI,iBAAiB,CAAC,CAAC,CAAC,CAOxG;AAED,sFAAsF;AACtF,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,CAAC,CAGjF;AAOD,2EAA2E;AAC3E,wBAAgB,sBAAsB,IAAI,qBAAqB,GAAG,IAAI,CAErE;AAYD,MAAM,WAAW,uBAAuB;IACtC,6EAA6E;IAC7E,WAAW,EAAE,qBAAqB,GAAG,IAAI,GAAG,SAAS,CAAA;IACrD,yFAAyF;IACzF,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC3B;AAED,wBAAgB,kBAAkB,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,uBAAuB,qBAa1F;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,oBAAoB,CAgB/D;AAED,+FAA+F;AAC/F,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;AAChE,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS,GAAG,CAAC,GAAG,SAAS,CAAA;AAWxF;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,SAAS,CAAA;CAAE,4BAU9F;AAED,uFAAuF;AACvF,wBAAgB,sBAAsB,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,SAAS,CAAA;CAAE,qBAEtF;AAED,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;CAe/B,CAAA;AAED,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;CAkBtC,CAAA;AAED,iBAAiB;AACjB,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ExperimentAssignments, type StorefrontExperiment } from '../experiments/assign';
|
|
1
2
|
export declare function isStorageRequest(url: URL): boolean;
|
|
2
3
|
/**
|
|
3
4
|
* Which format this visitor gets — the only part of the request, besides the
|
|
@@ -8,8 +9,9 @@ export declare function isStorageRequest(url: URL): boolean;
|
|
|
8
9
|
export declare function imageFormatKey(request: Request): 'webp' | 'orig';
|
|
9
10
|
/** Answer a storage request from Supabase, on the store's own origin. */
|
|
10
11
|
export declare function serveStorageImage(request: Request, url: URL, supabaseUrl: string | undefined): Promise<Response>;
|
|
11
|
-
/** The cache key for a request, or null when it is not cacheable.
|
|
12
|
-
|
|
12
|
+
/** The cache key for a request, or null when it is not cacheable. `variant`
|
|
13
|
+
* is the experiment part (`experimentCacheSuffix`); empty means control. */
|
|
14
|
+
export declare function edgeCacheKey(request: Request, buildId: string, variant?: string): Request | null;
|
|
13
15
|
/** Only what the response itself declares shareable and fresh for a while. */
|
|
14
16
|
export declare function edgeCacheable(response: Response): boolean;
|
|
15
17
|
export interface StorefrontEdgeOptions {
|
|
@@ -32,7 +34,32 @@ export interface StorefrontEdgeOptions {
|
|
|
32
34
|
* able to help.
|
|
33
35
|
*/
|
|
34
36
|
dev?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* A/B tests: the store's declared experiments (`config.experiments`) and the
|
|
39
|
+
* PostHog project they run in. Omitted or empty: no assignment, no cookie,
|
|
40
|
+
* the cache key is the page's alone — exactly the handler without tests.
|
|
41
|
+
*/
|
|
42
|
+
experiments?: StorefrontEdgeExperiments;
|
|
43
|
+
}
|
|
44
|
+
export interface StorefrontEdgeExperiments {
|
|
45
|
+
declared: Record<string, StorefrontExperiment> | undefined;
|
|
46
|
+
/** The PostHog project token (`phc_…`), the same the storefront sends with. */
|
|
47
|
+
posthogKey: string | undefined;
|
|
48
|
+
/** PostHog's region. Default `us`. */
|
|
49
|
+
region?: 'us' | 'eu';
|
|
50
|
+
/** The flags request's deadline; past it every experiment is control. Default 400 ms. */
|
|
51
|
+
timeoutMs?: number;
|
|
52
|
+
}
|
|
53
|
+
interface PageAssignment {
|
|
54
|
+
assignments: ExperimentAssignments;
|
|
55
|
+
/** The cache-key part; empty for control. */
|
|
56
|
+
variant: string;
|
|
57
|
+
/** Cookies for THIS visitor's response: a new id, a fresh assignment. */
|
|
58
|
+
cookies: string[];
|
|
35
59
|
}
|
|
60
|
+
/** Decide this request's variants. Never throws; every failure is control. */
|
|
61
|
+
export declare function assignForRequest(request: Request, options: StorefrontEdgeExperiments, fetchImpl?: typeof fetch): Promise<PageAssignment>;
|
|
36
62
|
/** The Worker's `fetch`, from the store's pieces. */
|
|
37
63
|
export declare function createStorefrontEdge(options: StorefrontEdgeOptions): (request: Request) => Promise<Response>;
|
|
64
|
+
export {};
|
|
38
65
|
//# sourceMappingURL=edge-handler.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"edge-handler.d.ts","sourceRoot":"","sources":["../../src/host/edge-handler.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"edge-handler.d.ts","sourceRoot":"","sources":["../../src/host/edge-handler.ts"],"names":[],"mappings":"AACA,OAAO,EAWL,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EAC1B,MAAM,uBAAuB,CAAA;AAwC9B,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAElD;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,CAEhE;AAED,yEAAyE;AACzE,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,QAAQ,CAAC,CA6BtH;AAED;6EAC6E;AAC7E,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,SAAK,GAAG,OAAO,GAAG,IAAI,CAa5F;AAED,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAIzD;AAgBD,MAAM,WAAW,qBAAqB;IACpC,sEAAsE;IACtE,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;IACtD,mDAAmD;IACnD,eAAe,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAA;IAClC,2CAA2C;IAC3C,OAAO,EAAE,MAAM,CAAA;IACf,uEAAuE;IACvE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,wFAAwF;IACxF,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAA;IAC1C,oEAAoE;IACpE,KAAK,CAAC,EAAE,KAAK,CAAA;IACb;;;;;OAKG;IACH,GAAG,CAAC,EAAE,OAAO,CAAA;IACb;;;;OAIG;IACH,WAAW,CAAC,EAAE,yBAAyB,CAAA;CACxC;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,GAAG,SAAS,CAAA;IAC1D,+EAA+E;IAC/E,UAAU,EAAE,MAAM,GAAG,SAAS,CAAA;IAC9B,sCAAsC;IACtC,MAAM,CAAC,EAAE,IAAI,GAAG,IAAI,CAAA;IACpB,yFAAyF;IACzF,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAaD,UAAU,cAAc;IACtB,WAAW,EAAE,qBAAqB,CAAA;IAClC,6CAA6C;IAC7C,OAAO,EAAE,MAAM,CAAA;IACf,yEAAyE;IACzE,OAAO,EAAE,MAAM,EAAE,CAAA;CAClB;AAED,8EAA8E;AAC9E,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,yBAAyB,EAClC,SAAS,CAAC,EAAE,OAAO,KAAK,GACvB,OAAO,CAAC,cAAc,CAAC,CAiCzB;AAkBD,qDAAqD;AACrD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAoC5G"}
|
|
@@ -3,6 +3,7 @@ import type { Product, ServiceHours } from '@fayz-ai/shop/types';
|
|
|
3
3
|
import { type ResolvedStorefrontConfig, type StorefrontConfig } from '../document/config';
|
|
4
4
|
import { type PageDataEntry } from '../blocks/page-data';
|
|
5
5
|
import { type MenuBusiness } from '../lib/seo';
|
|
6
|
+
import { type ExperimentAssignments } from '../experiments/assign';
|
|
6
7
|
/** One read the page declares, keyed as `useLoader` asks for it. */
|
|
7
8
|
export interface HostDataNeed {
|
|
8
9
|
key: string;
|
|
@@ -75,6 +76,13 @@ export interface StoreRouteData<Extra = unknown> {
|
|
|
75
76
|
missing: boolean;
|
|
76
77
|
/** Public origin of the request, for canonical and OpenGraph URLs. */
|
|
77
78
|
origin: string;
|
|
79
|
+
/**
|
|
80
|
+
* The variants the edge assigned this request (`createStorefrontEdge`'s
|
|
81
|
+
* `experiments`), read from the `x-fayz-experiments` header by the route.
|
|
82
|
+
* Variants only — never the visitor id: this is serialised into HTML the
|
|
83
|
+
* edge caches per variant bucket. Absent: every experiment is control.
|
|
84
|
+
*/
|
|
85
|
+
experiments?: ExperimentAssignments;
|
|
78
86
|
}
|
|
79
87
|
export interface StoreHeadContext {
|
|
80
88
|
config: ResolvedStorefrontConfig;
|
|
@@ -101,7 +109,9 @@ export interface StoreHost<Extra = unknown> {
|
|
|
101
109
|
config: ResolvedStorefrontConfig;
|
|
102
110
|
/** Where a path now lives, or null. */
|
|
103
111
|
redirectFor(path: string): string | null;
|
|
104
|
-
loadRoute(path: string, origin: string
|
|
112
|
+
loadRoute(path: string, origin: string, request?: {
|
|
113
|
+
experiments?: ExperimentAssignments | null;
|
|
114
|
+
}): Promise<StoreRouteData<Extra>>;
|
|
105
115
|
headFor(data: StoreRouteData<Extra> | undefined): StoreHead;
|
|
106
116
|
cacheControlFor(path: string): string;
|
|
107
117
|
/** Public pages render on the server; per-shopper ones only in the browser. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store-host.d.ts","sourceRoot":"","sources":["../../src/host/store-host.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgC,KAAK,SAAS,EAAE,KAAK,UAAU,EAAE,MAAM,eAAe,CAAA;AAE7F,OAAO,KAAK,EAAY,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAC1E,OAAO,EAAiB,KAAK,wBAAwB,EAAE,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAGxG,OAAO,EAAyC,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAG/F,OAAO,EAAgC,KAAK,YAAY,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"store-host.d.ts","sourceRoot":"","sources":["../../src/host/store-host.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgC,KAAK,SAAS,EAAE,KAAK,UAAU,EAAE,MAAM,eAAe,CAAA;AAE7F,OAAO,KAAK,EAAY,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAC1E,OAAO,EAAiB,KAAK,wBAAwB,EAAE,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAGxG,OAAO,EAAyC,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAG/F,OAAO,EAAgC,KAAK,YAAY,EAAE,MAAM,YAAY,CAAA;AAE5E,OAAO,EAAuB,KAAK,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAiBvF,oEAAoE;AACpE,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAChC;AAED,mEAAmE;AACnE,MAAM,WAAW,kBAAkB,CAAC,KAAK,GAAG,OAAO;IACjD,oFAAoF;IACpF,KAAK,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,EAAE,CAAA;IACpC,sFAAsF;IACtF,IAAI,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,CAAA;IAC1C,sEAAsE;IACtE,OAAO,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,GAAG,IAAI,GAAG,OAAO,CAAA;IACpD,uEAAuE;IACvE,IAAI,CAAC,CAAC,IAAI,EAAE,cAAc,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,gBAAgB,GAAG,SAAS,GAAG,IAAI,CAAA;IAC3E,yDAAyD;IACzD,MAAM,CAAC,CAAC,IAAI,EAAE,cAAc,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,gBAAgB,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;IAC3F,qEAAqE;IACrE,YAAY,CAAC,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;CACnC;AAED,MAAM,WAAW,gBAAgB,CAAC,KAAK,GAAG,OAAO;IAC/C,MAAM,EAAE,gBAAgB,GAAG,wBAAwB,CAAA;IACnD;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC/D,2EAA2E;IAC3E,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,CAAA;IAChC;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAClC,8BAA8B;IAC9B,MAAM,CAAC,EAAE,kBAAkB,CAAC,KAAK,CAAC,CAAA;CACnC;AAED,4EAA4E;AAC5E,MAAM,WAAW,cAAc,CAAC,KAAK,GAAG,OAAO;IAC7C,IAAI,EAAE,MAAM,CAAA;IACZ,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;IACtC,oEAAoE;IACpE,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IACvB,4DAA4D;IAC5D,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;IACnB,wFAAwF;IACxF,KAAK,EAAE,YAAY,EAAE,CAAA;IACrB,kBAAkB;IAClB,OAAO,EAAE,OAAO,CAAA;IAChB,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAA;IACd;;;;;OAKG;IACH,WAAW,CAAC,EAAE,qBAAqB,CAAA;CACpC;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,wBAAwB,CAAA;IAChC,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;CACf;AAED,sEAAsE;AACtE,MAAM,WAAW,SAAS;IACxB,qGAAqG;IACrG,IAAI,EAAE,KAAK,CAAC;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;IACtG,KAAK,EAAE,UAAU,EAAE,CAAA;IACnB,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACnD;AAED,MAAM,WAAW,SAAS,CAAC,KAAK,GAAG,OAAO;IACxC,MAAM,EAAE,wBAAwB,CAAA;IAChC,uCAAuC;IACvC,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;IACxC,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,qBAAqB,GAAG,IAAI,CAAA;KAAE,GAAG,OAAO,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAA;IACjI,OAAO,CAAC,IAAI,EAAE,cAAc,CAAC,KAAK,CAAC,GAAG,SAAS,GAAG,SAAS,CAAA;IAC3D,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;IACrC,+EAA+E;IAC/E,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;IACvC,eAAe,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAA;IAClC,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAC3C,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAAA;CAC5C;AAKD,eAAO,MAAM,YAAY,iEAAiE,CAAA;AAC1F,eAAO,MAAM,aAAa,uCAAuC,CAAA;AAIjE,iFAAiF;AACjF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,GAAG,MAAM,EAAE,CAIzE;AAUD;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,wBAAwB,EAChC,KAAK,EAAE;IAAE,KAAK,EAAE,SAAS,YAAY,EAAE,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,CAAA;CAAE,GAChH,YAAY,CAoBd;AAED,4EAA4E;AAC5E,wBAAgB,UAAU,CACxB,MAAM,EAAE,wBAAwB,EAChC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,YAAY,GACrB,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAsBhC;AAED,wBAAgB,eAAe,CAAC,KAAK,GAAG,OAAO,EAAE,OAAO,EAAE,gBAAgB,CAAC,KAAK,CAAC,GAAG,SAAS,CAAC,KAAK,CAAC,CAmKnG"}
|
package/dist/host.d.ts
CHANGED
|
@@ -4,4 +4,6 @@ export { withImagesOnOrigin, serveShopImagesFromOrigin, onOrigin } from './host/
|
|
|
4
4
|
export { menuDataNeeds } from './data/menu-needs';
|
|
5
5
|
export { buildSitemapXml, buildRobotsTxt } from './app/sitemap';
|
|
6
6
|
export type { MenuBusiness } from './lib/seo';
|
|
7
|
+
export { decodeExperimentHeader, EXPERIMENT_HEADER } from './experiments/assign';
|
|
8
|
+
export type { ExperimentAssignments, StorefrontExperiment } from './experiments/assign';
|
|
7
9
|
//# sourceMappingURL=host.d.ts.map
|
package/dist/host.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,eAAe,EACf,YAAY,EACZ,UAAU,EACV,gBAAgB,EAChB,YAAY,EACZ,aAAa,GACd,MAAM,mBAAmB,CAAA;AAC1B,YAAY,EACV,SAAS,EACT,gBAAgB,EAChB,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,SAAS,EACT,YAAY,GACb,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,kBAAkB,EAAE,yBAAyB,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAC7F,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACjD,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAC/D,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA"}
|
|
1
|
+
{"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,eAAe,EACf,YAAY,EACZ,UAAU,EACV,gBAAgB,EAChB,YAAY,EACZ,aAAa,GACd,MAAM,mBAAmB,CAAA;AAC1B,YAAY,EACV,SAAS,EACT,gBAAgB,EAChB,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,SAAS,EACT,YAAY,GACb,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,kBAAkB,EAAE,yBAAyB,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAC7F,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACjD,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAC/D,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAG7C,OAAO,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AAChF,YAAY,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA"}
|
package/dist/host.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { buildRobotsTxt, buildSitemapXml } from './chunk-WCAQRATH.js';
|
|
2
2
|
export { buildRobotsTxt, buildSitemapXml } from './chunk-WCAQRATH.js';
|
|
3
|
-
import { resolveConfig, storefrontPaths, isPublicPath, matchPath, collectDataNeeds, menuDataNeeds, resolvePageDataNeed, isKnownPath, productSeo, menuSeo, pageSeo, toBlockNodes, defaultPageBlocks } from './chunk-
|
|
4
|
-
export { menuDataNeeds } from './chunk-
|
|
3
|
+
import { resolveConfig, storefrontPaths, isPublicPath, matchPath, collectDataNeeds, menuDataNeeds, resolvePageDataNeed, isKnownPath, productSeo, menuSeo, pageSeo, toBlockNodes, defaultPageBlocks } from './chunk-UJHCVRK3.js';
|
|
4
|
+
export { menuDataNeeds } from './chunk-UJHCVRK3.js';
|
|
5
|
+
import { validateExperiments } from './chunk-746BEU7H.js';
|
|
6
|
+
export { EXPERIMENT_HEADER, decodeExperimentHeader } from './chunk-746BEU7H.js';
|
|
5
7
|
import { buildSeoTags } from '@fayz-ai/core';
|
|
6
8
|
import { getShopProvider, setShopProvider } from '@fayz-ai/shop/runtime';
|
|
7
9
|
|
|
@@ -66,6 +68,9 @@ function createStoreHost(options) {
|
|
|
66
68
|
const menuPath = config.delivery?.menuPath;
|
|
67
69
|
const redirects = options.redirects ?? (menuPath ? Object.fromEntries(["/catalog", "/menu"].filter((p) => p !== menuPath).map((p) => [p, menuPath])) : {});
|
|
68
70
|
const extend = options.extend ?? {};
|
|
71
|
+
const experimentIssues = validateExperiments(config.experiments);
|
|
72
|
+
if (experimentIssues.length) throw new Error(`Invalid storefront experiments:
|
|
73
|
+
- ${experimentIssues.join("\n- ")}`);
|
|
69
74
|
const canonicalHost = options.canonicalOrigin ? new URL(options.canonicalOrigin).hostname : null;
|
|
70
75
|
const isMenu = (path) => Boolean(menuPath && matchPath(menuPath, path));
|
|
71
76
|
function treeFor(path) {
|
|
@@ -89,7 +94,7 @@ function createStoreHost(options) {
|
|
|
89
94
|
return [];
|
|
90
95
|
}
|
|
91
96
|
}
|
|
92
|
-
async function loadRoute(path, origin) {
|
|
97
|
+
async function loadRoute(path, origin, request) {
|
|
93
98
|
const params = matchPath("/product/:slug", path) ?? {};
|
|
94
99
|
const needs = [
|
|
95
100
|
...collectDataNeeds(treeFor(path), params),
|
|
@@ -108,7 +113,8 @@ function createStoreHost(options) {
|
|
|
108
113
|
hoursFor(path)
|
|
109
114
|
]);
|
|
110
115
|
const missing = !isKnownPath(config, path) || Boolean(matchPath("/product/:slug", path)) && product === null || Boolean(extend.missing?.(path, extra));
|
|
111
|
-
|
|
116
|
+
const flags = request?.experiments?.flags;
|
|
117
|
+
return { path, entries, product, extra, hours, missing, origin, ...flags && Object.keys(flags).length ? { experiments: { flags } } : {} };
|
|
112
118
|
}
|
|
113
119
|
function menuFrom(data) {
|
|
114
120
|
const read = (loader) => {
|