@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
@@ -6,6 +6,7 @@ import { reportDevError } from "./dev-log.ts";
6
6
  import { element } from "./element.ts";
7
7
  import { triggered } from "./load-script.ts";
8
8
  import { data } from "./ref.ts";
9
+ import { GOOGLE_MAPS_CALLBACK, vendors } from "./vendors.ts";
9
10
 
10
11
  /** What `GoogleMap.astro` hands the element, serialised. */
11
12
  export interface GoogleMapConfig {
@@ -27,20 +28,17 @@ export interface GoogleMapConfig {
27
28
  }[];
28
29
  }
29
30
 
30
- const CALLBACK = "__atlasGoogleMaps";
31
-
32
31
  let loading: Promise<typeof google.maps> | undefined;
33
32
 
34
33
  /** The Maps JavaScript API, fetched once per page however many maps ask. */
35
34
  export function loadGoogleMaps(apiKey: string): Promise<typeof google.maps> {
36
35
  loading ??= new Promise((resolve, reject) => {
37
- (window as unknown as Record<string, () => void>)[CALLBACK] = () =>
38
- resolve(google.maps);
36
+ vendors()[GOOGLE_MAPS_CALLBACK] = () => resolve(google.maps);
39
37
  const params = {
40
38
  key: apiKey,
41
39
  v: "weekly",
42
40
  loading: "async",
43
- callback: CALLBACK,
41
+ callback: GOOGLE_MAPS_CALLBACK,
44
42
  };
45
43
  appendScript(
46
44
  `https://maps.googleapis.com/maps/api/js?${new URLSearchParams(params)}`
@@ -331,36 +331,22 @@ export async function photoSet(
331
331
  export type ImagePathOptions = Omit<Parameters<typeof getImage>[0], "src">;
332
332
 
333
333
  /**
334
- * The built URL of an optimised image, for the places that take a path rather
335
- * than an `<img>` — a JSON-LD node, a `<link>`, a manifest.
334
+ * The built URL of an optimised image, for places that take a path rather than
335
+ * an `<img>`: a CSS `url()` in an inline style or custom property, a
336
+ * `<link rel="preload">`, a manifest icon.
336
337
  *
337
- * ```ts
338
- * import logo from "~/assets/logo.png";
339
- * import { imagePath } from "@escape-game-over/atlas/astro/images";
340
- *
341
- * const company = organization({
342
- * logo: await imagePath(logo, { width: 512, height: 512, format: "png" }),
343
- * // …
344
- * });
338
+ * ```astro
339
+ * <section style={`--hero: url(${await imagePath(hero, { width: 1600, format: "webp" })})`}>
345
340
  * ```
346
341
  *
347
- * The narrowing is the reason this is a function rather than three lines at
348
- * each call site. `getImage().src` is typed `string`, every field it feeds here
349
- * is typed `UrlPath`, and a cast at each of those would put the same unchecked
350
- * assumption in as many places as there are consumers.
351
- *
352
- * **Not for entity data.** What comes back is `/_astro/logo.<hash>.png`, and the
353
- * hash moves every time the source file is touched. That is right for a page
354
- * resource, which is re-fetched from a document that was just re-fetched, and
355
- * wrong for anything a search engine stores against an identity — an
356
- * `Organization.logo` above all, which is held long after the build that
357
- * produced it. Those want a stable path out of `public/`, written as `/logo.png`
358
- * and joined to the site's own origin; both examples in this repository do that
359
- * deliberately and say so at the call site.
342
+ * A function rather than `getImage().src` at each call site because that is
343
+ * typed `string`, and a path is wanted as `UrlPath`: narrowed once here, with
344
+ * no cast at every caller.
360
345
  *
361
- * So the fit is: a `<link rel="preload">`, a manifest icon, an `<img>` built by
362
- * hand — resources fetched *with* the page. Long-lived references belong in
363
- * `public/`.
346
+ * **Not for structured data.** What comes back is `/_astro/hero.<hash>.webp`,
347
+ * and the hash moves whenever the source changes. Right for a resource fetched
348
+ * with the page; wrong for an `Organization.logo` or anything else a search
349
+ * engine stores against an identity, which wants a stable path in `public/`.
364
350
  */
365
351
  export async function imagePath(
366
352
  src: ImageMetadata,
@@ -27,12 +27,5 @@
27
27
  */
28
28
 
29
29
  export { siteFonts } from "./fonts.ts";
30
- export {
31
- type PublicFilesOptions,
32
- publicFiles,
33
- } from "./public-files.ts";
34
- export {
35
- type SiteFiles,
36
- type SiteRoutesOptions,
37
- siteRoutes,
38
- } from "./site-routes.ts";
30
+ export { publicFiles } from "./public-files.ts";
31
+ export { siteRoutes } from "./site-routes.ts";
@@ -1,5 +1,6 @@
1
1
  import type { CurrencyCode } from "../currency.ts";
2
2
  import type { DeclaredEvent, EventDataArgs } from "./event-data.ts";
3
+ import { vendors } from "./vendors.ts";
3
4
 
4
5
  /** Meta's standard events, which its ad optimisation understands by name. */
5
6
  const META_STANDARD_EVENTS = [
@@ -66,9 +67,6 @@ type MetaEventArgs<E extends MetaEventName> = E extends MetaStandardEvent
66
67
  : EventDataArgs<MetaCustomEvents[E & keyof MetaCustomEvents]>;
67
68
 
68
69
  /** What `meta-pixel.ts` defines once marketing is granted. */
69
- interface MetaWindow {
70
- fbq?: (...args: unknown[]) => void;
71
- }
72
70
 
73
71
  /**
74
72
  * Records an event with Meta's pixel — a lead sent, a booking made. A declared
@@ -83,7 +81,7 @@ export function metaEvent<E extends MetaEventName>(
83
81
  event: E,
84
82
  ...[data]: MetaEventArgs<E>
85
83
  ): void {
86
- const fbq = (window as unknown as MetaWindow).fbq;
84
+ const fbq = vendors().fbq;
87
85
  const method = (META_STANDARD_EVENTS as readonly string[]).includes(event)
88
86
  ? "track"
89
87
  : "trackCustom";
@@ -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
  /** Meta's `fbq`: a queue until `fbevents.js` arrives and replays it. */
5
- interface Fbq {
6
+ export interface Fbq {
6
7
  (...args: unknown[]): void;
7
8
  callMethod?: (...args: unknown[]) => void;
8
9
  queue: unknown[];
@@ -11,16 +12,11 @@ interface Fbq {
11
12
  version: string;
12
13
  }
13
14
 
14
- interface PixelWindow {
15
- fbq?: Fbq;
16
- _fbq?: Fbq;
17
- }
18
-
19
15
  const SCRIPT = "https://connect.facebook.net/en_US/fbevents.js";
20
16
 
21
17
  /** Meta's own base code, as it defines `fbq` before the script loads. */
22
18
  function defineFbq(): Fbq {
23
- const global = window as unknown as PixelWindow;
19
+ const global = vendors();
24
20
  if (global.fbq !== undefined) return global.fbq;
25
21
  // `arguments` is the list of whatever a call passed, which every regular
26
22
  // `function` gets without declaring it (an arrow function does not). The
@@ -57,7 +53,7 @@ export function metaPixel(pixelIds: readonly string[]): ConsentGated {
57
53
  appendScript(SCRIPT);
58
54
  },
59
55
  revoke: () => {
60
- (window as unknown as PixelWindow).fbq?.("consent", "revoke");
56
+ vendors().fbq?.("consent", "revoke");
61
57
  },
62
58
  };
63
59
  }
@@ -2,13 +2,8 @@ import type { Dirent } from "node:fs";
2
2
  import { readdir } from "node:fs/promises";
3
3
  import type { AstroIntegration } from "astro";
4
4
 
5
- /**
6
- * The module the generated union is declared in.
7
- *
8
- * Types only, so nothing has to exist at runtime: a consumer reaches it with
9
- * `import type`, which is erased before anything tries to resolve it.
10
- */
11
- const PUBLIC_MODULE = "atlas:public";
5
+ /** The package the generated declaration merges into, as consumers import it. */
6
+ const LIB_MODULE = "@escape-game-over/atlas";
12
7
 
13
8
  /** Files in `public/` that are not content: editor litter and placeholders. */
14
9
  const IGNORED = new Set([".DS_Store", "Thumbs.db", ".gitkeep"]);
@@ -38,13 +33,9 @@ async function walk(dir: URL, prefix = ""): Promise<string[]> {
38
33
  }
39
34
 
40
35
  /**
41
- * Types every file in `public/`, so linking to one is checked.
42
- *
43
- * ```ts
44
- * import type { PublicFile } from "atlas:public";
45
- *
46
- * const statement: PublicFile = "/reports/annual-2026.pdf";
47
- * ```
36
+ * Types every file in `public/`, so linking to one is checked: `site.fileUrl()`
37
+ * and a redirect's `{ file }` target take `PublicFilePath`, which this narrows
38
+ * from any path to the files that are there.
48
39
  *
49
40
  * `public/` is the one part of a site the compiler cannot see: files are copied
50
41
  * to the output verbatim, so a link to one is a string nothing verifies, and a
@@ -56,45 +47,19 @@ async function walk(dir: URL, prefix = ""): Promise<string[]> {
56
47
  * commit or add to a tsconfig. A file added while the dev server runs is picked
57
48
  * up when it restarts.
58
49
  */
59
- export interface PublicFilesOptions {
60
- /**
61
- * How the consuming project imports this package.
62
- *
63
- * Defaults to the published name, which is what a project installing it
64
- * from the registry writes. State it only where that is not the specifier
65
- * in the import statements — a path alias, or a fork under another name.
66
- *
67
- * Needed because the generated file *augments* that module: the union has to
68
- * reach `fileUrl` and the redirect target, and a declaration can only be
69
- * merged into a module named the way the consumer names it.
70
- */
71
- readonly libModule?: string;
72
- }
73
-
74
- export function publicFiles(
75
- options: PublicFilesOptions = {}
76
- ): AstroIntegration {
77
- const libModule = options.libModule ?? "@escape-game-over/atlas";
78
-
50
+ export function publicFiles(): AstroIntegration {
79
51
  return {
80
52
  name: "public-files",
81
53
  hooks: {
82
54
  "astro:config:done": async ({ config, injectTypes, logger }) => {
83
55
  const files = (await walk(config.publicDir)).sort();
84
- /**
85
- * The union, one file per line, indented by `spaces`.
86
- *
87
- * A count rather than the whitespace itself: the two call sites
88
- * sit at different depths in the emitted file, and passing the
89
- * indent as a literal means counting spaces by eye in a string
90
- * that looks identical whether it holds eight of them or nine.
91
- */
92
- const union = (spaces: number) =>
56
+ // One file per line, at the depth the declaration sits.
57
+ const union =
93
58
  files.length === 0
94
59
  ? "never"
95
60
  : files
96
61
  .map((file) => `"${file}"`)
97
- .join(`\n${" ".repeat(spaces)}| `);
62
+ .join("\n | ");
98
63
 
99
64
  injectTypes({
100
65
  filename: "public-files.d.ts",
@@ -105,17 +70,11 @@ export function publicFiles(
105
70
  // export lib has.
106
71
  content: `export {};
107
72
 
108
- declare module "${PUBLIC_MODULE}" {
109
- /** Every file in \`public/\`, as it is served. */
110
- export type PublicFile =
111
- | ${union(8)};
112
- }
113
-
114
- declare module "${libModule}" {
73
+ declare module "${LIB_MODULE}" {
115
74
  /** Merged so \`fileUrl\` and redirect targets accept only these. */
116
75
  interface PublicFileRegistry {
117
76
  path:
118
- | ${union(12)};
77
+ | ${union};
119
78
  }
120
79
  }
121
80
  `,
@@ -119,16 +119,11 @@ export function siteRoutes<Id extends string, L extends string>(
119
119
  const describe = (file: GeneratedFile): string =>
120
120
  `${file.name} (${size(file.body)})`;
121
121
 
122
- // Rebuilt per request in dev and once at the end of a build, rather than
123
- // computed here.
124
- //
125
- // Worth knowing what that does *not* buy: it does not make dev current. The
126
- // `site` and the `llms` file this closes over were built when the Astro
127
- // config was evaluated, and nothing re-evaluates that until the server
128
- // restarts — so rebuilding per request derives the same answer from the same
129
- // frozen inputs. Measured: editing a message refreshes the page that renders
130
- // it and leaves the same string in `llms.txt` untouched. A build is always
131
- // right, since it evaluates the config in a fresh process.
122
+ // Built when the dev server starts and when a build ends. The `site` and
123
+ // `llms` this closes over are fixed when the Astro config is evaluated, so
124
+ // in dev these files change only on a restart: editing a message refreshes
125
+ // the page that renders it, not `llms.txt`. A build is always right, since
126
+ // it evaluates the config in a fresh process.
132
127
  const generate = (): GeneratedFile[] => {
133
128
  const files: GeneratedFile[] = [...site.sitemap().files, site.robots()];
134
129
  // Every page's head links to it, so one is written unless the config
@@ -352,35 +347,11 @@ export function siteRoutes<Id extends string, L extends string>(
352
347
  * them out of the page graph.
353
348
  */
354
349
  "astro:server:setup": ({ server, logger }) => {
355
- /**
356
- * The paths this integration answers on, learned by generating
357
- * once and refreshed whenever we generate again.
358
- *
359
- * Its whole job is to keep `generate()` off the hot path. The
360
- * middleware sees every request the dev server takes — each
361
- * page, each asset, each Vite ping — and regenerating for all
362
- * of them rebuilds the sitemap three times over (once directly,
363
- * once inside `robots()`, once inside `llms()`) to answer a
364
- * request for `/favicon.ico`.
365
- *
366
- * A name that appears only later is the one thing this can
367
- * miss: a sitemap crossing `entryLimit` mid-session gains
368
- * `sitemap-0.xml` and friends, which are not in the set until
369
- * something regenerates. Acceptable because the set is
370
- * refreshed on every hit and the dev server restarts on a
371
- * config change — and because the alternative is paying for a
372
- * full generate on every asset request to catch it.
373
- */
374
- const served = new Set<string>();
375
- const remember = (files: readonly GeneratedFile[]) => {
376
- served.clear();
377
- for (const file of files) served.add(`/${file.name}`);
378
- return files;
379
- };
380
-
381
- remember(generate()).map((file) =>
382
- logger.info(`generated ${describe(file)}`)
383
- );
350
+ const files = new Map<string, GeneratedFile>();
351
+ for (const file of generate()) {
352
+ logger.info(`generated ${describe(file)}`);
353
+ files.set(`/${file.name}`, file);
354
+ }
384
355
  server.middlewares.use((request, response, next) => {
385
356
  const path = (request.url ?? "").split("?")[0] ?? "";
386
357
 
@@ -406,23 +377,8 @@ export function siteRoutes<Id extends string, L extends string>(
406
377
  return;
407
378
  }
408
379
 
409
- // Nothing of ours sits at this path, so hand it on without
410
- // building anything. This is the check that keeps the cost
411
- // below off every page and asset request in dev.
412
- if (!served.has(path)) {
413
- next();
414
- return;
415
- }
416
-
417
- // Regenerated per request, so an edit to a route or a
418
- // message shows up on reload rather than at restart.
419
- const file = remember(generate()).find(
420
- (candidate) => `/${candidate.name}` === path
421
- );
380
+ const file = files.get(path);
422
381
  if (file === undefined) {
423
- // The set said this was ours and the fresh build
424
- // disagrees — a file that stopped being generated, so
425
- // it is now a 404 like any other missing path.
426
382
  next();
427
383
  return;
428
384
  }
@@ -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
  /** Snap's standard events that a website sends. */
5
6
  export type SnapStandardEvent =
@@ -37,9 +38,6 @@ type SnapEventArgs<E extends SnapStandardEvent> = E extends "PURCHASE"
37
38
  : [data?: SnapEventData];
38
39
 
39
40
  /** What `snap-pixel.ts` defines once marketing is granted. */
40
- interface SnapWindow {
41
- snaptr?: (...args: unknown[]) => void;
42
- }
43
41
 
44
42
  /**
45
43
  * Records a standard event with Snap's pixel — a lead sent, a booking made.
@@ -53,7 +51,7 @@ export function snapEvent<E extends SnapStandardEvent>(
53
51
  ...[data]: SnapEventArgs<E>
54
52
  ): void {
55
53
  if (readConsent()?.marketing !== "granted") return;
56
- const snaptr = (window as unknown as SnapWindow).snaptr;
54
+ const snaptr = vendors().snaptr;
57
55
  if (data === undefined) snaptr?.("track", event);
58
56
  else snaptr?.("track", event, data);
59
57
  }
@@ -1,22 +1,19 @@
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
  /** Snap's `snaptr`: a queue until `scevent.min.js` arrives and replays it. */
5
- interface Snaptr {
6
+ export interface Snaptr {
6
7
  (...args: unknown[]): void;
7
8
  handleRequest?: (...args: unknown[]) => void;
8
9
  queue: unknown[];
9
10
  }
10
11
 
11
- interface PixelWindow {
12
- snaptr?: Snaptr;
13
- }
14
-
15
12
  const SCRIPT = "https://sc-static.net/scevent.min.js";
16
13
 
17
14
  /** Snap's own base code, as it defines `snaptr` before the script loads. */
18
15
  function defineSnaptr(): Snaptr {
19
- const global = window as unknown as PixelWindow;
16
+ const global = vendors();
20
17
  if (global.snaptr !== undefined) return global.snaptr;
21
18
  // `arguments` is the list of whatever a call passed, which every regular
22
19
  // `function` gets without declaring it (an arrow function does not). The
@@ -1,5 +1,6 @@
1
1
  import type { CurrencyCode } from "../currency.ts";
2
2
  import type { DeclaredEvent, EventDataArgs } from "./event-data.ts";
3
+ import { vendors } from "./vendors.ts";
3
4
 
4
5
  /** TikTok's standard events, by their current names (May 2025 onwards). */
5
6
  export type TikTokStandardEvent =
@@ -63,9 +64,6 @@ type TikTokEventArgs<E extends TikTokEventName> = E extends TikTokStandardEvent
63
64
  : EventDataArgs<TikTokCustomEvents[E & keyof TikTokCustomEvents]>;
64
65
 
65
66
  /** What `tiktok-pixel.ts` defines once marketing is granted. */
66
- interface TikTokWindow {
67
- ttq?: { track(event: string, data?: object): void };
68
- }
69
67
 
70
68
  /**
71
69
  * Records an event with TikTok's pixel — a lead sent, a booking made. Declared
@@ -80,7 +78,7 @@ export function tiktokEvent<E extends TikTokEventName>(
80
78
  event: E,
81
79
  ...[data]: TikTokEventArgs<E>
82
80
  ): void {
83
- const ttq = (window as unknown as TikTokWindow).ttq;
81
+ const ttq = vendors().ttq;
84
82
  if (data === undefined) ttq?.track(event);
85
83
  else ttq?.track(event, data);
86
84
  }
@@ -1,8 +1,15 @@
1
1
  import { appendScript } from "./append-script.ts";
2
2
  import type { ConsentGated } from "./consent-gate.ts";
3
+ import { vendors } from "./vendors.ts";
4
+
5
+ /** What a page calls on `ttq`: each queues, until `events.js` takes over. */
6
+ type TtqMethods = Record<
7
+ (typeof METHODS)[number],
8
+ (...args: unknown[]) => void
9
+ >;
3
10
 
4
11
  /** TikTok's `ttq`: a queue of calls until `events.js` arrives and replays it. */
5
- interface Ttq extends Array<unknown> {
12
+ export interface Ttq extends Array<unknown>, TtqMethods {
6
13
  methods: readonly string[];
7
14
  setAndDefer(target: unknown[], method: string): void;
8
15
  instance(id: string): unknown[];
@@ -10,12 +17,6 @@ interface Ttq extends Array<unknown> {
10
17
  _i: Record<string, unknown[] & { _u?: string }>;
11
18
  _t: Record<string, number>;
12
19
  _o: Record<string, object>;
13
- [method: string]: unknown;
14
- }
15
-
16
- interface PixelWindow {
17
- ttq?: Ttq;
18
- TiktokAnalyticsObject?: string;
19
20
  }
20
21
 
21
22
  const SCRIPT = "https://analytics.tiktok.com/i18n/pixel/events.js";
@@ -39,42 +40,73 @@ const METHODS = [
39
40
  "grantConsent",
40
41
  ] as const;
41
42
 
43
+ /**
44
+ * Each method, queuing its call on `target`. Written out rather than looped
45
+ * over `METHODS`, so the type sees every one: the return type rejects a name
46
+ * missing here, or one `METHODS` lacks.
47
+ */
48
+ function deferTo(target: unknown[]): TtqMethods {
49
+ const defer =
50
+ (method: string) =>
51
+ (...args: unknown[]) => {
52
+ target.push([method, ...args]);
53
+ };
54
+ return {
55
+ page: defer("page"),
56
+ track: defer("track"),
57
+ identify: defer("identify"),
58
+ instances: defer("instances"),
59
+ debug: defer("debug"),
60
+ on: defer("on"),
61
+ off: defer("off"),
62
+ once: defer("once"),
63
+ ready: defer("ready"),
64
+ alias: defer("alias"),
65
+ group: defer("group"),
66
+ enableCookie: defer("enableCookie"),
67
+ disableCookie: defer("disableCookie"),
68
+ holdConsent: defer("holdConsent"),
69
+ revokeConsent: defer("revokeConsent"),
70
+ grantConsent: defer("grantConsent"),
71
+ };
72
+ }
73
+
42
74
  /** TikTok's own base code, as it defines `ttq` before the script loads. */
43
75
  function defineTtq(): Ttq {
44
- const global = window as unknown as PixelWindow;
76
+ const global = vendors();
45
77
  if (global.ttq !== undefined) return global.ttq;
46
78
  global.TiktokAnalyticsObject = "ttq";
47
- const ttq = [] as unknown as Ttq;
48
- ttq.methods = METHODS;
49
- ttq.setAndDefer = (target, method) => {
50
- (target as unknown as Record<string, unknown>)[method] = (
51
- ...args: unknown[]
52
- ) => target.push([method, ...args]);
53
- };
54
- for (const method of METHODS) ttq.setAndDefer(ttq, method);
55
- ttq.instance = (id) => {
56
- const queue = ttq._i?.[id] ?? [];
57
- for (const method of METHODS) ttq.setAndDefer(queue, method);
58
- return queue;
59
- };
60
- ttq.load = (id, options = {}) => {
61
- ttq._i ??= {};
62
- ttq._i[id] = Object.assign([], { _u: SCRIPT });
63
- ttq._t ??= {};
64
- ttq._t[id] = Date.now();
65
- ttq._o ??= {};
66
- ttq._o[id] = options;
67
- appendScript(
68
- `${SCRIPT}?${new URLSearchParams({ sdkid: id, lib: "ttq" })}`
69
- );
70
- };
79
+ const queue: unknown[] = [];
80
+ const ttq: Ttq = Object.assign(queue, deferTo(queue), {
81
+ methods: METHODS,
82
+ // TikTok's script calls this itself, with any name, so it stays as
83
+ // loose as the snippet's.
84
+ setAndDefer: (target: unknown[], method: string) => {
85
+ Object.assign(target, {
86
+ [method]: (...args: unknown[]) =>
87
+ target.push([method, ...args]),
88
+ });
89
+ },
90
+ instance: (id: string) => {
91
+ const instance = ttq._i[id] ?? [];
92
+ return Object.assign(instance, deferTo(instance));
93
+ },
94
+ load: (id: string, options: object = {}) => {
95
+ ttq._i[id] = Object.assign([], { _u: SCRIPT });
96
+ ttq._t[id] = Date.now();
97
+ ttq._o[id] = options;
98
+ appendScript(
99
+ `${SCRIPT}?${new URLSearchParams({ sdkid: id, lib: "ttq" })}`
100
+ );
101
+ },
102
+ _i: {},
103
+ _t: {},
104
+ _o: {},
105
+ });
71
106
  global.ttq = ttq;
72
107
  return ttq;
73
108
  }
74
109
 
75
- const call = (ttq: Ttq, method: (typeof METHODS)[number], ...args: unknown[]) =>
76
- (ttq[method] as (...args: unknown[]) => void)(...args);
77
-
78
110
  /** The pixel: loaded on the first grant, paused and resumed by later answers. */
79
111
  export function tiktokPixel(pixelIds: readonly string[]): ConsentGated {
80
112
  let loaded = false;
@@ -83,15 +115,14 @@ export function tiktokPixel(pixelIds: readonly string[]): ConsentGated {
83
115
  const ttq = defineTtq();
84
116
  // Also lifts the hold of TikTok's Cookie Consent Mode, where a
85
117
  // pixel has it on in Events Manager.
86
- call(ttq, "grantConsent");
118
+ ttq.grantConsent();
87
119
  if (loaded) return;
88
120
  loaded = true;
89
121
  for (const id of pixelIds) ttq.load(id);
90
- call(ttq, "page");
122
+ ttq.page();
91
123
  },
92
124
  revoke: () => {
93
- const ttq = (window as unknown as PixelWindow).ttq;
94
- if (ttq !== undefined) call(ttq, "revokeConsent");
125
+ vendors().ttq?.revokeConsent();
95
126
  },
96
127
  };
97
128
  }
@@ -1,4 +1,5 @@
1
1
  import type { DeclaredEvent, EventDataArgs } from "./event-data.ts";
2
+ import { vendors } from "./vendors.ts";
2
3
 
3
4
  /**
4
5
  * The project's Umami events, each with the data it carries, `undefined` for
@@ -34,10 +35,5 @@ export function umamiEvent<E extends DeclaredEvent<UmamiEvents>>(
34
35
  // Flat string values; anything else in a declaration fails here.
35
36
  ...[data]: EventDataArgs<UmamiEvents[E] & Readonly<Record<string, string>>>
36
37
  ): void {
37
- const umami = (
38
- window as unknown as {
39
- umami?: { track(event: string, data?: object): void };
40
- }
41
- ).umami;
42
- umami?.track(name, data);
38
+ vendors().umami?.track(name, data);
43
39
  }