@escape-game-over/atlas 0.1.116 → 0.1.118
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/README.md +1 -1
- package/package.json +2 -2
- package/src/analytics/ids.ts +1 -13
- package/src/analytics/index.ts +18 -18
- package/src/analytics/vendor-settings.ts +162 -0
- package/src/astro/axon-event.ts +2 -4
- package/src/astro/axon-pixel.ts +3 -6
- package/src/astro/clarity-event.ts +2 -4
- package/src/astro/clarity.ts +3 -6
- package/src/astro/consent.ts +3 -6
- package/src/astro/drip-event.ts +2 -4
- package/src/astro/drip.ts +2 -7
- package/src/astro/escape-navigator-consent.ts +2 -6
- package/src/astro/escapeall-calendar.ts +2 -5
- package/src/astro/google-event.ts +3 -7
- package/src/astro/google-map.ts +3 -5
- package/src/astro/meta-event.ts +2 -4
- package/src/astro/meta-pixel.ts +4 -8
- package/src/astro/snap-event.ts +2 -4
- package/src/astro/snap-pixel.ts +3 -6
- package/src/astro/tiktok-event.ts +2 -4
- package/src/astro/tiktok-pixel.ts +70 -39
- package/src/astro/umami-event.ts +2 -6
- package/src/astro/vendors.ts +64 -0
- package/src/index.ts +1 -6
- package/src/routes/family.ts +139 -3
- package/src/site/define.ts +13 -9
- package/src/warn.ts +6 -6
- package/src/analytics/axon-pixel.ts +0 -24
- package/src/analytics/clarity.ts +0 -29
- package/src/analytics/drip.ts +0 -20
- package/src/analytics/meta-pixel.ts +0 -29
- package/src/analytics/snap-pixel.ts +0 -28
- package/src/analytics/tiktok-pixel.ts +0 -29
package/src/astro/snap-pixel.ts
CHANGED
|
@@ -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 =
|
|
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 = (
|
|
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 =
|
|
76
|
+
const global = vendors();
|
|
45
77
|
if (global.ttq !== undefined) return global.ttq;
|
|
46
78
|
global.TiktokAnalyticsObject = "ttq";
|
|
47
|
-
const
|
|
48
|
-
ttq
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
118
|
+
ttq.grantConsent();
|
|
87
119
|
if (loaded) return;
|
|
88
120
|
loaded = true;
|
|
89
121
|
for (const id of pixelIds) ttq.load(id);
|
|
90
|
-
|
|
122
|
+
ttq.page();
|
|
91
123
|
},
|
|
92
124
|
revoke: () => {
|
|
93
|
-
|
|
94
|
-
if (ttq !== undefined) call(ttq, "revokeConsent");
|
|
125
|
+
vendors().ttq?.revokeConsent();
|
|
95
126
|
},
|
|
96
127
|
};
|
|
97
128
|
}
|
package/src/astro/umami-event.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CONSENT_UPDATE_GLOBAL,
|
|
3
|
+
GOOGLE_EVENT_GLOBAL,
|
|
4
|
+
GoogleEventSink,
|
|
5
|
+
} from "../analytics/google.ts";
|
|
6
|
+
import type { Axon } from "./axon-pixel.ts";
|
|
7
|
+
import type { Clarity } from "./clarity.ts";
|
|
8
|
+
import type { ConsentChoices } from "./consent.ts";
|
|
9
|
+
import type { Fbq } from "./meta-pixel.ts";
|
|
10
|
+
import type { Snaptr } from "./snap-pixel.ts";
|
|
11
|
+
import type { Ttq } from "./tiktok-pixel.ts";
|
|
12
|
+
|
|
13
|
+
/** The callback the Maps API calls once it has loaded: `?callback=` names it. */
|
|
14
|
+
export const GOOGLE_MAPS_CALLBACK = "__atlasGoogleMaps";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Every global Atlas's scripts read or define: the vendors' own, and the ones
|
|
18
|
+
* Atlas's head script leaves for its modules.
|
|
19
|
+
*
|
|
20
|
+
* Each is optional, since a vendor's snippet defines it only once it runs,
|
|
21
|
+
* and a blocker can stop that. A file reads them through `vendors()` rather
|
|
22
|
+
* than casting `window` at each site.
|
|
23
|
+
*
|
|
24
|
+
* Not a `declare global`: that would put `window.fbq` in every app's types,
|
|
25
|
+
* on sites without Meta too. These stay Atlas's.
|
|
26
|
+
*/
|
|
27
|
+
interface Vendors {
|
|
28
|
+
fbq?: Fbq;
|
|
29
|
+
_fbq?: Fbq;
|
|
30
|
+
ttq?: Ttq;
|
|
31
|
+
TiktokAnalyticsObject?: string;
|
|
32
|
+
snaptr?: Snaptr;
|
|
33
|
+
axon?: Axon;
|
|
34
|
+
clarity?: Clarity;
|
|
35
|
+
_dcq?: unknown[][];
|
|
36
|
+
_dcs?: { account?: string };
|
|
37
|
+
umami?: { track(event: string, data?: object): void };
|
|
38
|
+
EscapeNavigator?: {
|
|
39
|
+
__pendingConsent?: { analytics: boolean; ads: boolean };
|
|
40
|
+
setConsent?: (consent: { analytics: boolean; ads: boolean }) => void;
|
|
41
|
+
};
|
|
42
|
+
// Its script builds selectors by appending to the one it is given, so the
|
|
43
|
+
// calendar is named by its id, not handed over as an element.
|
|
44
|
+
GetBookingCalendra?: (
|
|
45
|
+
selector: string,
|
|
46
|
+
settings: Record<string, unknown>
|
|
47
|
+
) => void;
|
|
48
|
+
[GOOGLE_MAPS_CALLBACK]?: () => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Atlas's head script's, under the names it is emitted with. */
|
|
52
|
+
type HeadGlobals = {
|
|
53
|
+
[Name in typeof CONSENT_UPDATE_GLOBAL]?: (choices: ConsentChoices) => void;
|
|
54
|
+
} & {
|
|
55
|
+
[Name in typeof GOOGLE_EVENT_GLOBAL]?: GoogleEventSink;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* `window`, typed with the globals above. A function, not a constant, so a
|
|
60
|
+
* module that imports it touches `window` only when it runs in a browser.
|
|
61
|
+
*/
|
|
62
|
+
export function vendors(): Window & Vendors & HeadGlobals {
|
|
63
|
+
return window;
|
|
64
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -188,12 +188,7 @@ export type {
|
|
|
188
188
|
RedirectTarget,
|
|
189
189
|
SitePath,
|
|
190
190
|
} from "./redirects.ts";
|
|
191
|
-
export {
|
|
192
|
-
enableRoutes,
|
|
193
|
-
familyGuard,
|
|
194
|
-
familyMemberSlugs,
|
|
195
|
-
type RouteIdsOf,
|
|
196
|
-
} from "./routes/family.ts";
|
|
191
|
+
export type { RouteFamily, RouteIdsOf } from "./routes/family.ts";
|
|
197
192
|
export { defineSite } from "./site/define.ts";
|
|
198
193
|
export type {
|
|
199
194
|
Crumb,
|
package/src/routes/family.ts
CHANGED
|
@@ -43,15 +43,16 @@ type MemberId<M> = M extends string
|
|
|
43
43
|
* The route ids a family produced.
|
|
44
44
|
*
|
|
45
45
|
* ```ts
|
|
46
|
-
* const
|
|
47
|
-
* export type ChallengeRouteId = RouteIdsOf<typeof
|
|
46
|
+
* const challenges = atlas.family("challenges", CHALLENGES);
|
|
47
|
+
* export type ChallengeRouteId = RouteIdsOf<typeof challenges>;
|
|
48
48
|
* ```
|
|
49
49
|
*
|
|
50
50
|
* Reading them back off the family means the prefix is written once, in the
|
|
51
51
|
* call. Spelling the ids out as `` `challenges-${string}` `` would be a second
|
|
52
52
|
* copy of it, free to drift the day the URL changes.
|
|
53
53
|
*/
|
|
54
|
-
export type RouteIdsOf<Family> =
|
|
54
|
+
export type RouteIdsOf<Family extends { readonly routes: unknown }> =
|
|
55
|
+
StringKeys<Family["routes"]>;
|
|
55
56
|
|
|
56
57
|
/**
|
|
57
58
|
* A URL prefix as a route-id prefix: `"news/posts"` -> `"news-posts"`.
|
|
@@ -349,3 +350,138 @@ export function familyGuard<
|
|
|
349
350
|
return <T extends string>(id: T): id is T & StringKeys<Family> =>
|
|
350
351
|
Object.hasOwn(family, id);
|
|
351
352
|
}
|
|
353
|
+
|
|
354
|
+
/** A family's translated prefix, as `slugs` joins it. */
|
|
355
|
+
type PrefixByOf<Options> = Options extends {
|
|
356
|
+
readonly prefixByLocale: infer P extends ByLocale;
|
|
357
|
+
}
|
|
358
|
+
? P
|
|
359
|
+
: Record<never, never>;
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* A family, and what a project does with it: spread its `routes` into the
|
|
363
|
+
* table, and ask the family — not a string — anything about its members.
|
|
364
|
+
*
|
|
365
|
+
* Every helper is bound to the prefix the family was made with, so the prefix
|
|
366
|
+
* is written once: a project cannot enable `"challenge"` members of a
|
|
367
|
+
* `"challenges"` family, or build a member's path under the wrong prefix.
|
|
368
|
+
*/
|
|
369
|
+
export interface RouteFamily<
|
|
370
|
+
Prefix extends string,
|
|
371
|
+
Member extends string,
|
|
372
|
+
Routes,
|
|
373
|
+
PrefixBy extends ByLocale,
|
|
374
|
+
> {
|
|
375
|
+
/** The member routes, to spread into `atlas.routes`. */
|
|
376
|
+
readonly routes: Routes;
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Whether a route id is one of this family's, as a type guard. See
|
|
380
|
+
* {@link familyGuard} for why it is generic in the input.
|
|
381
|
+
*/
|
|
382
|
+
is<T extends string>(id: T): id is T & StringKeys<Routes>;
|
|
383
|
+
|
|
384
|
+
/** A member's route id: `routeIdOf("blue-room")` is `"rooms-blue-room"`. */
|
|
385
|
+
routeIdOf(member: Member): StringKeys<Routes>;
|
|
386
|
+
|
|
387
|
+
/** The member a route id of this family is the page of. */
|
|
388
|
+
memberOf(routeId: StringKeys<Routes>): Member;
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* One override for many members, for a project's `overrideRoutes`: by
|
|
392
|
+
* default, `{ enabled: true }`. Keys stay literal; see {@link enableRoutes}.
|
|
393
|
+
*
|
|
394
|
+
* Takes any id, not only the family's members, so a helper generic over a
|
|
395
|
+
* venue's rooms can pass them through. A member the family lacks is still
|
|
396
|
+
* an error: on the `overrideRoutes` line, as a route the table lacks.
|
|
397
|
+
*/
|
|
398
|
+
enable<
|
|
399
|
+
const Picked extends readonly (string | { readonly id: string })[],
|
|
400
|
+
const Override extends { readonly enabled?: boolean } = {
|
|
401
|
+
readonly enabled: true;
|
|
402
|
+
},
|
|
403
|
+
>(
|
|
404
|
+
members: Picked,
|
|
405
|
+
override?: Override
|
|
406
|
+
): {
|
|
407
|
+
readonly [M in Picked[number] as `${Dashed<Prefix>}-${MemberId<M> &
|
|
408
|
+
string}`]: Override;
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* A member's paths at a segment of a project's own, in every locale,
|
|
413
|
+
* under the family's prefix and its translations. See
|
|
414
|
+
* {@link familyMemberSlugs}.
|
|
415
|
+
*/
|
|
416
|
+
slugs<
|
|
417
|
+
const Segment extends string,
|
|
418
|
+
const SegmentBy extends ByLocale = Record<never, never>,
|
|
419
|
+
>(
|
|
420
|
+
segment: Segment,
|
|
421
|
+
segmentByLocale?: SegmentBy
|
|
422
|
+
): FamilyMemberSlugs<Prefix, Segment, PrefixBy, SegmentBy>;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** Builds a {@link RouteFamily}: what `atlas.family` returns. */
|
|
426
|
+
export function createFamily<
|
|
427
|
+
const C extends SiteConfigShape,
|
|
428
|
+
const Prefix extends string,
|
|
429
|
+
const Members extends readonly FamilyMemberInput<LocalesOf<C>>[],
|
|
430
|
+
const Options extends FamilyOptions<LocalesOf<C>> = Record<string, never>,
|
|
431
|
+
>(
|
|
432
|
+
config: C,
|
|
433
|
+
prefix: Prefix,
|
|
434
|
+
members: Members,
|
|
435
|
+
options?: Options
|
|
436
|
+
): RouteFamily<
|
|
437
|
+
Prefix,
|
|
438
|
+
MemberId<Members[number]> & string,
|
|
439
|
+
ReturnType<typeof routeFamily<C, Prefix, Members, Options>>,
|
|
440
|
+
PrefixByOf<Options>
|
|
441
|
+
> {
|
|
442
|
+
const routes = routeFamily(config, prefix, members, options);
|
|
443
|
+
const enable = enableRoutes(prefix);
|
|
444
|
+
const ids = members.map((member) =>
|
|
445
|
+
typeof member === "string" ? member : member.id
|
|
446
|
+
);
|
|
447
|
+
const routeId = (member: string) =>
|
|
448
|
+
`${prefix.replaceAll("/", "-")}-${member}`;
|
|
449
|
+
const byRouteId = new Map(ids.map((member) => [routeId(member), member]));
|
|
450
|
+
|
|
451
|
+
return {
|
|
452
|
+
routes,
|
|
453
|
+
is: familyGuard(routes),
|
|
454
|
+
routeIdOf: (member) => {
|
|
455
|
+
if (!ids.includes(member)) {
|
|
456
|
+
throw new Error(
|
|
457
|
+
`"${member}" is not a member of the "${prefix}" family.`
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
return routeId(member) as never;
|
|
461
|
+
},
|
|
462
|
+
memberOf: (id) => {
|
|
463
|
+
const member = byRouteId.get(id);
|
|
464
|
+
if (member === undefined) {
|
|
465
|
+
throw new Error(
|
|
466
|
+
`"${id}" is not a route of the "${prefix}" family.`
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
return member as never;
|
|
470
|
+
},
|
|
471
|
+
enable: (picked, override) => enable(picked, override),
|
|
472
|
+
slugs: (segment, segmentByLocale) => {
|
|
473
|
+
// The join `familyMemberSlugs` makes, with the family's own prefix
|
|
474
|
+
// translations rather than a copy of them.
|
|
475
|
+
const slugByLocale = joinByLocale(
|
|
476
|
+
prefix,
|
|
477
|
+
segment,
|
|
478
|
+
options?.prefixByLocale,
|
|
479
|
+
segmentByLocale
|
|
480
|
+
);
|
|
481
|
+
return {
|
|
482
|
+
slug: `${prefix}/${segment}`,
|
|
483
|
+
...(slugByLocale === undefined ? {} : { slugByLocale }),
|
|
484
|
+
} as never;
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
}
|
package/src/site/define.ts
CHANGED
|
@@ -40,9 +40,9 @@ import {
|
|
|
40
40
|
type ValidateSlugDepth,
|
|
41
41
|
} from "../routes/define.ts";
|
|
42
42
|
import {
|
|
43
|
+
createFamily,
|
|
43
44
|
type FamilyMemberInput,
|
|
44
45
|
type FamilyOptions,
|
|
45
|
-
routeFamily,
|
|
46
46
|
} from "../routes/family.ts";
|
|
47
47
|
import { createSite } from "./create.ts";
|
|
48
48
|
|
|
@@ -56,7 +56,7 @@ import { createSite } from "./create.ts";
|
|
|
56
56
|
*
|
|
57
57
|
* // config/routes.ts, messages.ts, projects/<name>/project.ts, site.ts
|
|
58
58
|
* const rooms = atlas.family("rooms", ROOMS, { enabled: true });
|
|
59
|
-
* export const defaultRoutes = atlas.routes({ ...rooms, home: {…} });
|
|
59
|
+
* export const defaultRoutes = atlas.routes({ ...rooms.routes, home: {…} });
|
|
60
60
|
* export const defaultMessages = atlas.messages({…});
|
|
61
61
|
* export const messages = atlas.catalog(defaultMessages, { locales: […], override: {…} });
|
|
62
62
|
* export const { project, redirects } = atlas.project(messages, defaultRoutes, {…});
|
|
@@ -76,17 +76,21 @@ export function defineSite<
|
|
|
76
76
|
return {
|
|
77
77
|
config,
|
|
78
78
|
|
|
79
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* A route per member, under one prefix: `rooms/red-room` for `rooms`.
|
|
81
|
+
* Spread its `routes` into `routes()`; ask it the rest.
|
|
82
|
+
*/
|
|
80
83
|
family<
|
|
81
84
|
const Prefix extends string,
|
|
82
85
|
const Members extends readonly FamilyMemberInput<L>[],
|
|
83
86
|
const Options extends FamilyOptions<L> = Record<string, never>,
|
|
84
|
-
>(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
>(prefix: Prefix, members: Members, options?: Options) {
|
|
88
|
+
return createFamily<C, Prefix, Members, Options>(
|
|
89
|
+
config,
|
|
90
|
+
prefix,
|
|
91
|
+
members,
|
|
92
|
+
options
|
|
93
|
+
);
|
|
90
94
|
},
|
|
91
95
|
|
|
92
96
|
/** The route table every project starts from. */
|
package/src/warn.ts
CHANGED
|
@@ -43,12 +43,12 @@ const RESET = `${ESC}[0m`;
|
|
|
43
43
|
* `process` and simply gets colour, which is what a browser console renders
|
|
44
44
|
* anyway.
|
|
45
45
|
*/
|
|
46
|
-
const env = () =>
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
const env = () => {
|
|
47
|
+
const host: typeof globalThis & {
|
|
48
|
+
process?: { env?: Readonly<Record<string, string | undefined>> };
|
|
49
|
+
} = globalThis;
|
|
50
|
+
return host.process?.env;
|
|
51
|
+
};
|
|
52
52
|
|
|
53
53
|
// Functions, not values worked out at import: a browser bundle that imports
|
|
54
54
|
// anything near this file would otherwise run the reads on every page.
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { invalidId, UUID, type Uuid } from "./ids.ts";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* AppLovin's Axon pixel. Marketing: it loads only once a visitor grants that.
|
|
5
|
-
*
|
|
6
|
-
* @see https://support.applovin.com/en/growth/promoting-your-websites/axon-pixel-integration/axon-pixel-native-js
|
|
7
|
-
* @see https://support.applovin.com/en/growth/promoting-your-websites/axon-pixel-integration/events-and-objects
|
|
8
|
-
*/
|
|
9
|
-
export interface AxonPixelSettings {
|
|
10
|
-
/** From the Axon dashboard: a UUID. */
|
|
11
|
-
readonly eventKey: Uuid;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/** Fails the build on a key that is not an event key: it would report nowhere. */
|
|
15
|
-
export function checkAxonPixel(
|
|
16
|
-
axon: AxonPixelSettings | undefined,
|
|
17
|
-
at: string
|
|
18
|
-
): void {
|
|
19
|
-
if (axon === undefined || UUID.test(axon.eventKey)) return;
|
|
20
|
-
invalidId(
|
|
21
|
-
at,
|
|
22
|
-
`Axon event key "${axon.eventKey}" is not a UUID, so it would report nowhere. Copy it from the Axon dashboard.`
|
|
23
|
-
);
|
|
24
|
-
}
|
package/src/analytics/clarity.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { CLARITY_PROJECT_ID, invalidId } from "./ids.ts";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Microsoft Clarity. It has a cookieless mode, so like Google it loads for
|
|
5
|
-
* every visitor: without consent it records each page view on its own and sets
|
|
6
|
-
* no cookie, and the visitor's answer switches its cookies on or off.
|
|
7
|
-
*
|
|
8
|
-
* @see https://learn.microsoft.com/en-us/clarity/setup-and-installation/clarity-setup
|
|
9
|
-
* @see https://learn.microsoft.com/en-us/clarity/setup-and-installation/consent-mode
|
|
10
|
-
* @see https://learn.microsoft.com/en-us/clarity/setup-and-installation/clarity-consent-api-v2
|
|
11
|
-
*/
|
|
12
|
-
export interface ClaritySettings {
|
|
13
|
-
/** From the Clarity project's settings: 10 lowercase letters and digits. */
|
|
14
|
-
readonly projectId: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** Fails the build on an id that is not a project id: it would report nowhere. */
|
|
18
|
-
export function checkClarity(
|
|
19
|
-
clarity: ClaritySettings | undefined,
|
|
20
|
-
at: string
|
|
21
|
-
): void {
|
|
22
|
-
if (clarity === undefined || CLARITY_PROJECT_ID.test(clarity.projectId)) {
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
invalidId(
|
|
26
|
-
at,
|
|
27
|
-
`Clarity project id "${clarity.projectId}" is not 10 lowercase letters and digits, so it would report nowhere. Copy it from the Clarity project's settings.`
|
|
28
|
-
);
|
|
29
|
-
}
|
package/src/analytics/drip.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { DRIP_ACCOUNT_ID, invalidId, type NumericId } from "./ids.ts";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Drip's site tracking. Marketing: it loads only once a visitor grants that.
|
|
5
|
-
*
|
|
6
|
-
* @see https://help.drip.com/hc/en-us/articles/4424702539789-Install-Your-JavaScript-Snippet
|
|
7
|
-
*/
|
|
8
|
-
export interface DripSettings {
|
|
9
|
-
/** From Drip's Site Setup: digits, the `_dcs.account` of its snippet. */
|
|
10
|
-
readonly accountId: NumericId;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/** Fails the build on an id that is not an account id: it would report nowhere. */
|
|
14
|
-
export function checkDrip(drip: DripSettings | undefined, at: string): void {
|
|
15
|
-
if (drip === undefined || DRIP_ACCOUNT_ID.test(drip.accountId)) return;
|
|
16
|
-
invalidId(
|
|
17
|
-
at,
|
|
18
|
-
`Drip account id "${drip.accountId}" is not all digits, so it would report nowhere. Copy it from Drip's Site Setup.`
|
|
19
|
-
);
|
|
20
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import type { NonEmpty } from "../types.ts";
|
|
2
|
-
import { invalidId, META_PIXEL_ID, type NumericId } 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
|
-
/** Fails the build on an id that is not a pixel id: it would report nowhere. */
|
|
17
|
-
export function checkMetaPixel(
|
|
18
|
-
meta: MetaPixelSettings | undefined,
|
|
19
|
-
at: string
|
|
20
|
-
): void {
|
|
21
|
-
for (const id of meta?.pixelIds ?? []) {
|
|
22
|
-
if (!META_PIXEL_ID.test(id)) {
|
|
23
|
-
invalidId(
|
|
24
|
-
at,
|
|
25
|
-
`Meta pixel id "${id}" is not 15 or 16 digits, so it would report nowhere. Copy it from Events Manager.`
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { NonEmpty } from "../types.ts";
|
|
2
|
-
import { invalidId, UUID, type Uuid } from "./ids.ts";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Snapchat's pixel. Marketing: it loads only once a visitor grants that.
|
|
6
|
-
*
|
|
7
|
-
* @see https://businesshelp.snapchat.com/s/article/pixel-direct-implementation
|
|
8
|
-
* @see https://forbusiness.snapchat.com/en-US/blog/the-snap-pixel-how-it-works-and-how-to-install-it
|
|
9
|
-
*/
|
|
10
|
-
export interface SnapPixelSettings {
|
|
11
|
-
/** From Snap's Events Manager: a UUID. */
|
|
12
|
-
readonly pixelIds: NonEmpty<Uuid>;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** Fails the build on an id that is not a pixel id: it would report nowhere. */
|
|
16
|
-
export function checkSnapPixel(
|
|
17
|
-
snapchat: SnapPixelSettings | undefined,
|
|
18
|
-
at: string
|
|
19
|
-
): void {
|
|
20
|
-
for (const id of snapchat?.pixelIds ?? []) {
|
|
21
|
-
if (!UUID.test(id)) {
|
|
22
|
-
invalidId(
|
|
23
|
-
at,
|
|
24
|
-
`Snap pixel id "${id}" is not a UUID, so it would report nowhere. Copy it from Snap's Events Manager.`
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|