@escape-game-over/atlas 0.1.115 → 0.1.117

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.
Files changed (62) hide show
  1. package/README.md +1 -1
  2. package/docs/NOT-BUILT.md +2 -2
  3. package/package.json +2 -2
  4. package/src/analytics/ids.ts +1 -13
  5. package/src/analytics/index.ts +18 -18
  6. package/src/analytics/storage.ts +34 -39
  7. package/src/analytics/vendor-settings.ts +162 -0
  8. package/src/astro/axon-event.ts +2 -4
  9. package/src/astro/axon-pixel.ts +3 -6
  10. package/src/astro/clarity-event.ts +2 -4
  11. package/src/astro/clarity.ts +3 -6
  12. package/src/astro/consent.ts +3 -6
  13. package/src/astro/drip-event.ts +2 -4
  14. package/src/astro/drip.ts +2 -7
  15. package/src/astro/escape-navigator-consent.ts +2 -6
  16. package/src/astro/escapeall-calendar.ts +2 -5
  17. package/src/astro/google-event.ts +3 -7
  18. package/src/astro/google-map.ts +3 -5
  19. package/src/astro/images.ts +12 -26
  20. package/src/astro/index.ts +2 -9
  21. package/src/astro/meta-event.ts +2 -4
  22. package/src/astro/meta-pixel.ts +4 -8
  23. package/src/astro/public-files.ts +11 -52
  24. package/src/astro/site-routes.ts +11 -55
  25. package/src/astro/snap-event.ts +2 -4
  26. package/src/astro/snap-pixel.ts +3 -6
  27. package/src/astro/tiktok-event.ts +2 -4
  28. package/src/astro/tiktok-pixel.ts +70 -39
  29. package/src/astro/umami-event.ts +2 -6
  30. package/src/astro/vendors.ts +64 -0
  31. package/src/config.ts +8 -17
  32. package/src/content/rich.ts +15 -18
  33. package/src/i18n/catalog.ts +20 -1
  34. package/src/i18n/define.ts +0 -5
  35. package/src/i18n/translate.ts +9 -48
  36. package/src/index.ts +24 -166
  37. package/src/jsonld/article.ts +7 -17
  38. package/src/jsonld/business.ts +27 -28
  39. package/src/jsonld/creative-work.ts +8 -20
  40. package/src/jsonld/node.ts +37 -7
  41. package/src/jsonld/organization.ts +21 -30
  42. package/src/jsonld/place.ts +2 -6
  43. package/src/jsonld/product.ts +16 -22
  44. package/src/jsonld/service.ts +21 -37
  45. package/src/jsonld/video.ts +4 -8
  46. package/src/jsonld/website.ts +3 -5
  47. package/src/meta/content.ts +14 -66
  48. package/src/meta/index.ts +10 -32
  49. package/src/project.ts +4 -18
  50. package/src/routes/define.ts +0 -13
  51. package/src/routes/family.ts +135 -3
  52. package/src/site/api.ts +1 -21
  53. package/src/site/create.ts +17 -26
  54. package/src/site/define.ts +13 -9
  55. package/src/site/page.ts +1 -10
  56. package/src/warn.ts +6 -6
  57. package/src/analytics/axon-pixel.ts +0 -24
  58. package/src/analytics/clarity.ts +0 -29
  59. package/src/analytics/drip.ts +0 -20
  60. package/src/analytics/meta-pixel.ts +0 -29
  61. package/src/analytics/snap-pixel.ts +0 -28
  62. package/src/analytics/tiktok-pixel.ts +0 -29
package/README.md CHANGED
@@ -398,7 +398,7 @@ src/ the package — see the export map above for what is reachable
398
398
  meta/ head tags: canonical, hreflang, Open Graph, Twitter, robots
399
399
  jsonld/ the @graph — one file per node, each linked to Google's docs
400
400
  i18n/ defineMessages(), t(), placeholder extraction
401
- routes/ defineRoutes(), routeFamily(), merging and collisions
401
+ routes/ defineRoutes(), route families, merging and collisions
402
402
  analytics/ Umami and Google, the Consent Mode defaults, and whether a
403
403
  visitor has to be asked anything at all
404
404
  astro/ the only framework-aware code, plus the one component
package/docs/NOT-BUILT.md CHANGED
@@ -209,10 +209,10 @@ something renders a currency picker, and not before.
209
209
 
