@tokenoftrust/storefront-runner 2.2.9 → 2.2.10
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/apps/storefront/public/shared/commerce-chrome.css +1 -1
- package/apps/storefront/src/lib/rawChrome.ts +1 -1
- package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +3 -3
- package/package.json +1 -1
- package/packages/public-runtime/src/integration/ga4-sandbox-schedule.ts +131 -0
- package/packages/public-runtime/src/integration/lifecycle-boot.ts +32 -12
- package/packages/public-runtime/src/integration/native-ga4-sandbox.ts +5 -4
- package/packages/public-runtime/src/integration/post-paint.ts +8 -9
|
@@ -322,7 +322,7 @@ a.article-card__tag:hover { text-decoration: underline; }
|
|
|
322
322
|
.footer-brand .brand .b-prime,
|
|
323
323
|
.footer-brand .brand .b-vapor,
|
|
324
324
|
.footer-brand .brand .b-club { color: var(--color-primary-contrast); }
|
|
325
|
-
.footer-col
|
|
325
|
+
.footer-col .footer-col-title { color: var(--color-primary-contrast); font-size: .82rem; font-weight: bold; letter-spacing: .1em; text-transform: uppercase; margin: 0 0 14px; }
|
|
326
326
|
.footer-col ul { display: flex; flex-direction: column; gap: 9px; }
|
|
327
327
|
.footer-col a { color: color-mix(in srgb, var(--color-primary-contrast) 78%, transparent); font-size: .92rem; }
|
|
328
328
|
.footer-col a:hover { color: var(--color-primary-contrast); text-decoration: underline; }
|
|
@@ -480,7 +480,7 @@ function renderFooter(footer: ChromeFooter): string {
|
|
|
480
480
|
.join("");
|
|
481
481
|
return collapsible
|
|
482
482
|
? ` <div class="footer-col"><details>\n <summary>${esc(c.title)}</summary>\n <ul>${links}</ul>\n </details></div>`
|
|
483
|
-
: ` <div class="footer-col">\n <
|
|
483
|
+
: ` <div class="footer-col">\n <p class="footer-col-title">${esc(c.title)}</p>\n <ul>${links}</ul>\n </div>`;
|
|
484
484
|
})
|
|
485
485
|
.join("\n");
|
|
486
486
|
const trust = footer.trustMarks?.length
|
|
@@ -212,9 +212,9 @@ const componentVarsInline = Object.entries(componentVars)
|
|
|
212
212
|
</div>
|
|
213
213
|
<div class="pay-badges"><span>Visa</span><span>Mastercard</span><span>Amex</span><span>PayPal</span></div>
|
|
214
214
|
</div>
|
|
215
|
-
<div class="footer-col"><
|
|
216
|
-
<div class="footer-col"><
|
|
217
|
-
<div class="footer-col"><
|
|
215
|
+
<div class="footer-col"><p class="footer-col-title">Shop</p><ul><li><a href="#">Disposables</a></li><li><a href="#">E-Liquid</a></li><li><a href="#">Devices</a></li></ul></div>
|
|
216
|
+
<div class="footer-col"><p class="footer-col-title">Support</p><ul><li><a href="#">Contact</a></li><li><a href="#">Shipping</a></li><li><a href="#">Returns</a></li></ul></div>
|
|
217
|
+
<div class="footer-col"><p class="footer-col-title">Company</p><ul><li><a href="#">About</a></li><li><a href="#">Blog</a></li></ul></div>
|
|
218
218
|
</div>
|
|
219
219
|
<div class="trust-marks"><span>🔒 Secure checkout</span><span>✓ Age-verified</span><span>★ 4.9 / 5</span></div>
|
|
220
220
|
<div class="footer-warning"><strong>WARNING:</strong><p>This product contains nicotine. Nicotine is an addictive chemical.</p></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/storefront-runner",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.10",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
|
|
6
6
|
"packageManager": "pnpm@11.9.0",
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* When the GA4 sandbox iframe may mount.
|
|
3
|
+
*
|
|
4
|
+
* The frame is same-process: once it loads, gtag.js runs on the page main
|
|
5
|
+
* thread. Mounting it on the first idle turn after `load` puts those long
|
|
6
|
+
* tasks inside the window Lighthouse scores as Total Blocking Time. This
|
|
7
|
+
* scheduler waits for the first of:
|
|
8
|
+
*
|
|
9
|
+
* - a real pointer or key input (`pointerdown` / `keydown`, capture), or
|
|
10
|
+
* - `load`, then a fixed delay long enough to outlast that quiet window.
|
|
11
|
+
*
|
|
12
|
+
* The delay is a minimum. Input before `load` still mounts immediately.
|
|
13
|
+
* The callback never runs synchronously on the call. Cancel is idempotent
|
|
14
|
+
* and drops both the listeners and the timer. Commands sent before the
|
|
15
|
+
* frame exists stay queued by the transport, not by this scheduler.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** How long after `load` the sandbox waits when nobody has interacted. */
|
|
19
|
+
export const GA4_SANDBOX_POST_LOAD_DELAY_MS = 5_000;
|
|
20
|
+
|
|
21
|
+
/** The narrow slice of `window` this scheduler depends on. */
|
|
22
|
+
export interface Ga4SandboxScheduleWindow {
|
|
23
|
+
document?: {
|
|
24
|
+
readyState?: string;
|
|
25
|
+
addEventListener?(
|
|
26
|
+
type: string,
|
|
27
|
+
listener: (event: unknown) => void,
|
|
28
|
+
options?: { capture?: boolean },
|
|
29
|
+
): void;
|
|
30
|
+
removeEventListener?(
|
|
31
|
+
type: string,
|
|
32
|
+
listener: (event: unknown) => void,
|
|
33
|
+
options?: { capture?: boolean },
|
|
34
|
+
): void;
|
|
35
|
+
};
|
|
36
|
+
addEventListener(type: "load", listener: () => void, options?: { once?: boolean }): void;
|
|
37
|
+
removeEventListener?(type: "load", listener: () => void): void;
|
|
38
|
+
setTimeout(callback: () => void, ms: number): unknown;
|
|
39
|
+
clearTimeout(handle: unknown): void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface Ga4SandboxScheduleOptions {
|
|
43
|
+
/** Minimum wait after `load` before the sandbox mounts without input. */
|
|
44
|
+
postLoadDelayMs?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface Ga4SandboxScheduleHandle {
|
|
48
|
+
/** Idempotent. Prevents the callback from running if it has not run yet. */
|
|
49
|
+
cancel(): void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const INPUT_EVENTS = ["pointerdown", "keydown"] as const;
|
|
53
|
+
const INPUT_CAPTURE = { capture: true } as const;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Schedule `callback` for the first input, or for `postLoadDelayMs` after
|
|
57
|
+
* `load`, whichever comes first. Never runs on the call itself.
|
|
58
|
+
*/
|
|
59
|
+
export function scheduleGa4Sandbox(
|
|
60
|
+
win: Ga4SandboxScheduleWindow,
|
|
61
|
+
callback: () => void,
|
|
62
|
+
options: Ga4SandboxScheduleOptions = {},
|
|
63
|
+
): Ga4SandboxScheduleHandle {
|
|
64
|
+
const delay = options.postLoadDelayMs ?? GA4_SANDBOX_POST_LOAD_DELAY_MS;
|
|
65
|
+
let cancelled = false;
|
|
66
|
+
let ran = false;
|
|
67
|
+
let timeoutHandle: unknown;
|
|
68
|
+
let loadListener: (() => void) | undefined;
|
|
69
|
+
const inputTarget = win.document;
|
|
70
|
+
const addInput = inputTarget?.addEventListener;
|
|
71
|
+
const removeInput = inputTarget?.removeEventListener;
|
|
72
|
+
|
|
73
|
+
const detachInput = (): void => {
|
|
74
|
+
if (typeof removeInput !== "function") return;
|
|
75
|
+
for (const type of INPUT_EVENTS) removeInput.call(inputTarget, type, onInput, INPUT_CAPTURE);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const finish = (): void => {
|
|
79
|
+
if (cancelled || ran) return;
|
|
80
|
+
ran = true;
|
|
81
|
+
detachInput();
|
|
82
|
+
if (loadListener) {
|
|
83
|
+
win.removeEventListener?.("load", loadListener);
|
|
84
|
+
loadListener = undefined;
|
|
85
|
+
}
|
|
86
|
+
if (timeoutHandle !== undefined) {
|
|
87
|
+
win.clearTimeout(timeoutHandle);
|
|
88
|
+
timeoutHandle = undefined;
|
|
89
|
+
}
|
|
90
|
+
callback();
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
function onInput(): void {
|
|
94
|
+
finish();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (typeof addInput === "function") {
|
|
98
|
+
for (const type of INPUT_EVENTS) addInput.call(inputTarget, type, onInput, INPUT_CAPTURE);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const armDelay = (): void => {
|
|
102
|
+
if (cancelled || ran) return;
|
|
103
|
+
timeoutHandle = win.setTimeout(() => {
|
|
104
|
+
timeoutHandle = undefined;
|
|
105
|
+
finish();
|
|
106
|
+
}, delay);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
if (win.document?.readyState === "complete") {
|
|
110
|
+
armDelay();
|
|
111
|
+
} else {
|
|
112
|
+
loadListener = () => {
|
|
113
|
+
loadListener = undefined;
|
|
114
|
+
armDelay();
|
|
115
|
+
};
|
|
116
|
+
win.addEventListener("load", loadListener, { once: true });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
cancel() {
|
|
121
|
+
if (cancelled || ran) {
|
|
122
|
+
cancelled = true;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
cancelled = true;
|
|
126
|
+
detachInput();
|
|
127
|
+
if (loadListener) win.removeEventListener?.("load", loadListener);
|
|
128
|
+
if (timeoutHandle !== undefined) win.clearTimeout(timeoutHandle);
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
type NativeGa4SandboxScheduler,
|
|
50
50
|
} from "./native-ga4-sandbox.js";
|
|
51
51
|
import { schedulePostPaint, type PostPaintWindow } from "./post-paint.js";
|
|
52
|
+
import { scheduleGa4Sandbox, type Ga4SandboxScheduleWindow } from "./ga4-sandbox-schedule.js";
|
|
52
53
|
import { ManagedAnalyticsEventBrokerBridge } from "./managed-analytics-bridge.js";
|
|
53
54
|
import { minimizePageAttribution, type ManagedPageViewInput } from "./managed-analytics.js";
|
|
54
55
|
import { managedAnalyticsOpaqueRevision } from "./managed-analytics-value-safety.js";
|
|
@@ -124,14 +125,19 @@ export interface BootLifecycleOptions {
|
|
|
124
125
|
/** Reviewed isolated-origin GA4 transport; injectable for DOM-free tests. */
|
|
125
126
|
nativeGa4Transport?: NativeGa4Transport;
|
|
126
127
|
/**
|
|
127
|
-
* Defers
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* (a guarded synchronous no-op off the browser). Injectable so a test can
|
|
131
|
-
* assert nothing runs before the scheduler fires with a fake, controllable
|
|
132
|
-
* scheduler instead of real paint/idle timing.
|
|
128
|
+
* Defers the Amplitude HTTP capability until after first paint and idle —
|
|
129
|
+
* see `post-paint.ts`. Defaults to that scheduler over `window` (synchronous
|
|
130
|
+
* off the browser). Injectable so a test can hold the flush.
|
|
133
131
|
*/
|
|
134
132
|
schedulePlatformAdapterStart?: NativeGa4SandboxScheduler;
|
|
133
|
+
/**
|
|
134
|
+
* Mounts the GA4 sandbox iframe on the first pointer or key input, or a
|
|
135
|
+
* fixed delay after `load`, whichever comes first — see
|
|
136
|
+
* `ga4-sandbox-schedule.ts`. Defaults to that scheduler over `window`
|
|
137
|
+
* (synchronous off the browser). Commands sent before the frame exists stay
|
|
138
|
+
* queued in the transport. Injectable so a test can hold the mount.
|
|
139
|
+
*/
|
|
140
|
+
scheduleGa4Sandbox?: NativeGa4SandboxScheduler;
|
|
135
141
|
}
|
|
136
142
|
|
|
137
143
|
/**
|
|
@@ -328,7 +334,8 @@ function defaultConsentGate(
|
|
|
328
334
|
/**
|
|
329
335
|
* The default `schedulePlatformAdapterStart`: the real post-paint scheduler over the
|
|
330
336
|
* global `window`, or a synchronous immediate start off the browser (SSR, tests that
|
|
331
|
-
* import this module without a DOM) — never a hang.
|
|
337
|
+
* import this module without a DOM) — never a hang. Amplitude uses this. The GA4
|
|
338
|
+
* sandbox does not.
|
|
332
339
|
*/
|
|
333
340
|
function defaultPlatformAdapterScheduler(start: () => void): { cancel(): void } {
|
|
334
341
|
if (typeof window === "undefined") {
|
|
@@ -338,12 +345,24 @@ function defaultPlatformAdapterScheduler(start: () => void): { cancel(): void }
|
|
|
338
345
|
return schedulePostPaint(window as unknown as PostPaintWindow, start);
|
|
339
346
|
}
|
|
340
347
|
|
|
348
|
+
/**
|
|
349
|
+
* The default GA4 sandbox scheduler. Off the browser it starts synchronously so
|
|
350
|
+
* SSR and DOM-free tests do not hang. In the browser it never starts on this
|
|
351
|
+
* turn: first input, or the post-load delay, whichever comes first.
|
|
352
|
+
*/
|
|
353
|
+
function defaultGa4SandboxScheduler(start: () => void): { cancel(): void } {
|
|
354
|
+
if (typeof window === "undefined") {
|
|
355
|
+
start();
|
|
356
|
+
return { cancel() {} };
|
|
357
|
+
}
|
|
358
|
+
return scheduleGa4Sandbox(window as unknown as Ga4SandboxScheduleWindow, start);
|
|
359
|
+
}
|
|
360
|
+
|
|
341
361
|
/**
|
|
342
362
|
* Wrap a `fetch` capability so calls queue instead of running immediately, and are
|
|
343
|
-
* all issued together once `schedule` fires. This
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
* stays paint-timing-agnostic — it just calls the `fetch` capability it was given).
|
|
363
|
+
* all issued together once `schedule` fires. This keeps the Amplitude adapter's
|
|
364
|
+
* HTTP collection off the pre-paint critical path. `native-amplitude.ts` stays
|
|
365
|
+
* paint-timing-agnostic — it just calls the `fetch` capability it was given.
|
|
347
366
|
*/
|
|
348
367
|
function deferFetchCapability(
|
|
349
368
|
fetchCapability: typeof fetch,
|
|
@@ -395,6 +414,7 @@ export function bootIntegrationLifecycle(
|
|
|
395
414
|
(typeof win.fetch === "function" ? win.fetch.bind(win) : undefined);
|
|
396
415
|
const schedulePlatformAdapterStart =
|
|
397
416
|
options.schedulePlatformAdapterStart ?? defaultPlatformAdapterScheduler;
|
|
417
|
+
const scheduleGa4SandboxStart = options.scheduleGa4Sandbox ?? defaultGa4SandboxScheduler;
|
|
398
418
|
const fallbackLoader = options.importModule
|
|
399
419
|
? makeSameOriginEntryLoader(options.importModule)
|
|
400
420
|
: sameOriginEntryLoader;
|
|
@@ -419,7 +439,7 @@ export function bootIntegrationLifecycle(
|
|
|
419
439
|
return createNativeGa4SandboxTransport(
|
|
420
440
|
createBrowserNativeGa4SandboxHost(document, MessageChannel),
|
|
421
441
|
resolveNativeGa4SandboxUrl(win.location.href),
|
|
422
|
-
|
|
442
|
+
scheduleGa4SandboxStart,
|
|
423
443
|
);
|
|
424
444
|
},
|
|
425
445
|
});
|
|
@@ -111,10 +111,11 @@ const IMMEDIATE_SCHEDULER: NativeGa4SandboxScheduler = (start) => {
|
|
|
111
111
|
* needs a real same-site origin. The exact origin, iframe window, and one-shot transfer
|
|
112
112
|
* are the authority boundary. No general parent `message` listener is installed.
|
|
113
113
|
*
|
|
114
|
-
* The frame is not created or appended until `schedule` fires
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* `configure`/`send`
|
|
114
|
+
* The frame is not created or appended until `schedule` fires. Boot passes the
|
|
115
|
+
* GA4 sandbox schedule (`ga4-sandbox-schedule.ts`); a caller that omits
|
|
116
|
+
* `schedule` still mounts immediately. That append is what starts gtag.js.
|
|
117
|
+
* `configure`/`send` before the frame mounts stay queued and flush in order
|
|
118
|
+
* when the frame loads.
|
|
118
119
|
*/
|
|
119
120
|
export function createNativeGa4SandboxTransport(
|
|
120
121
|
host: NativeGa4SandboxHost,
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Paint-aware scheduling for
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* and main-thread time straight to LCP. This scheduler defers a callback until
|
|
8
|
-
* AFTER the document has painted and the main thread is otherwise idle:
|
|
2
|
+
* Paint-aware scheduling for platform adapter work that starts once the
|
|
3
|
+
* document has painted and the main thread is idle (Amplitude's HTTP
|
|
4
|
+
* collection). The GA4 sandbox uses `ga4-sandbox-schedule.ts` instead: its
|
|
5
|
+
* iframe shares the page process, so an idle start still runs gtag.js inside
|
|
6
|
+
* the Total Blocking Time window.
|
|
9
7
|
*
|
|
10
8
|
* - if the document has already finished loading, schedule on idle immediately;
|
|
11
9
|
* - otherwise wait for `load`, then schedule on idle;
|
|
12
10
|
* - "idle" is `requestIdleCallback` with a bounded timeout, falling back to a
|
|
13
11
|
* macrotask (`setTimeout(0)`) where `requestIdleCallback` does not exist.
|
|
14
12
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* The idle timeout is a maximum, not a minimum. Cancellable at every stage
|
|
14
|
+
* (before `load`, and while idle-queued), so a document torn down before the
|
|
15
|
+
* deferred work starts never runs it.
|
|
17
16
|
*/
|
|
18
17
|
|
|
19
18
|
/** The narrow slice of `window` this scheduler depends on — structural, edge-safe. */
|