@buildinams/core 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +24 -1
  2. package/dist/analytics/google/index.d.ts +158 -0
  3. package/dist/analytics/google/index.js +274 -0
  4. package/dist/analytics/google/index.js.map +1 -0
  5. package/dist/analytics/index.d.ts +101 -0
  6. package/dist/analytics/index.js +384 -0
  7. package/dist/analytics/index.js.map +1 -0
  8. package/dist/analytics/shopify/index.d.ts +186 -0
  9. package/dist/analytics/shopify/index.js +175 -0
  10. package/dist/analytics/shopify/index.js.map +1 -0
  11. package/dist/{chunk-5KDGGGVO.js → chunk-FJ7SNQQI.js} +24 -6
  12. package/dist/chunk-FJ7SNQQI.js.map +1 -0
  13. package/dist/chunk-G2BKC4BU.js +8 -0
  14. package/dist/chunk-G2BKC4BU.js.map +1 -0
  15. package/dist/chunk-GQVN2ECX.js +14 -0
  16. package/dist/chunk-GQVN2ECX.js.map +1 -0
  17. package/dist/chunk-LL3VTFNY.js +22 -0
  18. package/dist/chunk-LL3VTFNY.js.map +1 -0
  19. package/dist/chunk-M3HNM5HC.js +48 -0
  20. package/dist/chunk-M3HNM5HC.js.map +1 -0
  21. package/dist/chunk-MHFGMH3G.js +11 -0
  22. package/dist/chunk-MHFGMH3G.js.map +1 -0
  23. package/dist/chunk-OS2T6P2T.js +50 -0
  24. package/dist/chunk-OS2T6P2T.js.map +1 -0
  25. package/dist/chunk-QIJGG2F4.js +51 -0
  26. package/dist/chunk-QIJGG2F4.js.map +1 -0
  27. package/dist/consent/cookiebot/index.d.ts +16 -0
  28. package/dist/consent/cookiebot/index.js +27 -0
  29. package/dist/consent/cookiebot/index.js.map +1 -0
  30. package/dist/consent/google/index.d.ts +23 -0
  31. package/dist/consent/google/index.js +48 -0
  32. package/dist/consent/google/index.js.map +1 -0
  33. package/dist/consent/index.d.ts +1 -0
  34. package/dist/consent/index.js +3 -0
  35. package/dist/consent/index.js.map +1 -0
  36. package/dist/consent/onetrust/index.d.ts +23 -0
  37. package/dist/consent/onetrust/index.js +39 -0
  38. package/dist/consent/onetrust/index.js.map +1 -0
  39. package/dist/consent/shopify/index.d.ts +44 -0
  40. package/dist/consent/shopify/index.js +84 -0
  41. package/dist/consent/shopify/index.js.map +1 -0
  42. package/dist/cookies/index.d.ts +230 -0
  43. package/dist/cookies/index.js +188 -0
  44. package/dist/cookies/index.js.map +1 -0
  45. package/dist/define-events-CxZjoUlG.d.ts +255 -0
  46. package/dist/destination-5KJ5PeR3.d.ts +122 -0
  47. package/dist/index-Zef1nnTP.d.ts +36 -0
  48. package/dist/index.d.ts +2 -2
  49. package/dist/index.js +64 -1
  50. package/dist/index.js.map +1 -1
  51. package/dist/listeners-DkU-vBPa.d.ts +15 -0
  52. package/dist/navigation/index.js +1 -1
  53. package/dist/schema/index.d.ts +2 -0
  54. package/dist/schema/index.js +1 -1
  55. package/dist/store/index.js +1 -1
  56. package/dist/tracking/cart/index.d.ts +74 -0
  57. package/dist/tracking/cart/index.js +118 -0
  58. package/dist/tracking/cart/index.js.map +1 -0
  59. package/dist/tracking/dom/index.d.ts +320 -0
  60. package/dist/tracking/dom/index.js +1196 -0
  61. package/dist/tracking/dom/index.js.map +1 -0
  62. package/docs/analytics.md +883 -0
  63. package/docs/cookies.md +354 -0
  64. package/docs/decisions/001-analytics-project-events.md +31 -0
  65. package/docs/decisions/002-tracking-dom-opt-in.md +29 -0
  66. package/docs/decisions/003-tracking-element-data.md +27 -0
  67. package/docs/decisions/004-cookies-explicit-store.md +26 -0
  68. package/docs/decisions/005-cookies-invalid-reads-fall-back.md +27 -0
  69. package/docs/decisions/006-cookies-server-only-types.md +19 -0
  70. package/docs/decisions/007-cookies-options-on-definition.md +25 -0
  71. package/docs/decisions/008-cookies-in-vertical-slices.md +24 -0
  72. package/docs/decisions/009-analytics-consent-targets.md +29 -0
  73. package/docs/decisions/010-analytics-destination-contract.md +29 -0
  74. package/docs/decisions/011-tracking-decoupled.md +30 -0
  75. package/docs/tracking.md +221 -0
  76. package/package.json +83 -2
  77. package/dist/chunk-5KDGGGVO.js.map +0 -1
  78. package/dist/chunk-SIU7SKJK.js +0 -139
  79. package/dist/chunk-SIU7SKJK.js.map +0 -1