210
210
  `LocalBusinessInput.type` and `ProductInput.alsoA` take bare strings, so
211
211
  `"EntertainmentBussiness"` compiles, gets emitted, and is ignored by every
212
- reader — the silent failure that `RobotsDirective` and `OpenGraphType` were
212
+ reader — the silent failure that `RobotsDirective` was
213
213
  tightened to prevent.
214
214
 
215
- They are not the same case. Those two vocabularies are closed; schema.org has
215
+ They are not the same case. That vocabulary is closed; schema.org has
216
216
  around 800 types and is still growing. A partial enumeration of an open
217
217
  vocabulary rejects types that are perfectly valid, and the response to that is
218
218
  to cast past it — at which point the check is theatre. A literal union that
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@escape-game-over/atlas",
3
- "version": "0.1.115",
3
+ "version": "0.1.117",
4
4
  "type": "module",
5
5
  "description": "Typed, data-driven machinery for static multi-locale, multi-deployment Astro sites.",
6
6
  "private": false,
@@ -67,7 +67,7 @@
67
67
  "@biomejs/biome": "2.6.7",
68
68
  "@types/node": "26.6.6",
69
69
  "@vitest/coverage-istanbul": "5.1.0",
70
- "astro": "7.4.11",
70
+ "astro": "7.4.12",
71
71
  "typescript": "6.0.3",
72
72
  "vitest": "5.1.0"
73
73
  }
@@ -24,18 +24,6 @@ export function invalidId(at: string, message: string): never {
24
24
  throw new Error(`${at}: ${message}`);
25
25
  }
26
26
 
27
- /** A Meta pixel id: 15 or 16 digits. */
28
- export const META_PIXEL_ID = /^\d{15,16}$/;
29
-
30
- /** A TikTok pixel id: 20 capital letters and digits. */
31
- export const TIKTOK_PIXEL_ID = /^[A-Z0-9]{20}$/;
32
-
33
- /** A Drip account id: digits. */
34
- export const DRIP_ACCOUNT_ID = /^\d+$/;
35
-
36
- /** A Clarity project id: 10 lowercase letters and digits. */
37
- export const CLARITY_PROJECT_ID = /^[a-z0-9]{10}$/;
38
-
39
- /** A UUID, as Umami, Snap and Axon issue them. */
27
+ /** A UUID, as Umami, Snap, Axon and the booking vendors issue them. */
40
28
  export const UUID =
41
29
  /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -1,18 +1,21 @@
1
1
  import type { MetaTag } from "../meta/tag.ts";
2
2
  import type { HttpsUrl } from "../url.ts";
3
- import { type AxonPixelSettings, checkAxonPixel } from "./axon-pixel.ts";
4
- import { type ClaritySettings, checkClarity } from "./clarity.ts";
5
- import { checkDrip, type DripSettings } from "./drip.ts";
6
3
  import { type GoogleSettings, googleEmits, googleScripts } from "./google.ts";
7
- import { checkMetaPixel, type MetaPixelSettings } from "./meta-pixel.ts";
8
- import { checkSnapPixel, type SnapPixelSettings } from "./snap-pixel.ts";
9
4
  import type { AnalyticsTags } from "./tags.ts";
10
- import { checkTikTokPixel, type TikTokPixelSettings } from "./tiktok-pixel.ts";
11
5
  import {
12
6
  checkUmamiDomains,
13
7
  type UmamiSettings,
14
8
  umamiScripts,
15
9
  } from "./umami.ts";
10
+ import {
11
+ type AxonPixelSettings,
12
+ type ClaritySettings,
13
+ checkVendorIds,
14
+ type DripSettings,
15
+ type MetaPixelSettings,
16
+ type SnapPixelSettings,
17
+ type TikTokPixelSettings,
18
+ } from "./vendor-settings.ts";
16
19
 
17
20
  /**
18
21
  * Analytics, as head tags.
@@ -29,9 +32,6 @@ import {
29
32
  * project states ids; lib decides everything that follows from them.
30
33
  */
31
34
 
32
- export type { AxonPixelSettings } from "./axon-pixel.ts";
33
- export type { ClaritySettings } from "./clarity.ts";
34
- export type { DripSettings } from "./drip.ts";
35
35
  export {
36
36
  CONSENT_UPDATE_GLOBAL,
37
37
  type ConsentDefaults,
@@ -39,15 +39,20 @@ export {
39
39
  type GoogleSettings,
40
40
  } from "./google.ts";
41
41
  export type { GoogleTagId, NumericId, TagManagerId, Uuid } from "./ids.ts";
42
- export type { MetaPixelSettings } from "./meta-pixel.ts";
43
- export type { SnapPixelSettings } from "./snap-pixel.ts";
44
42
  export type { AnalyticsTags } from "./tags.ts";
45
- export type { TikTokPixelSettings } from "./tiktok-pixel.ts";
46
43
  export type {
47
44
  UmamiReplay,
48
45
  UmamiSettings,
49
46
  UmamiTracker,
50
47
  } from "./umami.ts";
48
+ export type {
49
+ AxonPixelSettings,
50
+ ClaritySettings,
51
+ DripSettings,
52
+ MetaPixelSettings,
53
+ SnapPixelSettings,
54
+ TikTokPixelSettings,
55
+ } from "./vendor-settings.ts";
51
56
 
52
57
  /** What a project may switch on. A vendor is a key here. */
53
58
  export interface AnalyticsSettings {
@@ -85,12 +90,7 @@ export function analyticsScripts(
85
90
  if (analytics === undefined) return { head: [], body: [] };
86
91
 
87
92
  checkUmamiDomains(analytics.umami, origin);
88
- checkMetaPixel(analytics.meta, origin);
89
- checkTikTokPixel(analytics.tiktok, origin);
90
- checkSnapPixel(analytics.snapchat, origin);
91
- checkAxonPixel(analytics.axon, origin);
92
- checkDrip(analytics.drip, origin);
93
- checkClarity(analytics.clarity, origin);
93
+ checkVendorIds(analytics, origin);
94
94
  const google = googleScripts(analytics.google, origin);
95
95
  return {
96
96
  head: [...google.head, ...umamiScripts(analytics.umami)],
@@ -97,7 +97,7 @@ export interface StoredItem {
97
97
  readonly consent: ConsentCategory | "none";
98
98
  }
99
99
 
100
- export interface StoredItemsOptions {
100
+ export interface StoredItemsOptions<Purpose> {
101
101
  /** As passed to `Document`, and with its default: `true`. */
102
102
  readonly rememberLocale?: boolean;
103
103
  /** As passed to `Document`, and with its default: `false`. */
@@ -109,12 +109,17 @@ export interface StoredItemsOptions {
109
109
  */
110
110
  readonly booking?: readonly BookingVendor[];
111
111
  /**
112
- * Rows the site sets first, then by what has to be granted — nothing,
113
- * analytics, marketing — and otherwise in catalog order. Off, the rows
114
- * come in catalog order: Atlas's own, then each vendor's site row before
115
- * its vendor-domain one.
112
+ * What each row is for, in the project's words — a message key, usually,
113
+ * since the words are the project's and in every locale.
114
+ *
115
+ * Required, and every row this deployment has must be named: a notice
116
+ * that lists a cookie without a purpose is the one thing it must not
117
+ * publish, so a vendor switched on, or a row a new Atlas adds, fails the
118
+ * build until the site says what it is for. Rows this deployment does not
119
+ * have may be named too; a site keeps its wording when a vendor is
120
+ * switched off.
116
121
  */
117
- readonly sort?: boolean;
122
+ readonly purposes: Readonly<Partial<Record<StoredId, Purpose>>>;
118
123
  }
119
124
 
120
125
  interface Stored {
@@ -264,7 +269,7 @@ type VendorWithOwn = {
264
269
  : never;
265
270
  }[keyof typeof CATALOG];
266
271
 
267
- /** Catalog order, which is also the order rows come in unsorted. */
272
+ /** Catalog order, which is how rows that sort alike stay ordered. */
268
273
  const VENDOR_ORDER: readonly StorageVendor[] = [
269
274
  "google-analytics",
270
275
  "google-ads",
@@ -372,8 +377,12 @@ const CONSENT_ORDER: readonly StoredItem["consent"][] = [
372
377
  "marketing",
373
378
  ];
374
379
 
375
- /** The rows the `sort` option puts first come first; ties keep their order. */
376
- export function compareStoredItems(a: StoredItem, b: StoredItem): number {
380
+ /**
381
+ * Rows the site sets first, then by what has to be granted — nothing,
382
+ * analytics, marketing. Ties keep catalog order: Atlas's own, then each
383
+ * vendor's site row before its vendor-domain one.
384
+ */
385
+ function compareStoredItems(a: StoredItem, b: StoredItem): number {
377
386
  return (
378
387
  Number(b.provider === "site") - Number(a.provider === "site") ||
379
388
  CONSENT_ORDER.indexOf(a.consent) - CONSENT_ORDER.indexOf(b.consent)
@@ -381,45 +390,22 @@ export function compareStoredItems(a: StoredItem, b: StoredItem): number {
381
390
  }
382
391
 
383
392
  /**
384
- * Each row with what the project says it is for — a message key, usually,
385
- * since the words are the project's and in every locale.
386
- *
387
- * Throws, naming every row without one, so a vendor switched on, or a row a
388
- * new Atlas adds, fails the build until the site has said what it is for: a
389
- * notice that lists a cookie without a purpose is the one thing it must not
390
- * publish. `purposes` may name rows this deployment does not have; a site
391
- * keeps its wording when a vendor is switched off.
392
- */
393
- export function withPurposes<K>(
394
- rows: readonly StoredItem[],
395
- purposes: Partial<Record<StoredId, K>>
396
- ): readonly (StoredItem & { readonly purpose: K })[] {
397
- const missing = rows.filter((row) => purposes[row.id] === undefined);
398
- if (missing.length > 0) {
399
- throw new Error(
400
- `The storage table has no purpose for ${missing.map((row) => `"${row.id}"`).join(", ")}. Add one for each to the purposes given to withPurposes.`
401
- );
402
- }
403
- return rows.map((row) => ({ ...row, purpose: purposes[row.id] as K }));
404
- }
405
-
406
- /**
407
- * Everything this deployment keeps on a device: Atlas's own keys, then each
408
- * loaded vendor's.
393
+ * Everything this deployment keeps on a device, each row with its purpose:
394
+ * the site's own rows first, then by what has to be granted.
409
395
  *
410
396
  * Atlas's rows follow what the page renders, which `Document` decides from
411
397
  * its props, so pass the same `rememberLocale` and `rememberCampaign` here.
412
398
  * The consent record is listed whenever there is a banner to answer.
413
399
  */
414
- export function storedItems(
400
+ export function storedItems<const Purpose>(
415
401
  analytics: AnalyticsSettings | undefined,
416
- options: StoredItemsOptions = {}
417
- ): readonly StoredItem[] {
402
+ options: StoredItemsOptions<Purpose>
403
+ ): readonly (StoredItem & { readonly purpose: Purpose })[] {
418
404
  const {
419
405
  rememberLocale = true,
420
406
  rememberCampaign = false,
421
407
  booking = [],
422
- sort = false,
408
+ purposes,
423
409
  } = options;
424
410
  const asked = consentVendors(analytics).length > 0;
425
411
 
@@ -506,5 +492,14 @@ export function storedItems(
506
492
 
507
493
  rows.push(...widgetRows);
508
494
 
509
- return sort ? rows.toSorted(compareStoredItems) : rows;
495
+ const missing = rows.filter((row) => purposes[row.id] === undefined);
496
+ if (missing.length > 0) {
497
+ throw new Error(
498
+ `The storage table has no purpose for ${missing.map((row) => `"${row.id}"`).join(", ")}. Add one for each to storedItems' purposes.`
499
+ );
500
+ }
501
+ return rows.toSorted(compareStoredItems).flatMap((row) => {
502
+ const purpose = purposes[row.id];
503
+ return purpose === undefined ? [] : [{ ...row, purpose }];
504
+ });
510
505
  }
@@ -0,0 +1,162 @@
1
+ import type { NonEmpty } from "../types.ts";
2
+ import { invalidId, type NumericId, UUID, type Uuid } from "./ids.ts";
3
+
4
+ /**
5
+ * Meta's pixel. Marketing: it loads only once a visitor grants that.
6
+ *
7
+ * @see https://developers.facebook.com/docs/meta-pixel/get-started
8
+ * @see https://developers.facebook.com/docs/meta-pixel/reference
9
+ * @see https://developers.facebook.com/docs/meta-pixel/implementation/gdpr
10
+ */
11
+ export interface MetaPixelSettings {
12
+ /** From Events Manager: 15 or 16 digits. Several share one script. */
13
+ readonly pixelIds: NonEmpty<NumericId>;
14
+ }
15
+
16
+ /**
17
+ * TikTok's pixel. Marketing: it loads only once a visitor grants that.
18
+ *
19
+ * @see https://ads.tiktok.com/help/article/get-started-pixel
20
+ * @see https://ads.tiktok.com/help/article/standard-events-parameters
21
+ * @see https://ads.tiktok.com/help/article/how-to-use-cookies-with-tiktok-pixel
22
+ */
23
+ export interface TikTokPixelSettings {
24
+ /** From TikTok Events Manager: 20 capital letters and digits. */
25
+ readonly pixelIds: NonEmpty<string>;
26
+ }
27
+
28
+ /**
29
+ * Snapchat's pixel. Marketing: it loads only once a visitor grants that.
30
+ *
31
+ * @see https://businesshelp.snapchat.com/s/article/pixel-direct-implementation
32
+ * @see https://forbusiness.snapchat.com/en-US/blog/the-snap-pixel-how-it-works-and-how-to-install-it
33
+ */
34
+ export interface SnapPixelSettings {
35
+ /** From Snap's Events Manager: a UUID. */
36
+ readonly pixelIds: NonEmpty<Uuid>;
37
+ }
38
+
39
+ /**
40
+ * AppLovin's Axon pixel. Marketing: it loads only once a visitor grants that.
41
+ *
42
+ * @see https://support.applovin.com/en/growth/promoting-your-websites/axon-pixel-integration/axon-pixel-native-js
43
+ * @see https://support.applovin.com/en/growth/promoting-your-websites/axon-pixel-integration/events-and-objects
44
+ */
45
+ export interface AxonPixelSettings {
46
+ /** From the Axon dashboard: a UUID. */
47
+ readonly eventKey: Uuid;
48
+ }
49
+
50
+ /**
51
+ * Drip's site tracking. Marketing: it loads only once a visitor grants that.
52
+ *
53
+ * @see https://help.drip.com/hc/en-us/articles/4424702539789-Install-Your-JavaScript-Snippet
54
+ */
55
+ export interface DripSettings {
56
+ /** From Drip's Site Setup: digits, the `_dcs.account` of its snippet. */
57
+ readonly accountId: NumericId;
58
+ }
59
+
60
+ /**
61
+ * Microsoft Clarity. It has a cookieless mode, so like Google it loads for
62
+ * every visitor: without consent it records each page view on its own and sets
63
+ * no cookie, and the visitor's answer switches its cookies on or off.
64
+ *
65
+ * @see https://learn.microsoft.com/en-us/clarity/setup-and-installation/clarity-setup
66
+ * @see https://learn.microsoft.com/en-us/clarity/setup-and-installation/consent-mode
67
+ * @see https://learn.microsoft.com/en-us/clarity/setup-and-installation/clarity-consent-api-v2
68
+ */
69
+ export interface ClaritySettings {
70
+ /** From the Clarity project's settings: 10 lowercase letters and digits. */
71
+ readonly projectId: string;
72
+ }
73
+
74
+ /** What these vendors' checks read: their slice of `AnalyticsSettings`. */
75
+ interface VendorSettings {
76
+ readonly meta?: MetaPixelSettings;
77
+ readonly tiktok?: TikTokPixelSettings;
78
+ readonly snapchat?: SnapPixelSettings;
79
+ readonly axon?: AxonPixelSettings;
80
+ readonly drip?: DripSettings;
81
+ readonly clarity?: ClaritySettings;
82
+ }
83
+
84
+ /** One vendor's id: where it is set, its shape, and where to copy it from. */
85
+ interface IdCheck {
86
+ readonly ids: (settings: VendorSettings) => readonly string[];
87
+ /** What the id is called, as the vendor calls it. */
88
+ readonly name: string;
89
+ readonly shape: RegExp;
90
+ /** The shape in words, for the error. */
91
+ readonly is: string;
92
+ readonly from: string;
93
+ }
94
+
95
+ /**
96
+ * Each vendor's id check: one row each, read in one loop. Only the shape, the
97
+ * words and the place to copy it from differ between vendors.
98
+ */
99
+ const ID_CHECKS: readonly IdCheck[] = [
100
+ {
101
+ ids: (settings) => settings.meta?.pixelIds ?? [],
102
+ name: "Meta pixel id",
103
+ shape: /^\d{15,16}$/,
104
+ is: "15 or 16 digits",
105
+ from: "Events Manager",
106
+ },
107
+ {
108
+ ids: (settings) => settings.tiktok?.pixelIds ?? [],
109
+ name: "TikTok pixel id",
110
+ shape: /^[A-Z0-9]{20}$/,
111
+ is: "20 capital letters and digits",
112
+ from: "TikTok Events Manager",
113
+ },
114
+ {
115
+ ids: (settings) => settings.snapchat?.pixelIds ?? [],
116
+ name: "Snap pixel id",
117
+ shape: UUID,
118
+ is: "a UUID",
119
+ from: "Snap's Events Manager",
120
+ },
121
+ {
122
+ ids: (settings) =>
123
+ settings.axon === undefined ? [] : [settings.axon.eventKey],
124
+ name: "Axon event key",
125
+ shape: UUID,
126
+ is: "a UUID",
127
+ from: "the Axon dashboard",
128
+ },
129
+ {
130
+ ids: (settings) =>
131
+ settings.drip === undefined ? [] : [settings.drip.accountId],
132
+ name: "Drip account id",
133
+ shape: /^\d+$/,
134
+ is: "all digits",
135
+ from: "Drip's Site Setup",
136
+ },
137
+ {
138
+ ids: (settings) =>
139
+ settings.clarity === undefined ? [] : [settings.clarity.projectId],
140
+ name: "Clarity project id",
141
+ shape: /^[a-z0-9]{10}$/,
142
+ is: "10 lowercase letters and digits",
143
+ from: "the Clarity project's settings",
144
+ },
145
+ ];
146
+
147
+ /**
148
+ * Fails the build on an id that is not what its vendor issues: configured,
149
+ * emitted, and reporting nowhere is the failure it prevents.
150
+ */
151
+ export function checkVendorIds(settings: VendorSettings, at: string): void {
152
+ for (const check of ID_CHECKS) {
153
+ for (const id of check.ids(settings)) {
154
+ if (!check.shape.test(id)) {
155
+ invalidId(
156
+ at,
157
+ `${check.name} "${id}" is not ${check.is}, so it would report nowhere. Copy it from ${check.from}.`
158
+ );
159
+ }
160
+ }
161
+ }
162
+ }
@@ -1,5 +1,6 @@
1
1
  import type { CurrencyCode } from "../currency.ts";
2
2
  import { readConsent } from "./consent.ts";
3
+ import { vendors } from "./vendors.ts";
3
4
 
4
5
  /**
5
6
  * Axon's events that a site without a cart sends. The cart and purchase events
@@ -15,9 +16,6 @@ export interface AxonEventData {
15
16
  }
16
17
 
17
18
  /** What `axon-pixel.ts` defines once marketing is granted. */
18
- interface AxonWindow {
19
- axon?: (...args: unknown[]) => void;
20
- }
21
19
 
22
20
  /**
23
21
  * Records an event with Axon's pixel — a lead sent, an account made.
@@ -28,7 +26,7 @@ interface AxonWindow {
28
26
  */
29
27
  export function axonEvent(event: AxonEvent, data?: AxonEventData): void {
30
28
  if (readConsent()?.marketing !== "granted") return;
31
- const axon = (window as unknown as AxonWindow).axon;
29
+ const axon = vendors().axon;
32
30
  if (data === undefined) axon?.("track", event);
33
31
  else axon?.("track", event, data);
34
32
  }
@@ -1,8 +1,9 @@
1
1
  import { appendScript } from "./append-script.ts";
2
2
  import type { ConsentGated } from "./consent-gate.ts";
3
+ import { vendors } from "./vendors.ts";
3
4
 
4
5
  /** Axon's `axon`: a queue until its scripts arrive and replay it. */
5
- interface Axon {
6
+ export interface Axon {
6
7
  (...args: unknown[]): void;
7
8
  performOperation?: (...args: unknown[]) => void;
8
9
  operationQueue: unknown[];
@@ -10,10 +11,6 @@ interface Axon {
10
11
  eventKey: string;
11
12
  }
12
13
 
13
- interface PixelWindow {
14
- axon?: Axon;
15
- }
16
-
17
14
  const SCRIPTS = [
18
15
  "https://s.axon.ai/pixel.js",
19
16
  "https://res4.applovin.com/p/l/loader.iife.js",
@@ -21,7 +18,7 @@ const SCRIPTS = [
21
18
 
22
19
  /** Axon's own base code, as it defines `axon` before the scripts load. */
23
20
  function defineAxon(eventKey: string): Axon {
24
- const global = window as unknown as PixelWindow;
21
+ const global = vendors();
25
22
  if (global.axon !== undefined) return global.axon;
26
23
  // `arguments` is the list of whatever a call passed, which every regular
27
24
  // `function` gets without declaring it (an arrow function does not). The
@@ -1,4 +1,5 @@
1
1
  import type { DeclaredEvent } from "./event-data.ts";
2
+ import { vendors } from "./vendors.ts";
2
3
 
3
4
  /**
4
5
  * The project's Clarity events. Clarity carries a name and nothing else, so
@@ -17,9 +18,6 @@ import type { DeclaredEvent } from "./event-data.ts";
17
18
  export interface ClarityEvents {}
18
19
 
19
20
  /** What `clarity.ts` defines once the page has loaded. */
20
- interface ClarityWindow {
21
- clarity?: (...args: unknown[]) => void;
22
- }
23
21
 
24
22
  /**
25
23
  * Marks the current Clarity recording with a named event — a form sent, a
@@ -29,5 +27,5 @@ interface ClarityWindow {
29
27
  * sets no cookie. Dropped before Clarity loads, or where it is not configured.
30
28
  */
31
29
  export function clarityEvent(name: DeclaredEvent<ClarityEvents>): void {
32
- (window as unknown as ClarityWindow).clarity?.("event", name);
30
+ vendors().clarity?.("event", name);
33
31
  }
@@ -1,19 +1,16 @@
1
1
  import { appendScript } from "./append-script.ts";
2
2
  import type { ConsentChoices } from "./consent.ts";
3
+ import { vendors } from "./vendors.ts";
3
4
 
4
5
  /** Clarity's `clarity`: a queue until its script arrives and replays it. */
5
- interface Clarity {
6
+ export interface Clarity {
6
7
  (...args: unknown[]): void;
7
8
  q?: unknown[];
8
9
  }
9
10
 
10
- interface ClarityWindow {
11
- clarity?: Clarity;
12
- }
13
-
14
11
  /** Clarity's own base code, as it defines `clarity` before the script loads. */
15
12
  function defineClarity(): Clarity {
16
- const global = window as unknown as ClarityWindow;
13
+ const global = vendors();
17
14
  if (global.clarity !== undefined) return global.clarity;
18
15
  // `arguments` is the list of whatever a call passed, which every regular
19
16
  // `function` gets without declaring it (an arrow function does not). The
@@ -12,6 +12,8 @@ export type {
12
12
  ConsentChoices,
13
13
  } from "../analytics/consent-storage.ts";
14
14
 
15
+ import { vendors } from "./vendors.ts";
16
+
15
17
  /**
16
18
  * The browser half of consent: remembering an answer, expiring it, and handing
17
19
  * it to Google.
@@ -120,12 +122,7 @@ export function recordConsent(choices: ConsentChoices): void {
120
122
  * copy of that list.
121
123
  */
122
124
  export function applyConsent(choices: ConsentChoices): void {
123
- const update = (
124
- window as unknown as Record<
125
- string,
126
- ((c: ConsentChoices) => void) | undefined
127
- >
128
- )[CONSENT_UPDATE_GLOBAL];
125
+ const update = vendors()[CONSENT_UPDATE_GLOBAL];
129
126
  // Only the categories, so a record's date does not ride along.
130
127
  update?.({ analytics: choices.analytics, marketing: choices.marketing });
131
128
  }
@@ -1,5 +1,6 @@
1
1
  import { readConsent } from "./consent.ts";
2
2
  import type { DeclaredEvent, EventDataArgs } from "./event-data.ts";
3
+ import { vendors } from "./vendors.ts";
3
4
 
4
5
  /**
5
6
  * The project's Drip events — the actions its workflows start on — each with
@@ -18,9 +19,6 @@ import type { DeclaredEvent, EventDataArgs } from "./event-data.ts";
18
19
  export interface DripEvents {}
19
20
 
20
21
  /** What `drip.ts` defines once marketing is granted. */
21
- interface DripWindow {
22
- _dcq?: unknown[][];
23
- }
24
22
 
25
23
  /**
26
24
  * Records an event with Drip, to start a workflow — a lead sent, a voucher
@@ -38,7 +36,7 @@ export function dripEvent<E extends DeclaredEvent<DripEvents>>(
38
36
  >
39
37
  ): void {
40
38
  if (readConsent()?.marketing !== "granted") return;
41
- const queue = (window as unknown as DripWindow)._dcq;
39
+ const queue = vendors()._dcq;
42
40
  if (properties === undefined) queue?.push(["track", action]);
43
41
  else queue?.push(["track", action, properties]);
44
42
  }
package/src/astro/drip.ts CHANGED
@@ -1,11 +1,6 @@
1
1
  import { appendScript } from "./append-script.ts";
2
2
  import type { ConsentGated } from "./consent-gate.ts";
3
-
4
- /** Drip's globals: a queue of calls, and the account its script reports to. */
5
- interface DripWindow {
6
- _dcq?: unknown[][];
7
- _dcs?: { account?: string };
8
- }
3
+ import { vendors } from "./vendors.ts";
9
4
 
10
5
  /**
11
6
  * Drip, loaded on the first grant. Its script records visits by itself, so
@@ -19,7 +14,7 @@ export function drip(accountId: string): ConsentGated {
19
14
  grant: () => {
20
15
  if (loaded) return;
21
16
  loaded = true;
22
- const global = window as unknown as DripWindow;
17
+ const global = vendors();
23
18
  global._dcq ??= [];
24
19
  global._dcs ??= {};
25
20
  global._dcs.account = accountId;
@@ -1,6 +1,7 @@
1
1
  import { ESCAPE_NAVIGATOR_LOADER } from "../booking/escape-navigator.ts";
2
2
  import { appendScript } from "./append-script.ts";
3
3
  import { CONSENT_EVENT, type ConsentChoices, readConsent } from "./consent.ts";
4
+ import { vendors } from "./vendors.ts";
4
5
 
5
6
  /**
6
7
  * Escape Navigator's consent, from the visitor's answer on this site: its
@@ -13,12 +14,7 @@ function escapeNavigatorConsent(choices: ConsentChoices | undefined) {
13
14
  };
14
15
  }
15
16
 
16
- interface EscapeNavigatorGlobal {
17
- __pendingConsent?: { analytics: boolean; ads: boolean };
18
- setConsent?: (consent: { analytics: boolean; ads: boolean }) => void;
19
- }
20
-
21
- const host = window as unknown as { EscapeNavigator?: EscapeNavigatorGlobal };
17
+ const host = vendors();
22
18
 
23
19
  // Before the loader runs, which reads the pending answer as it starts: its
24
20
  // tags never load on an answer the visitor has not given.
@@ -1,15 +1,12 @@
1
1
  import { appendScript } from "./append-script.ts";
2
+ import { vendors } from "./vendors.ts";
2
3
 
3
4
  /**
4
5
  * Draws each EscapeAll calendar on the page, once EscapeAll's script has
5
6
  * loaded: the call its store scripts make, with the settings the component
6
7
  * wrote on the element.
7
8
  */
8
- // Its script builds selectors by appending to the one it is given, so the
9
- // calendar is named by its id, not handed over as an element.
10
- type Calendar = (selector: string, settings: Record<string, unknown>) => void;
11
-
12
- const host = window as unknown as { GetBookingCalendra?: Calendar };
9
+ const host = vendors();
13
10
 
14
11
  const elements = [...document.querySelectorAll("[data-escapeall-company]")];
15
12
  const [first] = elements;
@@ -1,9 +1,7 @@
1
- import {
2
- GOOGLE_EVENT_GLOBAL,
3
- type GoogleEventSink,
4
- } from "../analytics/google.ts";
1
+ import { GOOGLE_EVENT_GLOBAL } from "../analytics/google.ts";
5
2
  import type { CurrencyCode } from "../currency.ts";
6
3
  import type { DeclaredEvent, EventDataArgs } from "./event-data.ts";
4
+ import { vendors } from "./vendors.ts";
7
5
 
8
6
  /** GA4's recommended events that carry items and revenue. */
9
7
  const ECOMMERCE_EVENTS = [
@@ -110,9 +108,7 @@ export function googleEvent<E extends GoogleEventName>(
110
108
  event: E,
111
109
  ...[data]: GoogleEventArgs<E>
112
110
  ): void {
113
- const send = (
114
- window as unknown as Record<string, GoogleEventSink | undefined>
115
- )[GOOGLE_EVENT_GLOBAL];
111
+ const send = vendors()[GOOGLE_EVENT_GLOBAL];
116
112
  send?.(
117
113
  event,
118
114
  data,