@@ -0,0 +1,188 @@
1
+ // src/cookies/cookie-header.ts
2
+ var decode = (value) => {
3
+ try {
4
+ return decodeURIComponent(value);
5
+ } catch {
6
+ return value;
7
+ }
8
+ };
9
+ var parseCookieHeader = (header) => {
10
+ const cookies = /* @__PURE__ */ new Map();
11
+ for (const pair of header.split(";")) {
12
+ const separatorIndex = pair.indexOf("=");
13
+ if (separatorIndex === -1) continue;
14
+ const name = pair.slice(0, separatorIndex).trim();
15
+ if (!name || cookies.has(name)) continue;
16
+ cookies.set(name, decode(pair.slice(separatorIndex + 1).trim()));
17
+ }
18
+ return cookies;
19
+ };
20
+ var SAME_SITE = { lax: "Lax", strict: "Strict", none: "None" };
21
+ var PRIORITY = { low: "Low", medium: "Medium", high: "High" };
22
+ var serializeCookie = (name, value, options = {}) => {
23
+ const parts = [`${name}=${encodeURIComponent(value)}`];
24
+ parts.push(`Path=${options.path ?? "/"}`);
25
+ if (options.domain) parts.push(`Domain=${options.domain}`);
26
+ if (typeof options.maxAge === "number") {
27
+ parts.push(`Max-Age=${Math.floor(options.maxAge)}`);
28
+ }
29
+ const expires = options.expires !== void 0 ? new Date(options.expires) : typeof options.maxAge === "number" ? new Date(Date.now() + options.maxAge * 1e3) : void 0;
30
+ if (expires) parts.push(`Expires=${expires.toUTCString()}`);
31
+ if (options.secure) parts.push("Secure");
32
+ if (options.httpOnly) parts.push("HttpOnly");
33
+ if (options.sameSite) parts.push(`SameSite=${SAME_SITE[options.sameSite]}`);
34
+ if (options.partitioned) parts.push("Partitioned");
35
+ if (options.priority) parts.push(`Priority=${PRIORITY[options.priority]}`);
36
+ return parts.join("; ");
37
+ };
38
+
39
+ // src/cookies/cookie-store.ts
40
+ var BROWSER_STORE = Symbol("browserCookies");
41
+
42
+ // src/cookies/browser-cookies.ts
43
+ var listeners = /* @__PURE__ */ new Set();
44
+ var notify = () => {
45
+ listeners.forEach((listener) => listener());
46
+ };
47
+ var assertBrowser = (method) => {
48
+ if (typeof document === "undefined") {
49
+ throw new Error(
50
+ `browserCookies.${method}() can only be called in the browser. On the server, pass \`await cookies()\` from "next/headers" instead.`
51
+ );
52
+ }
53
+ };
54
+ var addExternalChangeListeners = () => {
55
+ const cookieStore = window.cookieStore;
56
+ cookieStore?.addEventListener("change", notify);
57
+ window.addEventListener("focus", notify);
58
+ document.addEventListener("visibilitychange", notify);
59
+ return () => {
60
+ cookieStore?.removeEventListener("change", notify);
61
+ window.removeEventListener("focus", notify);
62
+ document.removeEventListener("visibilitychange", notify);
63
+ };
64
+ };
65
+ var removeExternalChangeListeners;
66
+ var browserCookies = {
67
+ [BROWSER_STORE]: true,
68
+ get(name) {
69
+ assertBrowser("get");
70
+ const value = parseCookieHeader(document.cookie).get(name);
71
+ return value === void 0 ? void 0 : { value };
72
+ },
73
+ set(name, value, options) {
74
+ assertBrowser("set");
75
+ if (options?.httpOnly) {
76
+ throw new Error(
77
+ `Cookie "${name}" is httpOnly and can only be written on the server.`
78
+ );
79
+ }
80
+ document.cookie = serializeCookie(name, value, options);
81
+ notify();
82
+ },
83
+ subscribe(listener) {
84
+ assertBrowser("subscribe");
85
+ listeners.add(listener);
86
+ removeExternalChangeListeners ??= addExternalChangeListeners();
87
+ return () => {
88
+ listeners.delete(listener);
89
+ if (listeners.size === 0) {
90
+ removeExternalChangeListeners?.();
91
+ removeExternalChangeListeners = void 0;
92
+ }
93
+ };
94
+ }
95
+ };
96
+
97
+ // src/cookies/cookie-schema.ts
98
+ var invalid = (expected, raw) => {
99
+ throw new Error(`Invalid cookie value: expected ${expected}, got "${raw}"`);
100
+ };
101
+ var cookieSchema = {
102
+ string: () => ({
103
+ parse: (raw) => raw,
104
+ serialize: (value) => value
105
+ }),
106
+ /** Finite numbers only. */
107
+ number: () => ({
108
+ parse: (raw) => {
109
+ const value = Number(raw);
110
+ if (raw.trim() === "" || !Number.isFinite(value)) {
111
+ return invalid("a finite number", raw);
112
+ }
113
+ return value;
114
+ },
115
+ serialize: (value) => String(value)
116
+ }),
117
+ /** Stored as `"true"` / `"false"`. */
118
+ boolean: () => ({
119
+ parse: (raw) => {
120
+ if (raw === "true") return true;
121
+ if (raw === "false") return false;
122
+ return invalid('"true" or "false"', raw);
123
+ },
124
+ serialize: (value) => String(value)
125
+ }),
126
+ /** One of a fixed set of string literals. */
127
+ enum: (values) => ({
128
+ parse: (raw) => {
129
+ if (!values.includes(raw)) {
130
+ return invalid(`one of ${values.join(", ")}`, raw);
131
+ }
132
+ return raw;
133
+ },
134
+ serialize: (value) => value
135
+ }),
136
+ /**
137
+ * JSON-encoded value. `validate` receives the result of `JSON.parse` and must return the typed value or throw.
138
+ * A zod/valibot/etc. `parse` function fits directly.
139
+ *
140
+ * @example
141
+ * ```ts
142
+ * cookieSchema.json(z.object({ analytics: z.boolean() }).parse);
143
+ *
144
+ * // without a validation library
145
+ * cookieSchema.json((input) => {
146
+ * if (typeof input !== "object" || input === null || !("analytics" in input)) throw new Error();
147
+ * return { analytics: input.analytics === true };
148
+ * });
149
+ * ```
150
+ */
151
+ json: (validate) => ({
152
+ parse: (raw) => validate(JSON.parse(raw)),
153
+ serialize: (value) => JSON.stringify(value)
154
+ })
155
+ };
156
+
157
+ // src/cookies/define-cookie.ts
158
+ function defineCookie(config) {
159
+ const { name, schema, defaultValue } = config;
160
+ const options = { path: "/", ...config.options };
161
+ const fallback = defaultValue === void 0 ? null : defaultValue;
162
+ const get = (store) => {
163
+ const raw = store.get(name)?.value;
164
+ if (raw === void 0) return fallback;
165
+ try {
166
+ return schema.parse(raw);
167
+ } catch {
168
+ return fallback;
169
+ }
170
+ };
171
+ const set = (store, value, overrides) => {
172
+ store.set(name, schema.serialize(value), { ...options, ...overrides });
173
+ };
174
+ const remove = (store) => {
175
+ store.set(name, "", { ...options, maxAge: 0, expires: /* @__PURE__ */ new Date(0) });
176
+ };
177
+ return {
178
+ name,
179
+ httpOnly: options.httpOnly ?? false,
180
+ get,
181
+ set,
182
+ delete: remove
183
+ };
184
+ }
185
+
186
+ export { browserCookies, cookieSchema, defineCookie };
187
+ //# sourceMappingURL=index.js.map
188
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/cookies/cookie-header.ts","../../src/cookies/cookie-store.ts","../../src/cookies/browser-cookies.ts","../../src/cookies/cookie-schema.ts","../../src/cookies/define-cookie.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { CookieAttributes } from \"./cookie-store\";\n\nconst decode = (value: string) => {\n\ttry {\n\t\treturn decodeURIComponent(value);\n\t} catch {\n\t\treturn value;\n\t}\n};\n\n/**\n * Parses a `Cookie` header / `document.cookie` string. The first occurrence of a name wins, matching how\n * browsers order cookies (most specific path first).\n */\nexport const parseCookieHeader = (header: string): Map<string, string> => {\n\tconst cookies = new Map<string, string>();\n\n\tfor (const pair of header.split(\";\")) {\n\t\tconst separatorIndex = pair.indexOf(\"=\");\n\t\tif (separatorIndex === -1) continue;\n\n\t\tconst name = pair.slice(0, separatorIndex).trim();\n\t\tif (!name || cookies.has(name)) continue;\n\n\t\tcookies.set(name, decode(pair.slice(separatorIndex + 1).trim()));\n\t}\n\n\treturn cookies;\n};\n\nconst SAME_SITE = { lax: \"Lax\", strict: \"Strict\", none: \"None\" } as const;\nconst PRIORITY = { low: \"Low\", medium: \"Medium\", high: \"High\" } as const;\n\n/**\n * Serializes a cookie to a `Set-Cookie` / `document.cookie` string, encoding the value the same way Next.js does.\n */\nexport const serializeCookie = (\n\tname: string,\n\tvalue: string,\n\toptions: CookieAttributes = {},\n): string => {\n\tconst parts = [`${name}=${encodeURIComponent(value)}`];\n\n\tparts.push(`Path=${options.path ?? \"/\"}`);\n\tif (options.domain) parts.push(`Domain=${options.domain}`);\n\n\tif (typeof options.maxAge === \"number\") {\n\t\tparts.push(`Max-Age=${Math.floor(options.maxAge)}`);\n\t}\n\n\tconst expires =\n\t\toptions.expires !== undefined\n\t\t\t? new Date(options.expires)\n\t\t\t: typeof options.maxAge === \"number\"\n\t\t\t\t? new Date(Date.now() + options.maxAge * 1000)\n\t\t\t\t: undefined;\n\tif (expires) parts.push(`Expires=${expires.toUTCString()}`);\n\n\tif (options.secure) parts.push(\"Secure\");\n\tif (options.httpOnly) parts.push(\"HttpOnly\");\n\tif (options.sameSite) parts.push(`SameSite=${SAME_SITE[options.sameSite]}`);\n\tif (options.partitioned) parts.push(\"Partitioned\");\n\tif (options.priority) parts.push(`Priority=${PRIORITY[options.priority]}`);\n\n\treturn parts.join(\"; \");\n};\n","/**\n * Every cookie attribute a store can write. Mirrors the options of Next.js' `cookies().set()`.\n */\nexport type CookieAttributes = {\n\t/** @default \"/\" */\n\tpath?: string;\n\tdomain?: string;\n\t/** Lifetime in seconds, relative to the moment the cookie is written. */\n\tmaxAge?: number;\n\t/** Absolute expiry. A `number` is a unix timestamp in milliseconds. */\n\texpires?: Date | number;\n\tsecure?: boolean;\n\t/** `\"none\"` requires `secure: true`, browsers reject the cookie otherwise. */\n\tsameSite?: \"lax\" | \"strict\" | \"none\";\n\t/** Hides the cookie from client-side JavaScript. Can only be written on the server. */\n\thttpOnly?: boolean;\n\tpartitioned?: boolean;\n\tpriority?: \"low\" | \"medium\" | \"high\";\n};\n\n/**\n * Anything cookies can be read from.\n *\n * Satisfied by:\n * - `await cookies()` from `next/headers` (server components, server actions, route handlers)\n * - `request.cookies` in Next.js proxy / middleware (`NextRequest`)\n * - `response.cookies` in Next.js proxy / middleware / route handlers (`NextResponse`)\n * - `browserCookies` from `@buildinams/core/cookies` (client)\n */\nexport type CookieReader = {\n\tget(name: string): { value: string } | undefined;\n};\n\n/**\n * Anything cookies can be written to.\n *\n * Satisfied by:\n * - `await cookies()` from `next/headers`, in server actions and route handlers only (Next.js throws in server components)\n * - `response.cookies` in Next.js proxy / middleware / route handlers (`NextResponse`)\n * - `browserCookies` from `@buildinams/core/cookies` (client)\n */\nexport type CookieWriter = {\n\tset(name: string, value: string, attributes?: CookieAttributes): unknown;\n};\n\nexport const BROWSER_STORE = Symbol(\"browserCookies\");\n\n/** Marks `browserCookies`, so server-only cookies can reject it at compile time. */\nexport type BrowserBrand = { readonly [BROWSER_STORE]: true };\n\n/** A `CookieReader` that isn't `browserCookies`. Every Next.js cookie store qualifies. */\nexport type ServerCookieReader = CookieReader & {\n\treadonly [BROWSER_STORE]?: never;\n};\n\n/** A `CookieWriter` that isn't `browserCookies`. Every writable Next.js cookie store qualifies. */\nexport type ServerCookieWriter = CookieWriter & {\n\treadonly [BROWSER_STORE]?: never;\n};\n","import { parseCookieHeader, serializeCookie } from \"./cookie-header\";\nimport type { BrowserBrand, CookieReader, CookieWriter } from \"./cookie-store\";\nimport { BROWSER_STORE } from \"./cookie-store\";\n\ntype Listener = () => void;\n\nconst listeners = new Set<Listener>();\n\nconst notify = () => {\n\tlisteners.forEach((listener) => listener());\n};\n\nconst assertBrowser = (method: string) => {\n\tif (typeof document === \"undefined\") {\n\t\tthrow new Error(\n\t\t\t`browserCookies.${method}() can only be called in the browser. On the server, pass \\`await cookies()\\` from \"next/headers\" instead.`,\n\t\t);\n\t}\n};\n\n// Picks up writes we don't make ourselves: server responses, other tabs, devtools.\nconst addExternalChangeListeners = () => {\n\tconst cookieStore = (\n\t\twindow as {\n\t\t\tcookieStore?: Pick<\n\t\t\t\tEventTarget,\n\t\t\t\t\"addEventListener\" | \"removeEventListener\"\n\t\t\t>;\n\t\t}\n\t).cookieStore;\n\n\tcookieStore?.addEventListener(\"change\", notify);\n\twindow.addEventListener(\"focus\", notify);\n\tdocument.addEventListener(\"visibilitychange\", notify);\n\n\treturn () => {\n\t\tcookieStore?.removeEventListener(\"change\", notify);\n\t\twindow.removeEventListener(\"focus\", notify);\n\t\tdocument.removeEventListener(\"visibilitychange\", notify);\n\t};\n};\n\ntype BrowserCookieStore = CookieReader &\n\tCookieWriter &\n\tBrowserBrand & {\n\t\t/** Calls `listener` whenever cookies may have changed. Returns an unsubscribe function. */\n\t\tsubscribe(listener: Listener): () => void;\n\t};\n\nlet removeExternalChangeListeners: (() => void) | undefined;\n\n/**\n * Client-side cookie store backed by `document.cookie`. Implements `CookieReader` and `CookieWriter`, so it can be\n * passed to any cookie definition: `themeCookie.get(browserCookies)`.\n *\n * Reads and writes are synchronous. Values are URL-encoded the same way Next.js does, so cookies written here can be\n * read on the server and vice versa.\n */\nexport const browserCookies: BrowserCookieStore = {\n\t[BROWSER_STORE]: true,\n\n\tget(name) {\n\t\tassertBrowser(\"get\");\n\t\tconst value = parseCookieHeader(document.cookie).get(name);\n\t\treturn value === undefined ? undefined : { value };\n\t},\n\n\tset(name, value, options) {\n\t\tassertBrowser(\"set\");\n\t\tif (options?.httpOnly) {\n\t\t\tthrow new Error(\n\t\t\t\t`Cookie \"${name}\" is httpOnly and can only be written on the server.`,\n\t\t\t);\n\t\t}\n\t\tdocument.cookie = serializeCookie(name, value, options);\n\t\tnotify();\n\t},\n\n\tsubscribe(listener) {\n\t\tassertBrowser(\"subscribe\");\n\t\tlisteners.add(listener);\n\t\tremoveExternalChangeListeners ??= addExternalChangeListeners();\n\n\t\treturn () => {\n\t\t\tlisteners.delete(listener);\n\t\t\tif (listeners.size === 0) {\n\t\t\t\tremoveExternalChangeListeners?.();\n\t\t\t\tremoveExternalChangeListeners = undefined;\n\t\t\t}\n\t\t};\n\t},\n};\n","/**\n * Converts between a typed value and the raw string stored in the cookie.\n *\n * - `parse` receives the decoded cookie string and must **throw** when the value is invalid.\n * Cookies are user-controlled input, so an invalid value is treated as \"cookie not set\".\n * - `serialize` turns a typed value into a string. URL-encoding is handled by the cookie store, don't encode here.\n *\n * Structurally compatible with `Schema<Value, string>` from `@buildinams/core/schema`.\n *\n * @example\n * ```ts\n * const dateSchema: CookieSchema<Date> = {\n * parse: (raw) => {\n * const date = new Date(raw);\n * if (Number.isNaN(date.getTime())) throw new Error(\"Invalid date\");\n * return date;\n * },\n * serialize: (value) => value.toISOString(),\n * };\n * ```\n */\nexport type CookieSchema<Value> = {\n\tparse: (raw: string) => Value;\n\tserialize: (value: Value) => string;\n};\n\nconst invalid = (expected: string, raw: string): never => {\n\tthrow new Error(`Invalid cookie value: expected ${expected}, got \"${raw}\"`);\n};\n\n/**\n * Ready-made `CookieSchema`s for common value types.\n *\n * @example\n * ```ts\n * cookieSchema.string(); // string\n * cookieSchema.number(); // number\n * cookieSchema.boolean(); // boolean\n * cookieSchema.enum([\"light\", \"dark\"]); // \"light\" | \"dark\"\n * cookieSchema.json(ConsentSchema.parse); // z.infer<typeof ConsentSchema>\n * ```\n */\nexport const cookieSchema = {\n\tstring: (): CookieSchema<string> => ({\n\t\tparse: (raw) => raw,\n\t\tserialize: (value) => value,\n\t}),\n\n\t/** Finite numbers only. */\n\tnumber: (): CookieSchema<number> => ({\n\t\tparse: (raw) => {\n\t\t\tconst value = Number(raw);\n\t\t\tif (raw.trim() === \"\" || !Number.isFinite(value)) {\n\t\t\t\treturn invalid(\"a finite number\", raw);\n\t\t\t}\n\t\t\treturn value;\n\t\t},\n\t\tserialize: (value) => String(value),\n\t}),\n\n\t/** Stored as `\"true\"` / `\"false\"`. */\n\tboolean: (): CookieSchema<boolean> => ({\n\t\tparse: (raw) => {\n\t\t\tif (raw === \"true\") return true;\n\t\t\tif (raw === \"false\") return false;\n\t\t\treturn invalid('\"true\" or \"false\"', raw);\n\t\t},\n\t\tserialize: (value) => String(value),\n\t}),\n\n\t/** One of a fixed set of string literals. */\n\tenum: <const Values extends readonly [string, ...string[]]>(\n\t\tvalues: Values,\n\t): CookieSchema<Values[number]> => ({\n\t\tparse: (raw) => {\n\t\t\tif (!(values as readonly string[]).includes(raw)) {\n\t\t\t\treturn invalid(`one of ${values.join(\", \")}`, raw);\n\t\t\t}\n\t\t\treturn raw as Values[number];\n\t\t},\n\t\tserialize: (value) => value,\n\t}),\n\n\t/**\n\t * JSON-encoded value. `validate` receives the result of `JSON.parse` and must return the typed value or throw.\n\t * A zod/valibot/etc. `parse` function fits directly.\n\t *\n\t * @example\n\t * ```ts\n\t * cookieSchema.json(z.object({ analytics: z.boolean() }).parse);\n\t *\n\t * // without a validation library\n\t * cookieSchema.json((input) => {\n\t * if (typeof input !== \"object\" || input === null || !(\"analytics\" in input)) throw new Error();\n\t * return { analytics: input.analytics === true };\n\t * });\n\t * ```\n\t */\n\tjson: <Value>(validate: (input: unknown) => Value): CookieSchema<Value> => ({\n\t\tparse: (raw) => validate(JSON.parse(raw)),\n\t\tserialize: (value) => JSON.stringify(value),\n\t}),\n};\n","import type { CookieSchema } from \"./cookie-schema\";\nimport type {\n\tCookieAttributes,\n\tCookieReader,\n\tCookieWriter,\n\tServerCookieReader,\n\tServerCookieWriter,\n} from \"./cookie-store\";\n\n/**\n * Attributes fixed on a cookie definition, applied to every write and to `delete()`.\n */\nexport type CookieOptions = Omit<CookieAttributes, \"expires\">;\n\n/**\n * Per-write lifetime overrides. Path, domain etc. stay on the definition so `delete()` always hits the same cookie.\n */\nexport type CookieWriteOptions = Pick<CookieAttributes, \"maxAge\" | \"expires\">;\n\ntype ReadValue<Value, HasDefault extends boolean> = HasDefault extends true\n\t? Value\n\t: Value | null;\n\n/**\n * A typed cookie readable and writable on the server and in the browser. Create with `defineCookie()`.\n *\n * - `Value`: the type written and read.\n * - `HasDefault`: `true` when a `defaultValue` is defined. Reads return `Value`, otherwise `Value | null`.\n */\nexport type Cookie<Value, HasDefault extends boolean = boolean> = {\n\treadonly name: string;\n\treadonly httpOnly: false;\n\t/** Reads and parses the cookie. Returns the default (or `null`) when it is missing or invalid. */\n\tget(store: CookieReader): ReadValue<Value, HasDefault>;\n\t/** Serializes and writes the cookie with the definition's options. */\n\tset(store: CookieWriter, value: Value, options?: CookieWriteOptions): void;\n\t/** Expires the cookie. */\n\tdelete(store: CookieWriter): void;\n};\n\n/**\n * A typed httpOnly cookie. Same API as `Cookie`, but only accepts server stores: passing `browserCookies` is a\n * compile error, since the browser can't read or write it.\n */\nexport type ServerCookie<Value, HasDefault extends boolean = boolean> = {\n\treadonly name: string;\n\treadonly httpOnly: true;\n\tget(store: ServerCookieReader): ReadValue<Value, HasDefault>;\n\tset(\n\t\tstore: ServerCookieWriter,\n\t\tvalue: Value,\n\t\toptions?: CookieWriteOptions,\n\t): void;\n\tdelete(store: ServerCookieWriter): void;\n};\n\n/**\n * The type written to a cookie.\n *\n * @example\n * ```ts\n * type Theme = CookieWriteValue<typeof themeCookie>; // \"light\" | \"dark\"\n * ```\n */\nexport type CookieWriteValue<\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tT extends Cookie<any> | ServerCookie<any>,\n> = Parameters<T[\"set\"]>[1];\n\n/**\n * The type read from a cookie: `CookieWriteValue | null` when there is no `defaultValue`.\n */\nexport type CookieReadValue<\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tT extends Cookie<any> | ServerCookie<any>,\n> = ReturnType<T[\"get\"]>;\n\ntype CookieConfig<Value, HttpOnly extends boolean> = {\n\t/** Cookie name, as stored in the browser. */\n\tname: string;\n\t/** How to convert between the typed value and the stored string. See `cookieSchema`. */\n\tschema: CookieSchema<Value>;\n\t/** Attributes used for every write. `path` defaults to `\"/\"`. */\n\toptions?: Omit<CookieOptions, \"httpOnly\"> & { httpOnly?: HttpOnly };\n};\n\ntype DefinedCookie<\n\tValue,\n\tHasDefault extends boolean,\n\tHttpOnly extends boolean,\n> = HttpOnly extends true\n\t? ServerCookie<Value, HasDefault>\n\t: Cookie<Value, HasDefault>;\n\n/**\n * Defines a typed cookie that can be read and written with the same API on the server (Next.js) and on the client.\n * With `options.httpOnly: true` it returns a `ServerCookie`, which rejects `browserCookies` at compile time.\n *\n * Define it next to the code it belongs to (e.g. `src/features/auth/cookies.ts`), in a module without\n * `\"use client\"` / `\"use server\"`, so both environments can import it.\n *\n * @example\n * ```ts\n * import { cookieSchema, defineCookie } from \"@buildinams/core/cookies\";\n *\n * export const themeCookie = defineCookie({\n * name: \"theme\",\n * schema: cookieSchema.enum([\"light\", \"dark\"]),\n * defaultValue: \"light\",\n * options: { maxAge: 60 * 60 * 24 * 365, sameSite: \"lax\" },\n * });\n *\n * // server component / server action / route handler\n * const theme = themeCookie.get(await cookies()); // \"light\" | \"dark\"\n * themeCookie.set(await cookies(), \"dark\"); // server action / route handler only\n *\n * // client\n * themeCookie.set(browserCookies, \"dark\");\n * const theme = useCookieValue(themeCookie); // @buildinams/react/cookies\n * ```\n */\nexport function defineCookie<Value, HttpOnly extends boolean = false>(\n\tconfig: CookieConfig<Value, HttpOnly> & { defaultValue: NoInfer<Value> },\n): DefinedCookie<Value, true, HttpOnly>;\nexport function defineCookie<Value, HttpOnly extends boolean = false>(\n\tconfig: CookieConfig<Value, HttpOnly> & { defaultValue?: undefined },\n): DefinedCookie<Value, false, HttpOnly>;\nexport function defineCookie<Value>(\n\tconfig: CookieConfig<Value, boolean> & { defaultValue?: Value },\n): Cookie<Value> | ServerCookie<Value> {\n\tconst { name, schema, defaultValue } = config;\n\tconst options: CookieOptions = { path: \"/\", ...config.options };\n\tconst fallback = defaultValue === undefined ? null : defaultValue;\n\n\tconst get = (store: CookieReader) => {\n\t\tconst raw = store.get(name)?.value;\n\t\tif (raw === undefined) return fallback;\n\t\ttry {\n\t\t\treturn schema.parse(raw);\n\t\t} catch {\n\t\t\treturn fallback;\n\t\t}\n\t};\n\n\tconst set = (\n\t\tstore: CookieWriter,\n\t\tvalue: Value,\n\t\toverrides?: CookieWriteOptions,\n\t) => {\n\t\tstore.set(name, schema.serialize(value), { ...options, ...overrides });\n\t};\n\n\tconst remove = (store: CookieWriter) => {\n\t\tstore.set(name, \"\", { ...options, maxAge: 0, expires: new Date(0) });\n\t};\n\n\treturn {\n\t\tname,\n\t\thttpOnly: options.httpOnly ?? false,\n\t\tget,\n\t\tset,\n\t\tdelete: remove,\n\t} as Cookie<Value> | ServerCookie<Value>;\n}\n"]}
@@ -0,0 +1,255 @@
1
+ type PageType = "home" | "collection" | "product" | "search" | "cart" | "checkout" | "account" | "content" | (string & Record<never, never>);
2
+ /** What `videos()` reports for a video. */
3
+ type VideoPlayed = {
4
+ videoTitle?: string;
5
+ videoUrl?: string;
6
+ /** E.g. `"html5"` or `"youtube"`. */
7
+ videoProvider?: string;
8
+ /** Seconds. */
9
+ videoDuration?: number;
10
+ /** Seconds. */
11
+ videoCurrentTime?: number;
12
+ videoPercent?: number;
13
+ };
14
+ /** What `forms()` reports for a form. Never field values. */
15
+ type FormUsed = {
16
+ formId?: string;
17
+ formName?: string;
18
+ /** Absolute URL the form submits to. */
19
+ formDestination?: string;
20
+ };
21
+ /** What `pageViews()` reports: the page's fields from `attributes("page-view")`, with the project's own. */
22
+ type PageViewed = {
23
+ url?: string;
24
+ title?: string;
25
+ pageType?: PageType;
26
+ };
27
+ /** What `clicks()` reports for a link or button. */
28
+ type Clicked = {
29
+ elementType: "link" | "button";
30
+ /** Stable id, independent of locale and copy. */
31
+ elementId?: string;
32
+ text?: string;
33
+ /** Absolute. Links only. */
34
+ url?: string;
35
+ /** Whether the link leaves the site. */
36
+ outbound?: boolean;
37
+ /** Where on the page, e.g. `"header"` or `"footer"`. */
38
+ location?: string;
39
+ };
40
+ /** What `scroll()` reports, once per URL for each threshold. */
41
+ type Scrolled = {
42
+ /** The threshold reached, of the element scrolled into view. */
43
+ percentScrolled: number;
44
+ /** Stable id, for pages with several tracked elements. */
45
+ elementId?: string;
46
+ };
47
+ /** The project's data for what `ecommerce()` reports, set on elements with `attach()`. */
48
+ type EcommerceShapes = {
49
+ /** A product, e.g. `{ product, variant }`. On cards and product pages alike. */
50
+ item: object;
51
+ list: object;
52
+ promotion: object;
53
+ };
54
+ /** A shape of `Shapes` that's an object, or `never` without it. */
55
+ type ShapeLeaf<Shapes, Key extends string> = Shapes extends {
56
+ [K in Key]: infer Leaf extends object;
57
+ } ? Leaf : never;
58
+ /** An item where it was seen or selected: its list, its position in it, and the promotion around it. */
59
+ type PlacedItem<Shapes extends EcommerceShapes> = {
60
+ item: Shapes["item"];
61
+ list?: Shapes["list"];
62
+ /** In its list, from 0. */
63
+ index?: number;
64
+ promotion?: Shapes["promotion"];
65
+ };
66
+ /** What `ecommerce()` reports, by name: the project's item, list and promotion, placed by the markup around them. */
67
+ type EcommerceOccurrences<Shapes extends EcommerceShapes> = {
68
+ /** On a product page or quick view, placed where the product was last selected. */
69
+ itemViewed: PlacedItem<Shapes>;
70
+ itemSelected: PlacedItem<Shapes>;
71
+ /** One per list and batch. */
72
+ itemsViewed: {
73
+ list?: Shapes["list"];
74
+ items: Omit<PlacedItem<Shapes>, "list">[];
75
+ };
76
+ /** With the items featured in it outside a list. */
77
+ promotionViewed: {
78
+ promotion: Shapes["promotion"];
79
+ items: Shapes["item"][];
80
+ };
81
+ /** With `item` when the click was on a featured item. */
82
+ promotionSelected: {
83
+ promotion: Shapes["promotion"];
84
+ item?: Shapes["item"];
85
+ };
86
+ };
87
+ /** A cart line's change, as `createCartChanges()` reports it. */
88
+ type CartChange<Item> = {
89
+ item: Item;
90
+ /** Added or removed. */
91
+ quantity: number;
92
+ /** Unit price. */
93
+ price: number;
94
+ };
95
+ /** What `createCartChanges()` reports, by name, for the project's cart line item. */
96
+ type CartOccurrences<Item> = {
97
+ addedToCart: {
98
+ currency: string;
99
+ value: number;
100
+ items: CartChange<Item>[];
101
+ };
102
+ removedFromCart: {
103
+ currency: string;
104
+ value: number;
105
+ items: CartChange<Item>[];
106
+ };
107
+ };
108
+
109
+ /** What a project emits: each event's name with its payload. Declare it with `type`, as an `interface` isn't one. */
110
+ type EventMap = Record<string, object>;
111
+ /** `EcommerceShapes` of a project's events. */
112
+ type ShapesOf<Events> = {
113
+ item: Events extends {
114
+ itemViewed: {
115
+ item: infer Item extends object;
116
+ };
117
+ } ? Item : Events extends {
118
+ itemSelected: {
119
+ item: infer Item extends object;
120
+ };
121
+ } ? Item : Events extends {
122
+ itemsViewed: {
123
+ items: {
124
+ item: infer Item extends object;
125
+ }[];
126
+ };
127
+ } ? Item : Events extends {
128
+ promotionViewed: {
129
+ items: (infer Item extends object)[];
130
+ };
131
+ } ? Item : never;
132
+ list: Events extends {
133
+ itemsViewed: {
134
+ list?: infer List;
135
+ };
136
+ } ? Extract<List, object> : Events extends {
137
+ itemSelected: {
138
+ list?: infer List;
139
+ };
140
+ } ? Extract<List, object> : Events extends {
141
+ itemViewed: {
142
+ list?: infer List;
143
+ };
144
+ } ? Extract<List, object> : never;
145
+ promotion: Events extends {
146
+ promotionViewed: {
147
+ promotion: infer Promotion extends object;
148
+ };
149
+ } ? Promotion : Events extends {
150
+ promotionSelected: {
151
+ promotion: infer Promotion extends object;
152
+ };
153
+ } ? Promotion : Events extends {
154
+ itemSelected: {
155
+ promotion?: infer Promotion;
156
+ };
157
+ } ? Extract<Promotion, object> : never;
158
+ };
159
+ /** The cart line item of a project's events. */
160
+ type CartItemOf<Events> = Events extends {
161
+ addedToCart: {
162
+ items: CartChange<infer Item>[];
163
+ };
164
+ } ? Item : never;
165
+ type AnalyticsEventName<Events extends EventMap> = keyof Events & string;
166
+ /** Payload is optional when every field of it is. */
167
+ type AnalyticsPayloadArgs<Events extends EventMap, N extends AnalyticsEventName<Events>> = Record<never, never> extends Events[N] ? [payload?: Events[N]] : [payload: Events[N]];
168
+ /** The logged-in visitor. Destinations decide which fields they forward, and only with the consent they need. */
169
+ type AnalyticsUser = {
170
+ id?: string;
171
+ email?: string;
172
+ /** E.164, e.g. `"+31612345678"`. */
173
+ phone?: string;
174
+ /** Whether the customer has ordered before. */
175
+ type?: "new" | "returning";
176
+ };
177
+ /** Fields for `context` and `setContext()`, with the project's own `Context`. */
178
+ type AnalyticsContextFields<Context extends object = Record<never, never>> = {
179
+ user?: AnalyticsUser & (Context extends {
180
+ user?: infer User;
181
+ } ? User : unknown);
182
+ /** BCP 47, e.g. `"nl-NL"`. */
183
+ locale?: string;
184
+ /** The storefront market, ISO 3166-1 alpha-2, e.g. `"NL"`. Not where the visitor is. */
185
+ market?: string;
186
+ } & Omit<Context, "user">;
187
+ type AnalyticsContext<Context extends object = Record<never, never>> = {
188
+ url: string;
189
+ title: string;
190
+ referrer: string;
191
+ } & AnalyticsContextFields<Context>;
192
+ /** What's tracked, as subscribers and connected destinations receive it. */
193
+ type AnalyticsEnvelope<Events extends EventMap, Context extends object = Record<never, never>, N extends AnalyticsEventName<Events> = AnalyticsEventName<Events>> = {
194
+ [K in N]: {
195
+ /** Unique per event; lets destinations dedupe client and server events (e.g. Meta `eventID`). */
196
+ id: string;
197
+ name: K;
198
+ payload: Events[K];
199
+ context: AnalyticsContext<Context>;
200
+ };
201
+ }[N];
202
+
203
+ declare const shapeType: unique symbol;
204
+ /** A type as a value, from `shape<T>()`. Holds nothing at runtime. */
205
+ type Shape<T> = {
206
+ readonly [shapeType]?: T;
207
+ };
208
+ /**
209
+ * A type as a value, e.g. an event's payload in `defineEvents()`. Holds nothing at runtime, so it costs nothing in the
210
+ * bundle.
211
+ *
212
+ * @example
213
+ * ```ts
214
+ * defineEvents({ login: { description: "…", payload: shape<{ method: "email" }>() } });
215
+ * ```
216
+ */
217
+ declare const shape: <T>() => Shape<T>;
218
+ /** One event of the project: what it means and when it's tracked, and its payload. */
219
+ type EventDefinition<Payload extends object = object> = {
220
+ /** When it's tracked and what it means, for whoever reads the catalog: developers, agents and analysts. */
221
+ description: string;
222
+ /** Leave it out for events without fields. */
223
+ payload?: Shape<Payload>;
224
+ };
225
+ /** Every event of the project, by name. */
226
+ type EventCatalog<Events extends EventMap = EventMap> = {
227
+ [N in keyof Events]: Record<never, never> extends Events[N] ? EventDefinition<Events[N]> : EventDefinition<Events[N]> & {
228
+ payload: Shape<Events[N]>;
229
+ };
230
+ };
231
+ /** The events of a catalog from `defineEvents()`, for `createAnalytics<Events>()`. */
232
+ type EventsOf<Catalog> = {
233
+ [N in keyof Catalog]: Catalog[N] extends {
234
+ payload?: Shape<infer Payload>;
235
+ } ? unknown extends Payload ? Record<never, never> : Payload : Record<never, never>;
236
+ };
237
+ /**
238
+ * Declares the project's events as a value: each event's name, what it means, and its payload. It's the one place
239
+ * to read what the project tracks, also at runtime through `analytics.routes()`. Pass it to `createAnalytics()`.
240
+ *
241
+ * @example
242
+ * ```ts
243
+ * export const events = defineEvents({
244
+ * newsletterSignedUp: {
245
+ * description: "The newsletter form was submitted successfully.",
246
+ * payload: shape<{ source: "footer" | "popup" }>(),
247
+ * },
248
+ * });
249
+ *
250
+ * export type Events = EventsOf<typeof events>;
251
+ * ```
252
+ */
253
+ declare const defineEvents: <Catalog extends Record<string, EventDefinition>>(catalog: Catalog) => Catalog;
254
+
255
+ export { type AnalyticsEventName as A, type CartItemOf as C, type EventMap as E, type FormUsed as F, type PageViewed as P, type Shape as S, type VideoPlayed as V, type AnalyticsPayloadArgs as a, type AnalyticsContextFields as b, type AnalyticsEnvelope as c, type EventCatalog as d, defineEvents as e, type EventDefinition as f, type EventsOf as g, type AnalyticsContext as h, type AnalyticsUser as i, type EcommerceShapes as j, type ShapesOf as k, type Clicked as l, type EcommerceOccurrences as m, type ShapeLeaf as n, type Scrolled as o, type PageType as p, type CartOccurrences as q, shape as s };
@@ -0,0 +1,122 @@
1
+ import { c as ConsentState, C as ConsentCategory } from './index-Zef1nnTP.js';
2
+ import { E as EventMap, h as AnalyticsContext, A as AnalyticsEventName, f as EventDefinition, c as AnalyticsEnvelope, g as EventsOf } from './define-events-CxZjoUlG.js';
3
+
4
+ /** One event in a destination's own vocabulary, narrowed by `name`. */
5
+ type DestinationEvent<Accepts extends object> = {
6
+ [N in keyof Accepts & string]: {
7
+ name: N;
8
+ payload: Accepts[N];
9
+ };
10
+ }[keyof Accepts & string];
11
+ /** What a destination's callbacks receive with each payload. */
12
+ type DestinationEventContext<Context extends object = Record<never, never>> = {
13
+ /** Of the tracked event: unique per event, e.g. for Meta's `eventID`. */
14
+ id: string;
15
+ /** Of the tracked event, with the project's own fields too. */
16
+ context: AnalyticsContext<Context>;
17
+ /** The current answer, `null` until answered, e.g. to send personal data only with marketing consent. */
18
+ consent: ConsentState;
19
+ };
20
+ /** What a destination's `setup` receives. */
21
+ type DestinationSetupContext<Accepts extends object, Context extends object = Record<never, never>> = {
22
+ /** Subscribes a callback to one of the destination's events. Returns a function that unsubscribes it. */
23
+ subscribe: <N extends keyof Accepts & string>(event: N, callback: (payload: Accepts[N], context: DestinationEventContext<Context>) => void) => () => void;
24
+ };
25
+ /**
26
+ * Forwards events to one provider (GTM, Meta, Shopify, …), in the provider's own vocabulary: `Accepts` holds the
27
+ * minimum shape of every event it takes. Shaped like Hydrogen's `StorefrontAnalyticsDestination`. `map()` turns it
28
+ * into one that accepts the project's events, which `withConsent()` hands to `createAnalytics()`. Telling a provider the
29
+ * visitor's consent is a `ConsentTarget`'s job.
30
+ */
31
+ type AnalyticsDestination<Accepts extends object, Context extends object = Record<never, never>> = {
32
+ /** Prefixes this destination's errors and warnings in the console. */
33
+ name: string;
34
+ /**
35
+ * The events it subscribes to, so `routes()` can list them without setting it up, e.g. on the server. Without it,
36
+ * it counts as receiving every event.
37
+ */
38
+ events?: readonly (keyof Accepts & string)[];
39
+ /**
40
+ * Runs once in the browser when analytics is created, before consent is answered: subscribe to events here, but
41
+ * load the provider's script once the first event arrives. Events wait for an async setup. When it throws or
42
+ * rejects, the error is logged and the destination receives nothing.
43
+ */
44
+ setup: (context: DestinationSetupContext<Accepts, Context>) => void | Promise<void>;
45
+ };
46
+ /** One event, several (e.g. one per item), or `null` to send nothing. */
47
+ type MappingResult<Accepts extends object> = DestinationEvent<Accepts> | readonly DestinationEvent<Accepts>[] | null;
48
+ /** Per event of `Events`, what a destination receives. Events without an entry aren't sent. */
49
+ type AnalyticsMapping<Events extends EventMap, Accepts extends object, Context extends object = Record<never, never>> = {
50
+ [N in AnalyticsEventName<Events>]?: (payload: Events[N], event: {
51
+ id: string;
52
+ name: N;
53
+ context: AnalyticsContext<Context>;
54
+ }) => MappingResult<Accepts>;
55
+ };
56
+ /**
57
+ * Categories that must all be granted before a destination receives events; until consent is answered, they're queued
58
+ * for it. `"none"` delivers every event straight away, even before consent is answered, for providers that gate on
59
+ * consent themselves, e.g. GTM with Consent Mode.
60
+ */
61
+ type ConsentRequirement = readonly [ConsentCategory, ...ConsentCategory[]] | "none";
62
+ /** What `createAnalytics()` takes: a destination of the project's events with the consent it requires, from `withConsent()`. */
63
+ type ConsentedDestination<Events extends EventMap, Context extends object = Record<never, never>> = AnalyticsDestination<Events, Context> & {
64
+ requires: ConsentRequirement;
65
+ };
66
+ /**
67
+ * Maps the project's events onto a destination: per event of `createAnalytics<Events>()`, what the destination
68
+ * receives, typed by what it accepts. Events without an entry aren't sent to it. Returns a destination that accepts the
69
+ * project's events, for `withConsent()`. Inside `withConsent()`, it takes the events from there. See `docs/analytics.md`.
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * map(metaDestination(), {
74
+ * newsletterSignedUp: ({ source }) => ({ name: "Lead", payload: { content_name: source } }),
75
+ * });
76
+ * ```
77
+ */
78
+ declare const map: <Accepts extends object, Events extends EventMap, Context extends object = Record<never, never>>(destination: AnalyticsDestination<Accepts>, mapping: AnalyticsMapping<NoInfer<Events>, Accepts, NoInfer<Context>>) => AnalyticsDestination<Events, Context>;
79
+ /**
80
+ * A mapping with an entry for every event of the catalog, e.g. to send the events without one of their own in a
81
+ * generic format. Spread it first, so the entries after it override it.
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * map(gtmDestination(), { ...mapEvery(events, toGa4CustomEvent), ...ga4Mapping<Events>({ item: toGa4Item }) });
86
+ * ```
87
+ */
88
+ declare const mapEvery: <Catalog extends Record<string, EventDefinition>, Result extends {
89
+ name: string;
90
+ payload: unknown;
91
+ } | readonly {
92
+ name: string;
93
+ payload: unknown;
94
+ }[] | null, Context extends object = Record<never, never>>(events: Catalog, toEvent: (event: AnalyticsEnvelope<EventsOf<Catalog>, Context>) => Result) => { [N in keyof Catalog & string]: (payload: EventsOf<Catalog>[N], event: {
95
+ id: string;
96
+ name: N;
97
+ context: AnalyticsContext<Context>;
98
+ }) => Result; };
99
+ /**
100
+ * Gives a destination of the project's events, usually from `map()`, the consent it requires, for `createAnalytics()`.
101
+ * Required, so consent is never left out by accident. Inside `createAnalytics()`, it takes the events from there.
102
+ *
103
+ * @example
104
+ * ```ts
105
+ * createAnalytics<Events>({
106
+ * events,
107
+ * consent,
108
+ * destinations: [
109
+ * withConsent(map(gtmDestination(), ga4Mapping<Events>({ item: toGa4Item })), "none"),
110
+ * withConsent(
111
+ * map(metaDestination(), {
112
+ * newsletterSignedUp: ({ source }) => ({ name: "Lead", payload: { content_name: source } }),
113
+ * }),
114
+ * ["marketing"],
115
+ * ),
116
+ * ],
117
+ * });
118
+ * ```
119
+ */
120
+ declare const withConsent: <Events extends EventMap, Context extends object = Record<never, never>>(destination: AnalyticsDestination<Events, Context>, requires: ConsentRequirement) => ConsentedDestination<Events, Context>;
121
+
122
+ export { type AnalyticsDestination as A, type ConsentedDestination as C, type DestinationEvent as D, type MappingResult as M, type AnalyticsMapping as a, type ConsentRequirement as b, type DestinationEventContext as c, type DestinationSetupContext as d, mapEvery as e, map as m, withConsent as w };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Generic consent categories. Strictly necessary storage is always allowed, so it has no category.
3
+ *
4
+ * | Category | Google Consent Mode v2 | Shopify Customer Privacy | Meta Pixel |
5
+ * | ------------- | ----------------------------------------------------- | ----------------------------- | ---------------- |
6
+ * | `preferences` | `functionality_storage`, `personalization_storage` | `preferences` | — |
7
+ * | `analytics` | `analytics_storage` | `analytics` | — |
8
+ * | `marketing` | `ad_storage`, `ad_user_data`, `ad_personalization` | `marketing`, read with `sale_of_data` | `grant`/`revoke` |
9
+ */
10
+ declare const CONSENT_CATEGORIES: readonly ["preferences", "analytics", "marketing"];
11
+ type ConsentCategory = (typeof CONSENT_CATEGORIES)[number];
12
+ type Consent = Record<ConsentCategory, boolean>;
13
+ /** `null` until the visitor has answered. */
14
+ type ConsentState = Consent | null;
15
+ /**
16
+ * Where consent comes from, usually a consent platform: `cookiebotConsent()`, `oneTrustConsent()`, or your own.
17
+ * Pass it to `createAnalytics()` from `@buildinams/core/analytics`.
18
+ */
19
+ type ConsentSource = {
20
+ /** The platform, e.g. `"cookiebot"`. Consent isn't applied to a `ConsentTarget` with the same name. */
21
+ name?: string;
22
+ /** `null` until the visitor has answered, and on the server. */
23
+ get: () => Consent | null;
24
+ /** Calls `listener` with answered consent when it may have changed; repeats are fine. Returns an unsubscribe function. */
25
+ subscribe: (listener: (consent: Consent) => void) => () => void;
26
+ };
27
+ /**
28
+ * Is told the visitor's consent, e.g. Google Consent Mode or Shopify's Customer Privacy API. Pass it to
29
+ * `createAnalytics({ consentTargets })`, which applies every answer, except to a target named like the source.
30
+ */
31
+ type ConsentTarget = {
32
+ name: string;
33
+ apply: (consent: Consent) => void | Promise<void>;
34
+ };
35
+
36
+ export { type ConsentCategory as C, type ConsentSource as a, type ConsentTarget as b, type ConsentState as c, type Consent as d, CONSENT_CATEGORIES as e };