@buildinams/core 0.3.0 → 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 (82) 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-IAAQVL6M.js → chunk-FJ7SNQQI.js} +24 -5
  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.d.ts +1 -1
  53. package/dist/navigation/index.js +1 -1
  54. package/dist/schema/index.d.ts +16 -9
  55. package/dist/schema/index.js +1 -1
  56. package/dist/schema-CfQvhbuU.d.ts +87 -0
  57. package/dist/store/index.js +1 -1
  58. package/dist/tracking/cart/index.d.ts +74 -0
  59. package/dist/tracking/cart/index.js +118 -0
  60. package/dist/tracking/cart/index.js.map +1 -0
  61. package/dist/tracking/dom/index.d.ts +320 -0
  62. package/dist/tracking/dom/index.js +1196 -0
  63. package/dist/tracking/dom/index.js.map +1 -0
  64. package/docs/analytics.md +883 -0
  65. package/docs/cookies.md +354 -0
  66. package/docs/decisions/001-analytics-project-events.md +31 -0
  67. package/docs/decisions/002-tracking-dom-opt-in.md +29 -0
  68. package/docs/decisions/003-tracking-element-data.md +27 -0
  69. package/docs/decisions/004-cookies-explicit-store.md +26 -0
  70. package/docs/decisions/005-cookies-invalid-reads-fall-back.md +27 -0
  71. package/docs/decisions/006-cookies-server-only-types.md +19 -0
  72. package/docs/decisions/007-cookies-options-on-definition.md +25 -0
  73. package/docs/decisions/008-cookies-in-vertical-slices.md +24 -0
  74. package/docs/decisions/009-analytics-consent-targets.md +29 -0
  75. package/docs/decisions/010-analytics-destination-contract.md +29 -0
  76. package/docs/decisions/011-tracking-decoupled.md +30 -0
  77. package/docs/tracking.md +221 -0
  78. package/package.json +84 -3
  79. package/dist/chunk-IAAQVL6M.js.map +0 -1
  80. package/dist/chunk-SIU7SKJK.js +0 -139
  81. package/dist/chunk-SIU7SKJK.js.map +0 -1
  82. package/dist/schema-iy92gbTC.d.ts +0 -36
@@ -0,0 +1,175 @@
1
+ import { IS_DEVELOPMENT } from '../../chunk-GQVN2ECX.js';
2
+
3
+ // src/analytics/shopify/shopify-destination.ts
4
+ var MAX_PENDING = 100;
5
+ var BUS_POLL_INTERVAL = 250;
6
+ var BUS_POLL_DURATION = 3e4;
7
+ var getShopify = () => window.Shopify;
8
+ var REQUIRED_FIELDS = ["id", "title", "vendor", "variantId"];
9
+ var toGid = (type, id) => /^\d+$/.test(id) ? `gid://shopify/${type}/${id}` : id;
10
+ var withGids = (product) => ({
11
+ ...product,
12
+ id: toGid("Product", product.id),
13
+ variantId: toGid("ProductVariant", product.variantId)
14
+ });
15
+ var isComplete = (product) => {
16
+ const missing = REQUIRED_FIELDS.filter((key) => !product[key]);
17
+ if (!product.price.amount) missing.push("price");
18
+ if (missing.length > 0 && IS_DEVELOPMENT) {
19
+ console.warn(
20
+ `[analytics] shopify: product "${product.id}" needs ${missing.join(", ")} to reach Shopify.`
21
+ );
22
+ }
23
+ return missing.length === 0;
24
+ };
25
+ var SHOPIFY_EVENTS = {
26
+ page_viewed: true,
27
+ search_viewed: true,
28
+ product_viewed: true,
29
+ collection_viewed: true,
30
+ product_added_to_cart: true,
31
+ cart_viewed: true,
32
+ cart_updated: true,
33
+ product_removed_from_cart: true
34
+ };
35
+ var toCartLine = (product) => ({
36
+ id: product.variantId,
37
+ quantity: product.quantity,
38
+ merchandise: {
39
+ id: product.variantId,
40
+ title: product.variantTitle,
41
+ price: product.price,
42
+ product: {
43
+ id: product.id,
44
+ title: product.title,
45
+ vendor: product.vendor,
46
+ productType: product.productType
47
+ }
48
+ }
49
+ });
50
+ var shopifyMapping = ({
51
+ product: toProduct,
52
+ cartProduct: toCartProduct,
53
+ getCartId
54
+ } = {}) => {
55
+ const mapping = {
56
+ pageViewed: (payload, { context: { url } }) => ({
57
+ name: "page_viewed",
58
+ payload: { url: new URL(payload.url ?? url, url).href }
59
+ })
60
+ };
61
+ if (toProduct) {
62
+ mapping.itemViewed = ({ item }, { context: { url } }) => {
63
+ const product = toProduct(item);
64
+ if (!product || !isComplete(product)) return null;
65
+ return {
66
+ name: "product_viewed",
67
+ payload: { url, products: [withGids(product)] }
68
+ };
69
+ };
70
+ }
71
+ if (toCartProduct) {
72
+ mapping.addedToCart = ({ currency, items }) => {
73
+ const cartId = getCartId?.();
74
+ if (!cartId) return null;
75
+ return items.flatMap(({ item, quantity, price }) => {
76
+ const fields = toCartProduct(item);
77
+ if (!fields) return [];
78
+ const product = {
79
+ ...fields,
80
+ price: { amount: String(price), currencyCode: currency },
81
+ quantity
82
+ };
83
+ if (!isComplete(product)) return [];
84
+ return [
85
+ {
86
+ name: "product_added_to_cart",
87
+ payload: {
88
+ cart: { id: cartId },
89
+ prevCart: null,
90
+ currentLine: toCartLine(withGids(product))
91
+ }
92
+ }
93
+ ];
94
+ });
95
+ };
96
+ }
97
+ return mapping;
98
+ };
99
+ var shopifyDestination = () => {
100
+ let warnedMissingBus = false;
101
+ let pending = [];
102
+ let polling;
103
+ let pollUntil = 0;
104
+ const flushPending = () => {
105
+ polling = void 0;
106
+ const bus = getShopify()?.analytics;
107
+ if (!bus) {
108
+ if (Date.now() < pollUntil) {
109
+ polling = setTimeout(flushPending, BUS_POLL_INTERVAL);
110
+ }
111
+ return;
112
+ }
113
+ const events = pending;
114
+ pending = [];
115
+ for (const { name, payload } of events) bus.publish(name, payload);
116
+ };
117
+ const publish = (event) => {
118
+ pending.push(event);
119
+ if (pending.length > MAX_PENDING) {
120
+ pending.splice(0, pending.length - MAX_PENDING);
121
+ }
122
+ if (IS_DEVELOPMENT && !getShopify()?.analytics && !warnedMissingBus) {
123
+ warnedMissingBus = true;
124
+ console.warn(
125
+ "[analytics] shopify: window.Shopify.analytics is missing. Render <ShopifyScripts> from @shopify/hydrogen/react."
126
+ );
127
+ }
128
+ if (polling === void 0) {
129
+ pollUntil = Date.now() + BUS_POLL_DURATION;
130
+ flushPending();
131
+ }
132
+ };
133
+ return {
134
+ name: "shopify",
135
+ setup: ({ subscribe }) => {
136
+ for (const name of Object.keys(
137
+ SHOPIFY_EVENTS
138
+ )) {
139
+ subscribe(
140
+ name,
141
+ (payload) => publish({ name, payload })
142
+ );
143
+ }
144
+ }
145
+ };
146
+ };
147
+
148
+ // src/analytics/shopify/storefront-cart-snapshot.ts
149
+ var toStorefrontCartSnapshot = (cart) => {
150
+ const lines = cart.lines.nodes ?? cart.lines.edges?.map(({ node }) => node) ?? [];
151
+ return {
152
+ id: cart.id,
153
+ currency: cart.cost?.totalAmount?.currencyCode ?? cart.cost?.subtotalAmount?.currencyCode ?? lines[0]?.cost.amountPerQuantity.currencyCode ?? "",
154
+ lines: lines.map((line) => ({
155
+ id: line.id,
156
+ quantity: line.quantity,
157
+ price: Number(line.cost.amountPerQuantity.amount),
158
+ item: line
159
+ }))
160
+ };
161
+ };
162
+ var toStorefrontCartProduct = ({
163
+ merchandise
164
+ }) => ({
165
+ id: merchandise.product.id,
166
+ title: merchandise.product.title,
167
+ vendor: merchandise.product.vendor,
168
+ variantId: merchandise.id,
169
+ variantTitle: merchandise.title,
170
+ productType: merchandise.product.productType ?? void 0
171
+ });
172
+
173
+ export { shopifyDestination, shopifyMapping, toStorefrontCartProduct, toStorefrontCartSnapshot };
174
+ //# sourceMappingURL=index.js.map
175
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/analytics/shopify/shopify-destination.ts","../../../src/analytics/shopify/storefront-cart-snapshot.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { IS_DEVELOPMENT } from \"../../config\";\nimport type {\n\tAnalyticsDestination,\n\tAnalyticsMapping,\n\tDestinationEvent,\n} from \"../destination\";\nimport type {\n\tCartEvents,\n\tCartItemOf,\n\tDomEvents,\n\tEcommerceEvents,\n\tEventMap,\n\tShapesOf,\n} from \"../events\";\n\n/** Hydrogen's `ProductPayload`. */\nexport type ShopifyProduct = {\n\tid: string;\n\ttitle: string;\n\tprice: { amount: string; currencyCode: string };\n\tvendor: string;\n\tvariantId: string;\n\tvariantTitle: string;\n\tquantity: number;\n\tproductType?: string;\n};\n\n// Structural, so this package doesn't depend on `@shopify/hydrogen`.\ntype ShopifyGlobal = {\n\tanalytics?: {\n\t\tpublish: (name: string, payload: Record<string, unknown>) => void;\n\t};\n};\n\n// Keeps the buffer bounded when `<ShopifyScripts>` is never rendered.\nconst MAX_PENDING = 100;\n\n// How often, and how long, to look for the bus while events wait for it.\nconst BUS_POLL_INTERVAL = 250;\nconst BUS_POLL_DURATION = 30_000;\n\nconst getShopify = () =>\n\t(window as unknown as { Shopify?: ShopifyGlobal }).Shopify;\n\n// Shopify's script drops an event whose products lack any of these.\nconst REQUIRED_FIELDS = [\"id\", \"title\", \"vendor\", \"variantId\"] as const;\n\nconst toGid = (type: \"Product\" | \"ProductVariant\", id: string) =>\n\t/^\\d+$/.test(id) ? `gid://shopify/${type}/${id}` : id;\n\n// Numeric ids, e.g. `legacyResourceId`, as the GIDs Shopify expects.\nconst withGids = (product: ShopifyProduct): ShopifyProduct => ({\n\t...product,\n\tid: toGid(\"Product\", product.id),\n\tvariantId: toGid(\"ProductVariant\", product.variantId),\n});\n\nconst isComplete = (product: ShopifyProduct) => {\n\tconst missing: string[] = REQUIRED_FIELDS.filter((key) => !product[key]);\n\tif (!product.price.amount) missing.push(\"price\");\n\tif (missing.length > 0 && IS_DEVELOPMENT) {\n\t\tconsole.warn(\n\t\t\t`[analytics] shopify: product \"${product.id}\" needs ${missing.join(\", \")} to reach Shopify.`,\n\t\t);\n\t}\n\treturn missing.length === 0;\n};\n\ntype ShopifyCartLine = {\n\tid: string;\n\tquantity: number;\n\tmerchandise: {\n\t\tid: string;\n\t\ttitle: string;\n\t\tprice: ShopifyProduct[\"price\"];\n\t\tproduct: Pick<ShopifyProduct, \"id\" | \"title\" | \"vendor\" | \"productType\">;\n\t};\n};\n\n/**\n * The events of Hydrogen's analytics bus, with the fields Shopify's script needs. Hydrogen adds the shop itself.\n */\nexport type ShopifyEvents = {\n\tpage_viewed: { url: string };\n\tsearch_viewed: { url: string; searchTerm: string };\n\tproduct_viewed: { url: string; products: ShopifyProduct[] };\n\tcollection_viewed: {\n\t\turl: string;\n\t\tcollection: { id: string; handle: string };\n\t};\n\tproduct_added_to_cart: {\n\t\tcart: { id: string };\n\t\tprevCart: null;\n\t\tcurrentLine: ShopifyCartLine;\n\t};\n\t// Not mapped by `shopifyMapping()`; Hydrogen's `useCartAnalytics()` usually sends them.\n\tcart_viewed: { url: string; cart: { id: string } } & Record<string, unknown>;\n\tcart_updated: { cart: { id: string } } & Record<string, unknown>;\n\tproduct_removed_from_cart: { cart: { id: string } } & Record<string, unknown>;\n};\n\nconst SHOPIFY_EVENTS = {\n\tpage_viewed: true,\n\tsearch_viewed: true,\n\tproduct_viewed: true,\n\tcollection_viewed: true,\n\tproduct_added_to_cart: true,\n\tcart_viewed: true,\n\tcart_updated: true,\n\tproduct_removed_from_cart: true,\n} satisfies Record<keyof ShopifyEvents, true>;\n\nconst toCartLine = (product: ShopifyProduct): ShopifyCartLine => ({\n\tid: product.variantId,\n\tquantity: product.quantity,\n\tmerchandise: {\n\t\tid: product.variantId,\n\t\ttitle: product.variantTitle,\n\t\tprice: product.price,\n\t\tproduct: {\n\t\t\tid: product.id,\n\t\t\ttitle: product.title,\n\t\t\tvendor: product.vendor,\n\t\t\tproductType: product.productType,\n\t\t},\n\t},\n});\n\nexport type ShopifyMappingOptions<Item, CartItem> = {\n\t/**\n\t * For `itemViewed`: the project's item as Hydrogen's `ProductPayload`, or `null` to leave it out. Products without a\n\t * field Shopify requires are left out too, with a warning in development.\n\t */\n\tproduct?: (item: Item) => ShopifyProduct | null;\n\t/** For `addedToCart`: the project's cart line item as a product. Price and quantity come from the line. */\n\tcartProduct?: (\n\t\titem: CartItem,\n\t) => Omit<ShopifyProduct, \"price\" | \"quantity\"> | null;\n\t/**\n\t * The current cart's GID, read when the event is delivered. `addedToCart` reaches Shopify only with it. Leave it\n\t * out when Hydrogen's `useCartAnalytics()` tracks the cart, or adds to cart are counted twice.\n\t */\n\tgetCartId?: () => string | null | undefined;\n};\n\n/**\n * Maps `pageViewed`, and with the adapters for the project's items `itemViewed` and `addedToCart`, to Shopify's events.\n * Spread it to add others, e.g. `search_viewed` or `collection_viewed`.\n *\n * @example\n * ```ts\n * shopifyMapping<Events>({\n * product: ({ product, variant }) => ({\n * id: product.id,\n * title: product.title,\n * vendor: product.vendor,\n * variantId: variant.id,\n * variantTitle: variant.title,\n * price: variant.price,\n * quantity: 1,\n * }),\n * cartProduct: toCartProduct,\n * getCartId: () => cartStore.get().id,\n * });\n * ```\n */\nexport const shopifyMapping = <\n\tEvents extends EventMap,\n\tContext extends object = Record<never, never>,\n>({\n\tproduct: toProduct,\n\tcartProduct: toCartProduct,\n\tgetCartId,\n}: ShopifyMappingOptions<\n\tShapesOf<Events>[\"item\"],\n\tCartItemOf<Events>\n> = {}): AnalyticsMapping<Events, ShopifyEvents, Context> => {\n\ttype ViewItem = EcommerceEvents<ShapesOf<Events>>[\"itemViewed\"];\n\ttype AddToCart = CartEvents<CartItemOf<Events>>[\"addedToCart\"];\n\n\tconst mapping: AnalyticsMapping<\n\t\tPick<DomEvents, \"pageViewed\"> & {\n\t\t\titemViewed: ViewItem;\n\t\t\taddedToCart: AddToCart;\n\t\t},\n\t\tShopifyEvents\n\t> = {\n\t\tpageViewed: (payload, { context: { url } }) => ({\n\t\t\tname: \"page_viewed\",\n\t\t\tpayload: { url: new URL(payload.url ?? url, url).href },\n\t\t}),\n\t};\n\tif (toProduct) {\n\t\tmapping.itemViewed = ({ item }, { context: { url } }) => {\n\t\t\tconst product = toProduct(item);\n\t\t\t// Shopify drops the whole event when the product is incomplete.\n\t\t\tif (!product || !isComplete(product)) return null;\n\t\t\treturn {\n\t\t\t\tname: \"product_viewed\",\n\t\t\t\tpayload: { url, products: [withGids(product)] },\n\t\t\t};\n\t\t};\n\t}\n\tif (toCartProduct) {\n\t\tmapping.addedToCart = ({ currency, items }) => {\n\t\t\tconst cartId = getCartId?.();\n\t\t\tif (!cartId) return null;\n\t\t\treturn items.flatMap(({ item, quantity, price }) => {\n\t\t\t\tconst fields = toCartProduct(item);\n\t\t\t\tif (!fields) return [];\n\t\t\t\tconst product: ShopifyProduct = {\n\t\t\t\t\t...fields,\n\t\t\t\t\tprice: { amount: String(price), currencyCode: currency },\n\t\t\t\t\tquantity,\n\t\t\t\t};\n\t\t\t\tif (!isComplete(product)) return [];\n\t\t\t\treturn [\n\t\t\t\t\t{\n\t\t\t\t\t\tname: \"product_added_to_cart\" as const,\n\t\t\t\t\t\tpayload: {\n\t\t\t\t\t\t\tcart: { id: cartId },\n\t\t\t\t\t\t\tprevCart: null,\n\t\t\t\t\t\t\tcurrentLine: toCartLine(withGids(product)),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t];\n\t\t\t});\n\t\t};\n\t}\n\treturn mapping as unknown as AnalyticsMapping<Events, ShopifyEvents, Context>;\n};\n\n/**\n * Publishes events to Hydrogen's analytics bus (`window.Shopify.analytics`), which `<ShopifyScripts>` from\n * `@shopify/hydrogen/react` sets up and forwards to Shopify Analytics. Map it with `shopifyMapping<Events>()` and give it\n * `withConsent(…, [\"analytics\"])`, as Hydrogen's bus only delivers with analytics consent. Consent reaches Shopify through\n * `shopifyCustomerPrivacy()` from `@buildinams/core/consent/shopify`, or comes from it with `shopifyConsent()`. See\n * `docs/analytics.md`.\n *\n * @example\n * ```ts\n * export const analytics = createAnalytics<Events>({\n * events,\n * consent: cookiebotConsent(),\n * consentTargets: [shopifyCustomerPrivacy()],\n * destinations: [\n * withConsent(map(shopifyDestination(), shopifyMapping<Events>({ product: toShopifyProduct })), [\"analytics\"]),\n * ],\n * });\n * ```\n */\nexport const shopifyDestination = (): AnalyticsDestination<ShopifyEvents> => {\n\tlet warnedMissingBus = false;\n\t// Delivered before `<ShopifyScripts>` set up the bus, e.g. when it streams in late.\n\tlet pending: DestinationEvent<ShopifyEvents>[] = [];\n\tlet polling: ReturnType<typeof setTimeout> | undefined;\n\tlet pollUntil = 0;\n\n\tconst flushPending = () => {\n\t\tpolling = undefined;\n\t\tconst bus = getShopify()?.analytics;\n\t\tif (!bus) {\n\t\t\tif (Date.now() < pollUntil) {\n\t\t\t\tpolling = setTimeout(flushPending, BUS_POLL_INTERVAL);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tconst events = pending;\n\t\tpending = [];\n\t\tfor (const { name, payload } of events) bus.publish(name, payload);\n\t};\n\n\tconst publish = (event: DestinationEvent<ShopifyEvents>) => {\n\t\tpending.push(event);\n\t\tif (pending.length > MAX_PENDING) {\n\t\t\tpending.splice(0, pending.length - MAX_PENDING);\n\t\t}\n\t\tif (IS_DEVELOPMENT && !getShopify()?.analytics && !warnedMissingBus) {\n\t\t\twarnedMissingBus = true;\n\t\t\tconsole.warn(\n\t\t\t\t\"[analytics] shopify: window.Shopify.analytics is missing. Render <ShopifyScripts> from @shopify/hydrogen/react.\",\n\t\t\t);\n\t\t}\n\t\tif (polling === undefined) {\n\t\t\tpollUntil = Date.now() + BUS_POLL_DURATION;\n\t\t\tflushPending();\n\t\t}\n\t};\n\n\treturn {\n\t\tname: \"shopify\",\n\t\tsetup: ({ subscribe }) => {\n\t\t\tfor (const name of Object.keys(\n\t\t\t\tSHOPIFY_EVENTS,\n\t\t\t) as (keyof ShopifyEvents)[]) {\n\t\t\t\tsubscribe(name, (payload) =>\n\t\t\t\t\tpublish({ name, payload } as DestinationEvent<ShopifyEvents>),\n\t\t\t\t);\n\t\t\t}\n\t\t},\n\t};\n};\n","import type { CartSnapshot } from \"../../tracking/cart\";\n\ntype Money = { amount: string; currencyCode: string };\n\n/** A Storefront API cart line, with the fields `toStorefrontCartSnapshot()` reads. */\nexport type ShopifyStorefrontCartLine = {\n\tid: string;\n\tquantity: number;\n\tcost: { amountPerQuantity: Money };\n\tmerchandise: {\n\t\tid: string;\n\t\ttitle: string;\n\t\tsku?: string | null;\n\t\tproduct: {\n\t\t\tid: string;\n\t\t\ttitle: string;\n\t\t\tvendor: string;\n\t\t\tproductType?: string | null;\n\t\t};\n\t};\n};\n\n/** A Storefront API cart, with lines as `nodes` or `edges`. */\nexport type ShopifyStorefrontCart = {\n\tid: string;\n\tcost?: { totalAmount?: Money; subtotalAmount?: Money };\n\tlines: {\n\t\tnodes?: ShopifyStorefrontCartLine[];\n\t\tedges?: { node: ShopifyStorefrontCartLine }[];\n\t};\n};\n\n/**\n * A Storefront API cart as a `CartSnapshot` for `createCartChanges()`, with each line as the item, so `ga4Mapping()`'s\n * `cartItem` and `shopifyMapping()`'s `cartProduct` read it as it came from Shopify.\n */\nexport const toStorefrontCartSnapshot = (\n\tcart: ShopifyStorefrontCart,\n): CartSnapshot<ShopifyStorefrontCartLine> => {\n\tconst lines =\n\t\tcart.lines.nodes ?? cart.lines.edges?.map(({ node }) => node) ?? [];\n\treturn {\n\t\tid: cart.id,\n\t\tcurrency:\n\t\t\tcart.cost?.totalAmount?.currencyCode ??\n\t\t\tcart.cost?.subtotalAmount?.currencyCode ??\n\t\t\tlines[0]?.cost.amountPerQuantity.currencyCode ??\n\t\t\t\"\",\n\t\tlines: lines.map((line) => ({\n\t\t\tid: line.id,\n\t\t\tquantity: line.quantity,\n\t\t\tprice: Number(line.cost.amountPerQuantity.amount),\n\t\t\titem: line,\n\t\t})),\n\t};\n};\n\n/** For `shopifyMapping()`'s `cartProduct`, with carts from `toStorefrontCartSnapshot()`. */\nexport const toStorefrontCartProduct = ({\n\tmerchandise,\n}: ShopifyStorefrontCartLine) => ({\n\tid: merchandise.product.id,\n\ttitle: merchandise.product.title,\n\tvendor: merchandise.product.vendor,\n\tvariantId: merchandise.id,\n\tvariantTitle: merchandise.title,\n\tproductType: merchandise.product.productType ?? undefined,\n});\n"]}
@@ -22,18 +22,37 @@ var createStringSchema = (template) => {
22
22
  // src/schema/href-schema.ts
23
23
  var hrefSchema = createStringSchema({
24
24
  parse: (href) => {
25
- const [pathname = "", search = ""] = href.split("?");
25
+ href = href.includes("://") ? href.replace("://", "").split("/").slice(1).join("/") : href;
26
+ let hash = "";
27
+ let pathname = "";
28
+ let search = "";
29
+ if (href.includes("#")) {
30
+ [href = "", hash = ""] = href.split("#");
31
+ }
32
+ if (href.includes("?")) {
33
+ [pathname = "", search = ""] = href.split("?");
34
+ }
26
35
  return {
27
36
  pathname,
37
+ hash,
28
38
  searchParams: new URLSearchParams(search)
29
39
  };
30
40
  },
31
41
  stringify: (state) => {
32
- const paramsString = state.searchParams.toString();
33
- return state.pathname + (paramsString ? `?${paramsString}` : "");
42
+ let href = state.pathname;
43
+ if (state.searchParams) {
44
+ const search = state.searchParams.toString();
45
+ if (search) {
46
+ href += `?${search}`;
47
+ }
48
+ }
49
+ if (state.hash) {
50
+ href += `#${state.hash}`;
51
+ }
52
+ return href;
34
53
  }
35
54
  });
36
55
 
37
56
  export { createSchema, createStringSchema, hrefSchema };
38
- //# sourceMappingURL=chunk-IAAQVL6M.js.map
39
- //# sourceMappingURL=chunk-IAAQVL6M.js.map
57
+ //# sourceMappingURL=chunk-FJ7SNQQI.js.map
58
+ //# sourceMappingURL=chunk-FJ7SNQQI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/schema/schema.ts","../src/schema/string-schema.ts","../src/schema/href-schema.ts"],"names":[],"mappings":";AAkGO,IAAM,YAAA,GAAe,CAK3B,QAAA,MACmD;AAAA,EACnD,GAAG,QAAA;AAAA,EAEH,YAAA,EAAc,CAAC,KAAA,EAAO,IAAA,KAAS;AAC9B,IAAA,OAAO,SAAS,SAAA,CAAU,IAAA,CAAK,SAAS,KAAA,CAAM,KAAK,CAAC,CAAC,CAAA;AAAA,EACtD;AACD,CAAA;;;AC9FO,IAAM,kBAAA,GAAqB,CACjC,QAAA,KAC4C;AAC5C,EAAA,MAAM,SAAS,YAAA,CAA+C;AAAA,IAC7D,WAAW,QAAA,CAAS,SAAA;AAAA,IACpB,OAAO,QAAA,CAAS;AAAA,GAChB,CAAA;AAED,EAAA,OAAO;AAAA,IACN,OAAO,MAAA,CAAO,KAAA;AAAA,IACd,WAAW,MAAA,CAAO,SAAA;AAAA,IAClB,gBAAgB,MAAA,CAAO;AAAA,GACxB;AACD;;;ACKO,IAAM,aAAa,kBAAA,CAWxB;AAAA,EACD,KAAA,EAAO,CAAC,IAAA,KAAS;AAEhB,IAAA,IAAA,GAAO,KAAK,QAAA,CAAS,KAAK,CAAA,GACvB,IAAA,CAAK,QAAQ,KAAA,EAAO,EAAE,CAAA,CAAE,KAAA,CAAM,GAAG,CAAA,CAAE,KAAA,CAAM,CAAC,CAAA,CAAE,IAAA,CAAK,GAAG,CAAA,GACpD,IAAA;AAEH,IAAA,IAAI,IAAA,GAAO,EAAA;AACX,IAAA,IAAI,QAAA,GAAW,EAAA;AACf,IAAA,IAAI,MAAA,GAAS,EAAA;AAEb,IAAA,IAAI,IAAA,CAAK,QAAA,CAAS,GAAG,CAAA,EAAG;AACvB,MAAA,CAAC,OAAO,EAAA,EAAI,IAAA,GAAO,EAAE,CAAA,GAAI,IAAA,CAAK,MAAM,GAAG,CAAA;AAAA,IACxC;AACA,IAAA,IAAI,IAAA,CAAK,QAAA,CAAS,GAAG,CAAA,EAAG;AACvB,MAAA,CAAC,WAAW,EAAA,EAAI,MAAA,GAAS,EAAE,CAAA,GAAI,IAAA,CAAK,MAAM,GAAG,CAAA;AAAA,IAC9C;AAEA,IAAA,OAAO;AAAA,MACN,QAAA;AAAA,MACA,IAAA;AAAA,MACA,YAAA,EAAc,IAAI,eAAA,CAAgB,MAAM;AAAA,KACzC;AAAA,EACD,CAAA;AAAA,EACA,SAAA,EAAW,CAAC,KAAA,KAAU;AACrB,IAAA,IAAI,OAAO,KAAA,CAAM,QAAA;AACjB,IAAA,IAAI,MAAM,YAAA,EAAc;AACvB,MAAA,MAAM,MAAA,GAAS,KAAA,CAAM,YAAA,CAAa,QAAA,EAAS;AAC3C,MAAA,IAAI,MAAA,EAAQ;AACX,QAAA,IAAA,IAAQ,IAAI,MAAM,CAAA,CAAA;AAAA,MACnB;AAAA,IACD;AACA,IAAA,IAAI,MAAM,IAAA,EAAM;AACf,MAAA,IAAA,IAAQ,CAAA,CAAA,EAAI,MAAM,IAAI,CAAA,CAAA;AAAA,IACvB;AACA,IAAA,OAAO,IAAA;AAAA,EACR;AACD,CAAC","file":"chunk-FJ7SNQQI.js","sourcesContent":["/**\n * Defines the schema for parsing and stringifying states to and from primitive values.\n *\n * The `Schema` type is a generic type that takes:\n * - `State`: A state object type.\n * - `Serialized`: The raw value type\n * - `SerializableState?`: (optional) A (partial) state object type that can be used for serialization. Defaults to `State` if not provided.\n *\n * It returns the following methods:\n * - `parse`: A function that takes a raw value and returns the corresponding state object.\n * - `serialize`: A function that takes a (serializable) state object and returns a raw value that represents the given state.\n * - `parserialize`: A function that takes a raw value and a reducer function - it returns a new raw value that represents the reduced state.\n *\n * @see createSchema for a helper function to create a `Schema` from simpler parsing and serializing functions.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type Schema<State = any, Serialized = any, SerializableState = State> = {\n\t/** Parse a raw value into the state shape */\n\tparse: (value: Serialized) => State;\n\t/** Serialize a state shape into a raw value */\n\tserialize: (state: SerializableState) => Serialized;\n\n\t/** Compute a new serialized value based on a given serialized value and a reducer of the current state. */\n\tparserialize: (\n\t\tvalue: Serialized,\n\t\tnext: (state: State) => SerializableState,\n\t) => Serialized;\n};\n\n/**\n * A utility type to extract the state shape from a given `Schema`.\n */\nexport type SchemaState<T extends Schema> =\n\tT extends Schema<infer U> ? U : never;\n\n/**\n * A utility type to extract the serialized value type from a given `Schema`.\n */\nexport type SchemaSerialized<T extends Schema> =\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tT extends Schema<any, infer U> ? U : never;\n\n/**\n * A utility type to extract the serializable state shape from a given `Schema`.\n */\nexport type SchemaSerializableState<T extends Schema> =\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tT extends Schema<any, any, infer U> ? U : never;\n\n/**\n * Low-level helper function to create a basic `Schema` that receives the config for converting between serialized values and state objects.\n *\n * The template requires two methods:\n * - `parse`: A method that takes a serialized value and returns the corresponding state object.\n * - `serialize`: A method that takes a state object and returns a serialized value that represents the given state.\n *\n * The `parserialize` method is automatically implemented based on the provided `parse` and `serialize` methods, allowing you to compute a new serialized value based on an existing serialized value and a reducer function for the state.\n *\n * @example\n * ```ts\n * import { createSchema } from \"@buildinams/core/schema\";\n *\n * const numberSchema = createSchema<\n * number, // State\n * string // Serialized\n * >({\n * parse: (value) => parseInt(value, 10),\n * serialize: (state) => state.toString(),\n * });\n *\n * const result = numberSchema.parserialize(\"42\", (state) => state + 1);\n * console.log(result); // \"43\"\n * ```\n *\n * @example\n * ```ts\n * import { createSchema } from \"@buildinams/core/schema\";\n *\n * const hrefSchema: createSchema<\n * { pathname: string, searchParams: URLSearchParams },\n * string,\n * { pathname: string, searchParams?: URLSearchParams } // SerializableState allows to define different state used for serialization, in this case making searchParams optional to allow for more flexible stringification logic\n * >({\n * parse: (href) => {\n * const [pathname = \"\", search = \"\"] = href.split(\"?\");\n * return {\n * pathname,\n * searchParams: new URLSearchParams(search),\n * };\n * },\n * serialize: (serializableState) => {\n * if (!serializableState.searchParams) return serializableState.pathname;\n * const paramsString = serializableState.searchParams.toString();\n * return serializableState.pathname + (paramsString ? `?${paramsString}` : \"\");\n * },\n * })\n * ```\n */\nexport const createSchema = <\n\tState = never,\n\tSerialized = never,\n\tSerializableState = State,\n>(\n\ttemplate: Omit<Schema<State, Serialized, SerializableState>, \"parserialize\">,\n): Schema<State, Serialized, SerializableState> => ({\n\t...template,\n\n\tparserialize: (value, next) => {\n\t\treturn template.serialize(next(template.parse(value)));\n\t},\n});\n","import type { Schema } from \"./schema\";\nimport { createSchema } from \"./schema\";\n\nexport type StringSchema<State = never, SerializableState = State> = {\n\tparse: Schema<State, string, SerializableState>[\"parse\"];\n\tstringify: Schema<State, string, SerializableState>[\"serialize\"];\n\tparseStringify: Schema<State, string, SerializableState>[\"parserialize\"];\n};\n\n/**\n * Helper function to create a `StringSchema` which converts between strings and state objects.\n *\n * The template requires two functions:\n * - `parse`: A function that takes a string and returns the corresponding state object.\n * - `stringify`: A function that takes a state object and returns a string that represents the given state.\n */\nexport const createStringSchema = <State = never, SerializableState = State>(\n\ttemplate: Omit<StringSchema<State, SerializableState>, \"parseStringify\">,\n): StringSchema<State, SerializableState> => {\n\tconst schema = createSchema<State, string, SerializableState>({\n\t\tserialize: template.stringify,\n\t\tparse: template.parse,\n\t});\n\n\treturn {\n\t\tparse: schema.parse,\n\t\tstringify: schema.serialize,\n\t\tparseStringify: schema.parserialize,\n\t};\n};\n","import { createStringSchema } from \"./string-schema\";\n\n/**\n * Utility schema for parsing and stringifying href strings into structured state objects that include the pathname and URL search parameters.\n *\n * @example\n * ```ts\n * import { createSchema, hrefSchema } from \"@buildinams/core/schema\";\n *\n * const myHrefSchema = createSchema<{\n * page: number,\n * // Used to compute next href - don't use directly. Use `.stringify()` instead\n * _href: string\n * }>({\n * parse: (href) => {\n * const { searchParams } = hrefSchema.parse(href);\n * return {\n * page: parseInt(searchParams.get(\"page\") || \"1\", 10),\n * _href: href,\n * }\n * },\n * serialize: (state) => {\n * const { pathname, searchParams } = hrefSchema.parse(state._href);\n * searchParams.delete(\"page\");\n * if (state.page > 1) {\n * searchParams.set(\"page\", state.page.toString());\n * }\n *\n * // note that `searchParams` are optional for serialization\n * return hrefSchema.stringify({ pathname, searchParams });\n * }\n * });\n * ```\n */\nexport const hrefSchema = createStringSchema<\n\t{\n\t\tpathname: string;\n\t\thash: string;\n\t\tsearchParams: URLSearchParams;\n\t},\n\t{\n\t\tpathname: string;\n\t\thash?: string;\n\t\tsearchParams?: URLSearchParams;\n\t}\n>({\n\tparse: (href) => {\n\t\t// strip the protocol and domain from the href if present, leaving only the pathname and search parameters\n\t\thref = href.includes(\"://\")\n\t\t\t? href.replace(\"://\", \"\").split(\"/\").slice(1).join(\"/\")\n\t\t\t: href;\n\n\t\tlet hash = \"\";\n\t\tlet pathname = \"\";\n\t\tlet search = \"\";\n\n\t\tif (href.includes(\"#\")) {\n\t\t\t[href = \"\", hash = \"\"] = href.split(\"#\");\n\t\t}\n\t\tif (href.includes(\"?\")) {\n\t\t\t[pathname = \"\", search = \"\"] = href.split(\"?\");\n\t\t}\n\n\t\treturn {\n\t\t\tpathname,\n\t\t\thash,\n\t\t\tsearchParams: new URLSearchParams(search),\n\t\t};\n\t},\n\tstringify: (state) => {\n\t\tlet href = state.pathname;\n\t\tif (state.searchParams) {\n\t\t\tconst search = state.searchParams.toString();\n\t\t\tif (search) {\n\t\t\t\thref += `?${search}`;\n\t\t\t}\n\t\t}\n\t\tif (state.hash) {\n\t\t\thref += `#${state.hash}`;\n\t\t}\n\t\treturn href;\n\t},\n});\n"]}
@@ -0,0 +1,8 @@
1
+ // src/analytics/define-events.ts
2
+ var SHAPE = Object.freeze({});
3
+ var shape = () => SHAPE;
4
+ var defineEvents = (catalog) => catalog;
5
+
6
+ export { defineEvents, shape };
7
+ //# sourceMappingURL=chunk-G2BKC4BU.js.map
8
+ //# sourceMappingURL=chunk-G2BKC4BU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/analytics/define-events.ts"],"names":[],"mappings":";AAOA,IAAM,KAAA,GAAsB,MAAA,CAAO,MAAA,CAAO,EAAE,CAAA;AAWrC,IAAM,QAAQ,MAAmB;AA0CjC,IAAM,YAAA,GAAe,CAC3B,OAAA,KACa","file":"chunk-G2BKC4BU.js","sourcesContent":["import type { EventMap } from \"./events\";\n\ndeclare const shapeType: unique symbol;\n\n/** A type as a value, from `shape<T>()`. Holds nothing at runtime. */\nexport type Shape<T> = { readonly [shapeType]?: T };\n\nconst SHAPE: Shape<never> = Object.freeze({});\n\n/**\n * A type as a value, e.g. an event's payload in `defineEvents()`. Holds nothing at runtime, so it costs nothing in the\n * bundle.\n *\n * @example\n * ```ts\n * defineEvents({ login: { description: \"…\", payload: shape<{ method: \"email\" }>() } });\n * ```\n */\nexport const shape = <T>(): Shape<T> => SHAPE;\n\n/** One event of the project: what it means and when it's tracked, and its payload. */\nexport type EventDefinition<Payload extends object = object> = {\n\t/** When it's tracked and what it means, for whoever reads the catalog: developers, agents and analysts. */\n\tdescription: string;\n\t/** Leave it out for events without fields. */\n\tpayload?: Shape<Payload>;\n};\n\n/** Every event of the project, by name. */\nexport type EventCatalog<Events extends EventMap = EventMap> = {\n\t[N in keyof Events]: Record<never, never> extends Events[N]\n\t\t? EventDefinition<Events[N]>\n\t\t: EventDefinition<Events[N]> & { payload: Shape<Events[N]> };\n};\n\n/** The events of a catalog from `defineEvents()`, for `createAnalytics<Events>()`. */\nexport type EventsOf<Catalog> = {\n\t[N in keyof Catalog]: Catalog[N] extends { payload?: Shape<infer Payload> }\n\t\t? unknown extends Payload\n\t\t\t? Record<never, never>\n\t\t\t: Payload\n\t\t: Record<never, never>;\n};\n\n/**\n * Declares the project's events as a value: each event's name, what it means, and its payload. It's the one place\n * to read what the project tracks, also at runtime through `analytics.routes()`. Pass it to `createAnalytics()`.\n *\n * @example\n * ```ts\n * export const events = defineEvents({\n * newsletterSignedUp: {\n * description: \"The newsletter form was submitted successfully.\",\n * payload: shape<{ source: \"footer\" | \"popup\" }>(),\n * },\n * });\n *\n * export type Events = EventsOf<typeof events>;\n * ```\n */\nexport const defineEvents = <Catalog extends Record<string, EventDefinition>>(\n\tcatalog: Catalog,\n): Catalog => catalog;\n"]}
@@ -0,0 +1,14 @@
1
+ // src/config/index.ts
2
+ var IS_DEVELOPMENT = process.env.NODE_ENV === "development";
3
+ var IS_BUILD = process.env.NODE_ENV === "production";
4
+ var IS_IN_BROWSER = (() => {
5
+ try {
6
+ return !!window && "document" in window && !!window.document?.createElement;
7
+ } catch {
8
+ return false;
9
+ }
10
+ })();
11
+
12
+ export { IS_BUILD, IS_DEVELOPMENT, IS_IN_BROWSER };
13
+ //# sourceMappingURL=chunk-GQVN2ECX.js.map
14
+ //# sourceMappingURL=chunk-GQVN2ECX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config/index.ts"],"names":[],"mappings":";AAAO,IAAM,cAAA,GAAiB,OAAA,CAAQ,GAAA,CAAI,QAAA,KAAa;AAChD,IAAM,QAAA,GAAW,OAAA,CAAQ,GAAA,CAAI,QAAA,KAAa;AAE1C,IAAM,iBAAiB,MAAM;AACnC,EAAA,IAAI;AACH,IAAA,OAAO,CAAC,CAAC,MAAA,IAAU,UAAA,IAAc,UAAU,CAAC,CAAC,OAAO,QAAA,EAAU,aAAA;AAAA,EAC/D,CAAA,CAAA,MAAQ;AACP,IAAA,OAAO,KAAA;AAAA,EACR;AACD,CAAA","file":"chunk-GQVN2ECX.js","sourcesContent":["export const IS_DEVELOPMENT = process.env.NODE_ENV === \"development\";\nexport const IS_BUILD = process.env.NODE_ENV === \"production\";\n\nexport const IS_IN_BROWSER = (() => {\n\ttry {\n\t\treturn !!window && \"document\" in window && !!window.document?.createElement;\n\t} catch {\n\t\treturn false;\n\t}\n})();\n"]}
@@ -0,0 +1,22 @@
1
+ // src/functions/shallow-equal.ts
2
+ function shallowEqual(a, b) {
3
+ if (a === b) return true;
4
+ if (!a) return false;
5
+ if (!b) return false;
6
+ if (typeof a !== "object") return false;
7
+ if (typeof b !== "object") return false;
8
+ const aKeys = Object.keys(a);
9
+ const bKeys = Object.keys(b);
10
+ const { length } = aKeys;
11
+ if (bKeys.length !== length) return false;
12
+ for (const key of aKeys) {
13
+ if (a[key] !== b[key]) {
14
+ return false;
15
+ }
16
+ }
17
+ return true;
18
+ }
19
+
20
+ export { shallowEqual };
21
+ //# sourceMappingURL=chunk-LL3VTFNY.js.map
22
+ //# sourceMappingURL=chunk-LL3VTFNY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/functions/shallow-equal.ts"],"names":[],"mappings":";AAUO,SAAS,YAAA,CAAa,GAAe,CAAA,EAAe;AAC1D,EAAA,IAAI,CAAA,KAAM,GAAG,OAAO,IAAA;AACpB,EAAA,IAAI,CAAC,GAAG,OAAO,KAAA;AACf,EAAA,IAAI,CAAC,GAAG,OAAO,KAAA;AACf,EAAA,IAAI,OAAO,CAAA,KAAM,QAAA,EAAU,OAAO,KAAA;AAClC,EAAA,IAAI,OAAO,CAAA,KAAM,QAAA,EAAU,OAAO,KAAA;AAElC,EAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,IAAA,CAAK,CAAC,CAAA;AAC3B,EAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,IAAA,CAAK,CAAC,CAAA;AAC3B,EAAA,MAAM,EAAE,QAAO,GAAI,KAAA;AAEnB,EAAA,IAAI,KAAA,CAAM,MAAA,KAAW,MAAA,EAAQ,OAAO,KAAA;AAEpC,EAAA,KAAA,MAAW,OAAO,KAAA,EAAO;AACxB,IAAA,IAAI,CAAA,CAAE,GAAG,CAAA,KAAM,CAAA,CAAE,GAAG,CAAA,EAAG;AACtB,MAAA,OAAO,KAAA;AAAA,IACR;AAAA,EACD;AAEA,EAAA,OAAO,IAAA;AACR","file":"chunk-LL3VTFNY.js","sourcesContent":["import type { AnyObject } from \"src/types\";\n\n/**\n * Compares two objects.\n * @example\n * shallowEqual({ a: \"a\" }, {}); // false\n * shallowEqual({ a: \"a\" }, { b: \"b\" }); // false\n * shallowEqual({ a: \"a\" }, { a: \"a\" }); // true\n * shallowEqual({ a: \"a\" }, { a: \"a\", b: \"b\" }); // false\n */\nexport function shallowEqual(a?: AnyObject, b?: AnyObject) {\n\tif (a === b) return true;\n\tif (!a) return false;\n\tif (!b) return false;\n\tif (typeof a !== \"object\") return false;\n\tif (typeof b !== \"object\") return false;\n\n\tconst aKeys = Object.keys(a);\n\tconst bKeys = Object.keys(b);\n\tconst { length } = aKeys;\n\n\tif (bKeys.length !== length) return false;\n\n\tfor (const key of aKeys) {\n\t\tif (a[key] !== b[key]) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\treturn true;\n}\n"]}
@@ -0,0 +1,48 @@
1
+ // src/tracking/listeners.ts
2
+ var createListeners = ({
3
+ start,
4
+ stop
5
+ } = {}) => {
6
+ const entries = /* @__PURE__ */ new Set();
7
+ const subscribe = ((...args) => {
8
+ if (typeof window === "undefined") return () => {
9
+ };
10
+ const entry = args.length === 1 ? { listener: args[0] } : {
11
+ names: new Set(typeof args[0] === "string" ? [args[0]] : args[0]),
12
+ listener: args[1]
13
+ };
14
+ entries.add(entry);
15
+ if (entries.size === 1) {
16
+ try {
17
+ start?.();
18
+ } catch (error) {
19
+ entries.delete(entry);
20
+ throw error;
21
+ }
22
+ }
23
+ return () => {
24
+ if (!entries.has(entry)) return;
25
+ if (entries.size === 1) stop?.();
26
+ entries.delete(entry);
27
+ };
28
+ });
29
+ const emit = (event) => {
30
+ for (const { names, listener } of [...entries]) {
31
+ if (names && !names.has(event.name)) continue;
32
+ try {
33
+ listener(event);
34
+ } catch (error) {
35
+ console.error(`[analytics] ${event.name}`, error);
36
+ }
37
+ }
38
+ };
39
+ return {
40
+ subscribe,
41
+ emit,
42
+ hasListeners: () => entries.size > 0
43
+ };
44
+ };
45
+
46
+ export { createListeners };
47
+ //# sourceMappingURL=chunk-M3HNM5HC.js.map
48
+ //# sourceMappingURL=chunk-M3HNM5HC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tracking/listeners.ts"],"names":[],"mappings":";AAoBO,IAAM,kBAAkB,CAAsB;AAAA,EACpD,KAAA;AAAA,EACA;AACD,CAAA,GAGI,EAAC,KAAM;AACV,EAAA,MAAM,OAAA,uBAAc,GAAA,EAAkB;AAEtC,EAAA,MAAM,SAAA,IAAa,IACf,IAAA,KAGC;AACJ,IAAA,IAAI,OAAO,MAAA,KAAW,WAAA,EAAa,OAAO,MAAM;AAAA,IAAC,CAAA;AACjD,IAAA,MAAM,KAAA,GACL,KAAK,MAAA,KAAW,CAAA,GACb,EAAE,QAAA,EAAU,IAAA,CAAK,CAAC,CAAA,EAAE,GACpB;AAAA,MACA,KAAA,EAAO,IAAI,GAAA,CAAI,OAAO,KAAK,CAAC,CAAA,KAAM,QAAA,GAAW,CAAC,KAAK,CAAC,CAAC,CAAA,GAAI,IAAA,CAAK,CAAC,CAAC,CAAA;AAAA,MAChE,QAAA,EAAU,KAAK,CAAC;AAAA,KACjB;AACH,IAAA,OAAA,CAAQ,IAAI,KAAK,CAAA;AACjB,IAAA,IAAI,OAAA,CAAQ,SAAS,CAAA,EAAG;AACvB,MAAA,IAAI;AACH,QAAA,KAAA,IAAQ;AAAA,MACT,SAAS,KAAA,EAAO;AAEf,QAAA,OAAA,CAAQ,OAAO,KAAK,CAAA;AACpB,QAAA,MAAM,KAAA;AAAA,MACP;AAAA,IACD;AACA,IAAA,OAAO,MAAM;AACZ,MAAA,IAAI,CAAC,OAAA,CAAQ,GAAA,CAAI,KAAK,CAAA,EAAG;AAEzB,MAAA,IAAI,OAAA,CAAQ,IAAA,KAAS,CAAA,EAAG,IAAA,IAAO;AAC/B,MAAA,OAAA,CAAQ,OAAO,KAAK,CAAA;AAAA,IACrB,CAAA;AAAA,EACD,CAAA,CAAA;AAEA,EAAA,MAAM,IAAA,GAAO,CAAC,KAAA,KAAiB;AAC9B,IAAA,KAAA,MAAW,EAAE,KAAA,EAAO,QAAA,MAAc,CAAC,GAAG,OAAO,CAAA,EAAG;AAC/C,MAAA,IAAI,SAAS,CAAC,KAAA,CAAM,GAAA,CAAI,KAAA,CAAM,IAAI,CAAA,EAAG;AACrC,MAAA,IAAI;AACH,QAAA,QAAA,CAAS,KAAK,CAAA;AAAA,MACf,SAAS,KAAA,EAAO;AACf,QAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,YAAA,EAAe,KAAA,CAAM,IAAI,IAAI,KAAK,CAAA;AAAA,MACjD;AAAA,IACD;AAAA,EACD,CAAA;AAEA,EAAA,OAAO;AAAA,IACN,SAAA;AAAA,IACA,IAAA;AAAA,IACA,YAAA,EAAc,MAAM,OAAA,CAAQ,IAAA,GAAO;AAAA,GACpC;AACD","file":"chunk-M3HNM5HC.js","sourcesContent":["type Named = { name: string };\n\n/**\n * Subscribes `listener` to every event, or only to those named. A listener that throws is logged, so it doesn't break\n * the others. Returns a function that unsubscribes. Does nothing on the server.\n */\nexport type Subscribe<Event extends Named> = {\n\t(listener: (event: Event) => void): () => void;\n\t<Name extends Event[\"name\"]>(\n\t\tnames: Name | readonly Name[],\n\t\tlistener: (event: Extract<Event, { name: Name }>) => void,\n\t): () => void;\n};\n\ntype Entry<Event> = {\n\tnames?: ReadonlySet<string>;\n\tlistener: (event: Event) => void;\n};\n\n/** `start` runs when the first listener subscribes, `stop` when the last one unsubscribes, before it's removed. */\nexport const createListeners = <Event extends Named>({\n\tstart,\n\tstop,\n}: {\n\tstart?: () => void;\n\tstop?: () => void;\n} = {}) => {\n\tconst entries = new Set<Entry<Event>>();\n\n\tconst subscribe = ((\n\t\t...args:\n\t\t\t| [Entry<Event>[\"listener\"]]\n\t\t\t| [string | readonly string[], Entry<Event>[\"listener\"]]\n\t) => {\n\t\tif (typeof window === \"undefined\") return () => {};\n\t\tconst entry: Entry<Event> =\n\t\t\targs.length === 1\n\t\t\t\t? { listener: args[0] }\n\t\t\t\t: {\n\t\t\t\t\t\tnames: new Set(typeof args[0] === \"string\" ? [args[0]] : args[0]),\n\t\t\t\t\t\tlistener: args[1],\n\t\t\t\t\t};\n\t\tentries.add(entry);\n\t\tif (entries.size === 1) {\n\t\t\ttry {\n\t\t\t\tstart?.();\n\t\t\t} catch (error) {\n\t\t\t\t// So a later subscription starts again.\n\t\t\t\tentries.delete(entry);\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t}\n\t\treturn () => {\n\t\t\tif (!entries.has(entry)) return;\n\t\t\t// Before removing it, so what `stop` reports still reaches it.\n\t\t\tif (entries.size === 1) stop?.();\n\t\t\tentries.delete(entry);\n\t\t};\n\t}) as Subscribe<Event>;\n\n\tconst emit = (event: Event) => {\n\t\tfor (const { names, listener } of [...entries]) {\n\t\t\tif (names && !names.has(event.name)) continue;\n\t\t\ttry {\n\t\t\t\tlistener(event);\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(`[analytics] ${event.name}`, error);\n\t\t\t}\n\t\t}\n\t};\n\n\treturn {\n\t\tsubscribe,\n\t\temit,\n\t\thasListeners: () => entries.size > 0,\n\t};\n};\n"]}
@@ -0,0 +1,11 @@
1
+ // src/consent/consent.ts
2
+ var CONSENT_CATEGORIES = [
3
+ "preferences",
4
+ "analytics",
5
+ "marketing"
6
+ ];
7
+ var hasConsent = (consent, categories = []) => consent !== null && categories.every((category) => consent[category]);
8
+
9
+ export { CONSENT_CATEGORIES, hasConsent };
10
+ //# sourceMappingURL=chunk-MHFGMH3G.js.map
11
+ //# sourceMappingURL=chunk-MHFGMH3G.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/consent/consent.ts"],"names":[],"mappings":";AASO,IAAM,kBAAA,GAAqB;AAAA,EACjC,aAAA;AAAA,EACA,WAAA;AAAA,EACA;AACD;AASO,IAAM,UAAA,GAAa,CACzB,OAAA,EACA,UAAA,GAAyC,EAAC,KAE1C,OAAA,KAAY,IAAA,IAAQ,UAAA,CAAW,KAAA,CAAM,CAAC,QAAA,KAAa,OAAA,CAAQ,QAAQ,CAAC","file":"chunk-MHFGMH3G.js","sourcesContent":["/**\n * Generic consent categories. Strictly necessary storage is always allowed, so it has no category.\n *\n * | Category | Google Consent Mode v2 | Shopify Customer Privacy | Meta Pixel |\n * | ------------- | ----------------------------------------------------- | ----------------------------- | ---------------- |\n * | `preferences` | `functionality_storage`, `personalization_storage` | `preferences` | — |\n * | `analytics` | `analytics_storage` | `analytics` | — |\n * | `marketing` | `ad_storage`, `ad_user_data`, `ad_personalization` | `marketing`, read with `sale_of_data` | `grant`/`revoke` |\n */\nexport const CONSENT_CATEGORIES = [\n\t\"preferences\",\n\t\"analytics\",\n\t\"marketing\",\n] as const;\n\nexport type ConsentCategory = (typeof CONSENT_CATEGORIES)[number];\n\nexport type Consent = Record<ConsentCategory, boolean>;\n\n/** `null` until the visitor has answered. */\nexport type ConsentState = Consent | null;\n\nexport const hasConsent = (\n\tconsent: ConsentState,\n\tcategories: readonly ConsentCategory[] = [],\n): consent is Consent =>\n\tconsent !== null && categories.every((category) => consent[category]);\n\n/**\n * Where consent comes from, usually a consent platform: `cookiebotConsent()`, `oneTrustConsent()`, or your own.\n * Pass it to `createAnalytics()` from `@buildinams/core/analytics`.\n */\nexport type ConsentSource = {\n\t/** The platform, e.g. `\"cookiebot\"`. Consent isn't applied to a `ConsentTarget` with the same name. */\n\tname?: string;\n\t/** `null` until the visitor has answered, and on the server. */\n\tget: () => Consent | null;\n\t/** Calls `listener` with answered consent when it may have changed; repeats are fine. Returns an unsubscribe function. */\n\tsubscribe: (listener: (consent: Consent) => void) => () => void;\n};\n\n/**\n * Is told the visitor's consent, e.g. Google Consent Mode or Shopify's Customer Privacy API. Pass it to\n * `createAnalytics({ consentTargets })`, which applies every answer, except to a target named like the source.\n */\nexport type ConsentTarget = {\n\tname: string;\n\tapply: (consent: Consent) => void | Promise<void>;\n};\n"]}
@@ -0,0 +1,50 @@
1
+ // src/functions/apply-state.ts
2
+ function isUpdater(argument) {
3
+ return typeof argument === "function";
4
+ }
5
+ function isLazyValue(value) {
6
+ return typeof value === "function";
7
+ }
8
+ function applyState(argument, currentValue) {
9
+ if (isUpdater(argument)) {
10
+ const value = isLazyValue(currentValue) ? currentValue() : currentValue;
11
+ return argument(value);
12
+ }
13
+ return argument;
14
+ }
15
+
16
+ // src/functions/default-value.ts
17
+ function defaultValue(...values) {
18
+ for (const value of values) {
19
+ if (value !== void 0) return value;
20
+ }
21
+ return void 0;
22
+ }
23
+
24
+ // src/functions/type-guards.ts
25
+ var notEmpty = (value) => value !== null && value !== void 0;
26
+ var isHTMLElement = (node) => node instanceof HTMLElement;
27
+ var isInArray = (item, array) => array.includes(item);
28
+ var isObject = (object) => {
29
+ try {
30
+ return typeof object === "object" && object !== null && !Array.isArray(object);
31
+ } catch {
32
+ throw new Error("isObject: Invalid input");
33
+ }
34
+ };
35
+ var isTruthy = (value) => !!value;
36
+ function invariant(condition, message) {
37
+ if (condition) return;
38
+ if (typeof message !== "string") throw new Error("Invariant failed");
39
+ throw new Error(message);
40
+ }
41
+ var hasOwnProperty = (object, prop) => {
42
+ if (typeof Object.hasOwn === "function") {
43
+ return Object.hasOwn(object, prop);
44
+ }
45
+ return Object.prototype.hasOwnProperty.call(object, prop);
46
+ };
47
+
48
+ export { applyState, defaultValue, hasOwnProperty, invariant, isHTMLElement, isInArray, isObject, isTruthy, notEmpty };
49
+ //# sourceMappingURL=chunk-OS2T6P2T.js.map
50
+ //# sourceMappingURL=chunk-OS2T6P2T.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/functions/apply-state.ts","../src/functions/default-value.ts","../src/functions/type-guards.ts"],"names":[],"mappings":";AAIA,SAAS,UACR,QAAA,EACqD;AACrD,EAAA,OAAO,OAAO,QAAA,KAAa,UAAA;AAC5B;AAEA,SAAS,YAAe,KAAA,EAA8B;AACrD,EAAA,OAAO,OAAO,KAAA,KAAU,UAAA;AACzB;AASO,SAAS,UAAA,CACf,UACA,YAAA,EACC;AACD,EAAA,IAAI,SAAA,CAAU,QAAQ,CAAA,EAAG;AACxB,IAAA,MAAM,KAAA,GAAQ,WAAA,CAAY,YAAY,CAAA,GAAI,cAAa,GAAI,YAAA;AAC3D,IAAA,OAAO,SAAS,KAAK,CAAA;AAAA,EACtB;AACA,EAAA,OAAO,QAAA;AACR;;;ACtBO,SAAS,gBAAgB,MAAA,EAAmB;AAClD,EAAA,KAAA,MAAW,SAAS,MAAA,EAAQ;AAC3B,IAAA,IAAI,KAAA,KAAU,QAAW,OAAO,KAAA;AAAA,EACjC;AACA,EAAA,OAAO,MAAA;AACR;;;ACGO,IAAM,QAAA,GAAW,CACvB,KAAA,KACqB,KAAA,KAAU,QAAQ,KAAA,KAAU;AAgB3C,IAAM,aAAA,GAAgB,CAAC,IAAA,KAC7B,IAAA,YAAgB;AA4BV,IAAM,YAAY,CACxB,IAAA,EACA,KAAA,KACe,KAAA,CAAM,SAAS,IAAS;AAkBjC,IAAM,QAAA,GAAW,CACvB,MAAA,KACuC;AACvC,EAAA,IAAI;AACH,IAAA,OACC,OAAO,WAAW,QAAA,IAAY,MAAA,KAAW,QAAQ,CAAC,KAAA,CAAM,QAAQ,MAAM,CAAA;AAAA,EAExE,CAAA,CAAA,MAAQ;AACP,IAAA,MAAM,IAAI,MAAM,yBAAyB,CAAA;AAAA,EAC1C;AACD;AAkBO,IAAM,QAAA,GAAW,CAAI,KAAA,KAC3B,CAAC,CAAC;AAWI,SAAS,SAAA,CAEf,WACA,OAAA,EACoB;AACpB,EAAA,IAAI,SAAA,EAAW;AACf,EAAA,IAAI,OAAO,OAAA,KAAY,QAAA,EAAU,MAAM,IAAI,MAAM,kBAAkB,CAAA;AACnE,EAAA,MAAM,IAAI,MAAM,OAAO,CAAA;AACxB;AASO,IAAM,cAAA,GAAiB,CAC7B,MAAA,EAEA,IAAA,KACqB;AACrB,EAAA,IAAI,OAAO,MAAA,CAAO,MAAA,KAAW,UAAA,EAAY;AACxC,IAAA,OAAO,MAAA,CAAO,MAAA,CAAO,MAAA,EAAQ,IAAI,CAAA;AAAA,EAClC;AACA,EAAA,OAAO,MAAA,CAAO,SAAA,CAAU,cAAA,CAAe,IAAA,CAAK,QAAQ,IAAI,CAAA;AACzD","file":"chunk-OS2T6P2T.js","sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport type { BivariantCallback, SetStateAction } from \"src/types\";\n\nfunction isUpdater<T>(\n\targument: SetStateAction<T>,\n): argument is BivariantCallback<(prevState: T) => T> {\n\treturn typeof argument === \"function\";\n}\n\nfunction isLazyValue<T>(value: any): value is () => T {\n\treturn typeof value === \"function\";\n}\n\n/**\n * Receives a `setState` argument and calls it with `currentValue` if it's a\n * function. Otherwise return the argument as the new value.\n * @example\n * applyState((value) => value + 1, 1); // 2\n * applyState(2, 1); // 2\n */\nexport function applyState<T>(\n\targument: SetStateAction<T>,\n\tcurrentValue: T | (() => T),\n) {\n\tif (isUpdater(argument)) {\n\t\tconst value = isLazyValue(currentValue) ? currentValue() : currentValue;\n\t\treturn argument(value);\n\t}\n\treturn argument;\n}\n","/* eslint-disable @typescript-eslint/no-explicit-any */\n/**\n * Returns the first value that is not `undefined`.\n */\nexport function defaultValue<T extends readonly any[]>(\n\t...values: T\n): DefaultValue<T>;\n\nexport function defaultValue(...values: unknown[]) {\n\tfor (const value of values) {\n\t\tif (value !== undefined) return value;\n\t}\n\treturn undefined;\n}\n\ntype DefaultValue<T extends readonly any[], Other = never> = T extends [\n\tinfer Head,\n\t...infer Rest,\n]\n\t? Rest extends []\n\t\t? T[number] | Other\n\t\t: undefined extends Head\n\t\t\t? DefaultValue<Rest, Exclude<Other | Head, undefined>>\n\t\t\t: Exclude<T[number], undefined>\n\t: never;\n","import type { AnyObject } from \"src/types\";\n\n/**\n * @function notEmpty\n * @description Checks if a value is not null or undefined.\n *\n * @param value - The value to check.\n * @returns Whether the value is not null or undefined.\n *\n * @example\n * ```ts\n * notEmpty(\"Hello\"); // true\n * notEmpty(null); // false\n * notEmpty(undefined); // false\n * ```\n */\nexport const notEmpty = <TValue>(\n\tvalue: TValue | null | undefined,\n): value is TValue => value !== null && value !== undefined;\n\n/**\n * @function isHTMLElement\n * @description Checks if a value is an instance of HTMLElement.\n *\n * @param node - The value to check.\n * @returns Whether the value is an instance of HTMLElement.\n *\n * @example\n * ```ts\n * isHTMLElement(document.querySelector('body')); // true\n * isHTMLElement({}); // false\n * ```\n */\n\nexport const isHTMLElement = (node: unknown): node is HTMLElement =>\n\tnode instanceof HTMLElement;\n\n/**\n * @function isInArray\n * @description Typesafe check if an item is in an array without being explicit about const types\n *\n * @param item - The item to check.\n * @param array - The array to check against.\n *\n * @returns Whether the item is in the array.\n *\n * @example\n * ```ts\n * // Default situation in TS:\n * const arr = ['hello', 'world'] as const;\n *\n * let someString = 'something';\n *\n * arr.includes(someString); // Argument of type 'string' is not assignable to parameter of type '\"hello\" | \"world\"'\n *\n * // Using isInArray:\n * const arr = ['hello', 'world'] as const;\n *\n * let someString = 'something';\n *\n * isInArray(someString, arr); // false, but no type error\n * ```\n */\nexport const isInArray = <T, A extends T>(\n\titem: T,\n\tarray: ReadonlyArray<A>,\n): item is A => array.includes(item as A);\n\n/**\n * @function isObject\n * @description Checks if a value is an object (excluding arrays).\n *\n * @param object - The value to check.\n * @returns Whether the value is truthy.\n *\n * @example\n * ```ts\n * isObject({ hello: world }); // true\n * isObject({}); // true\n * isObject([]); // false\n * isObject(\"\"); // false\n * isObject(null); // false\n * ```\n */\nexport const isObject = (\n\tobject: unknown,\n): object is Record<string, unknown> => {\n\ttry {\n\t\treturn (\n\t\t\ttypeof object === \"object\" && object !== null && !Array.isArray(object)\n\t\t);\n\t} catch {\n\t\tthrow new Error(\"isObject: Invalid input\");\n\t}\n};\n\n/**\n * @function isTruthy\n * @description Checks if a value is truthy.\n *\n * @param value - The value to check.\n * @returns Whether the value is truthy.\n *\n * @example\n * ```ts\n * isTruthy(\"foo\"); // true\n * isTruthy(1); // true\n * isTruthy(0); // false\n * isTruthy(\"\"); // false\n * ```\n *\n */\nexport const isTruthy = <T>(value?: T | undefined | null | false): value is T =>\n\t!!value;\n\n/**\n * Asserts that a condition is true, otherwise throws an error.\n * @example\n * invariant(\n * condition,\n * process.env.NODE_ENV !== \"production\" && \"Invariant failed\"\n * );\n */\n// assert functions need to be an explicit function(){}\nexport function invariant(\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tcondition: any,\n\tmessage?: string | boolean,\n): asserts condition {\n\tif (condition) return;\n\tif (typeof message !== \"string\") throw new Error(\"Invariant failed\");\n\tthrow new Error(message);\n}\n\n/**\n * Checks if an object has a property as its own (not inherited) property.\n * @example\n * hasOwnProperty({ a: \"a\" }, \"a\"); // true\n * hasOwnProperty({ a: \"a\" }, \"b\"); // false\n * hasOwnProperty(Object.create({ a: \"a\" }), \"a\"); // false\n */\nexport const hasOwnProperty = <T extends AnyObject>(\n\tobject: T,\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tprop: keyof any,\n): prop is keyof T => {\n\tif (typeof Object.hasOwn === \"function\") {\n\t\treturn Object.hasOwn(object, prop);\n\t}\n\treturn Object.prototype.hasOwnProperty.call(object, prop);\n};\n"]}
@@ -0,0 +1,51 @@
1
+ // src/consent/event-consent-source.ts
2
+ var safely = (read) => {
3
+ try {
4
+ return read();
5
+ } catch (error) {
6
+ console.error("[consent] read", error);
7
+ return null;
8
+ }
9
+ };
10
+ var eventConsentSource = ({
11
+ name,
12
+ read,
13
+ events,
14
+ target = () => window
15
+ }) => {
16
+ const get = () => typeof window === "undefined" ? null : safely(read);
17
+ const listeners = /* @__PURE__ */ new Set();
18
+ const onChange = () => {
19
+ const next = get();
20
+ if (!next) return;
21
+ listeners.forEach((listener) => {
22
+ try {
23
+ listener(next);
24
+ } catch (error) {
25
+ console.error("[consent] subscribe", error);
26
+ }
27
+ });
28
+ };
29
+ return {
30
+ name,
31
+ get,
32
+ subscribe: (listener) => {
33
+ if (listeners.size === 0 && typeof window !== "undefined") {
34
+ events.forEach((event) => target().addEventListener(event, onChange));
35
+ }
36
+ listeners.add(listener);
37
+ return () => {
38
+ listeners.delete(listener);
39
+ if (listeners.size === 0) {
40
+ events.forEach(
41
+ (event) => target().removeEventListener(event, onChange)
42
+ );
43
+ }
44
+ };
45
+ }
46
+ };
47
+ };
48
+
49
+ export { eventConsentSource };
50
+ //# sourceMappingURL=chunk-QIJGG2F4.js.map
51
+ //# sourceMappingURL=chunk-QIJGG2F4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/consent/event-consent-source.ts"],"names":[],"mappings":";AAEA,IAAM,MAAA,GAAS,CAAC,IAAA,KAA+B;AAC9C,EAAA,IAAI;AACH,IAAA,OAAO,IAAA,EAAK;AAAA,EACb,SAAS,KAAA,EAAO;AACf,IAAA,OAAA,CAAQ,KAAA,CAAM,kBAAkB,KAAK,CAAA;AACrC,IAAA,OAAO,IAAA;AAAA,EACR;AACD,CAAA;AAaO,IAAM,qBAAqB,CAAC;AAAA,EAClC,IAAA;AAAA,EACA,IAAA;AAAA,EACA,MAAA;AAAA,EACA,SAAS,MAAM;AAChB,CAAA,KAAgD;AAC/C,EAAA,MAAM,MAAM,MAAO,OAAO,WAAW,WAAA,GAAc,IAAA,GAAO,OAAO,IAAI,CAAA;AACrE,EAAA,MAAM,SAAA,uBAAgB,GAAA,EAAgC;AACtD,EAAA,MAAM,WAAW,MAAM;AACtB,IAAA,MAAM,OAAO,GAAA,EAAI;AACjB,IAAA,IAAI,CAAC,IAAA,EAAM;AACX,IAAA,SAAA,CAAU,OAAA,CAAQ,CAAC,QAAA,KAAa;AAC/B,MAAA,IAAI;AACH,QAAA,QAAA,CAAS,IAAI,CAAA;AAAA,MACd,SAAS,KAAA,EAAO;AACf,QAAA,OAAA,CAAQ,KAAA,CAAM,uBAAuB,KAAK,CAAA;AAAA,MAC3C;AAAA,IACD,CAAC,CAAA;AAAA,EACF,CAAA;AAEA,EAAA,OAAO;AAAA,IACN,IAAA;AAAA,IACA,GAAA;AAAA,IACA,SAAA,EAAW,CAAC,QAAA,KAAa;AACxB,MAAA,IAAI,SAAA,CAAU,IAAA,KAAS,CAAA,IAAK,OAAO,WAAW,WAAA,EAAa;AAC1D,QAAA,MAAA,CAAO,OAAA,CAAQ,CAAC,KAAA,KAAU,MAAA,GAAS,gBAAA,CAAiB,KAAA,EAAO,QAAQ,CAAC,CAAA;AAAA,MACrE;AACA,MAAA,SAAA,CAAU,IAAI,QAAQ,CAAA;AAEtB,MAAA,OAAO,MAAM;AACZ,QAAA,SAAA,CAAU,OAAO,QAAQ,CAAA;AACzB,QAAA,IAAI,SAAA,CAAU,SAAS,CAAA,EAAG;AACzB,UAAA,MAAA,CAAO,OAAA;AAAA,YAAQ,CAAC,KAAA,KACf,MAAA,EAAO,CAAE,mBAAA,CAAoB,OAAO,QAAQ;AAAA,WAC7C;AAAA,QACD;AAAA,MACD,CAAA;AAAA,IACD;AAAA,GACD;AACD","file":"chunk-QIJGG2F4.js","sourcesContent":["import type { Consent, ConsentSource } from \"./consent\";\n\nconst safely = (read: () => Consent | null) => {\n\ttry {\n\t\treturn read();\n\t} catch (error) {\n\t\tconsole.error(\"[consent] read\", error);\n\t\treturn null;\n\t}\n};\n\ntype EventConsentSourceOptions = {\n\tname: string;\n\t/** Reads the platform's global. `null` until answered. */\n\tread: () => Consent | null;\n\t/** Events after which `read` may return something new. */\n\tevents: readonly string[];\n\t/** Where the events fire. Default `window`. */\n\ttarget?: () => EventTarget;\n};\n\n/** A `ConsentSource` for a consent platform that keeps consent on a global and announces changes with events. */\nexport const eventConsentSource = ({\n\tname,\n\tread,\n\tevents,\n\ttarget = () => window,\n}: EventConsentSourceOptions): ConsentSource => {\n\tconst get = () => (typeof window === \"undefined\" ? null : safely(read));\n\tconst listeners = new Set<(consent: Consent) => void>();\n\tconst onChange = () => {\n\t\tconst next = get();\n\t\tif (!next) return;\n\t\tlisteners.forEach((listener) => {\n\t\t\ttry {\n\t\t\t\tlistener(next);\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\"[consent] subscribe\", error);\n\t\t\t}\n\t\t});\n\t};\n\n\treturn {\n\t\tname,\n\t\tget,\n\t\tsubscribe: (listener) => {\n\t\t\tif (listeners.size === 0 && typeof window !== \"undefined\") {\n\t\t\t\tevents.forEach((event) => target().addEventListener(event, onChange));\n\t\t\t}\n\t\t\tlisteners.add(listener);\n\n\t\t\treturn () => {\n\t\t\t\tlisteners.delete(listener);\n\t\t\t\tif (listeners.size === 0) {\n\t\t\t\t\tevents.forEach((event) =>\n\t\t\t\t\t\ttarget().removeEventListener(event, onChange),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t};\n\t\t},\n\t};\n};\n"]}
@@ -0,0 +1,16 @@
1
+ import { a as ConsentSource } from '../../index-Zef1nnTP.js';
2
+
3
+ /**
4
+ * Consent from Cookiebot. Answered once the visitor responds, or straight away where Cookiebot implies consent.
5
+ * `statistics` maps to `analytics`.
6
+ *
7
+ * @see https://www.cookiebot.com/en/developer/
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * export const analytics = createAnalytics({ consent: cookiebotConsent(), destinations: [gtmDestination()] });
12
+ * ```
13
+ */
14
+ declare const cookiebotConsent: () => ConsentSource;
15
+
16
+ export { cookiebotConsent };
@@ -0,0 +1,27 @@
1
+ import { eventConsentSource } from '../../chunk-QIJGG2F4.js';
2
+
3
+ // src/consent/cookiebot/cookiebot.ts
4
+ var cookiebotConsent = () => eventConsentSource({
5
+ name: "cookiebot",
6
+ read: () => {
7
+ const { Cookiebot } = window;
8
+ const consent = Cookiebot?.consent;
9
+ if (!consent || !Cookiebot.hasResponse && consent.method == null) {
10
+ return null;
11
+ }
12
+ return {
13
+ preferences: consent.preferences,
14
+ analytics: consent.statistics,
15
+ marketing: consent.marketing
16
+ };
17
+ },
18
+ events: [
19
+ "CookiebotOnConsentReady",
20
+ "CookiebotOnAccept",
21
+ "CookiebotOnDecline"
22
+ ]
23
+ });
24
+
25
+ export { cookiebotConsent };
26
+ //# sourceMappingURL=index.js.map
27
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/consent/cookiebot/cookiebot.ts"],"names":[],"mappings":";;;AAyBO,IAAM,gBAAA,GAAmB,MAC/B,kBAAA,CAAmB;AAAA,EAClB,IAAA,EAAM,WAAA;AAAA,EACN,MAAM,MAAM;AACX,IAAA,MAAM,EAAE,WAAU,GAAI,MAAA;AACtB,IAAA,MAAM,UAAU,SAAA,EAAW,OAAA;AAC3B,IAAA,IAAI,CAAC,OAAA,IAAY,CAAC,UAAU,WAAA,IAAe,OAAA,CAAQ,UAAU,IAAA,EAAO;AACnE,MAAA,OAAO,IAAA;AAAA,IACR;AACA,IAAA,OAAO;AAAA,MACN,aAAa,OAAA,CAAQ,WAAA;AAAA,MACrB,WAAW,OAAA,CAAQ,UAAA;AAAA,MACnB,WAAW,OAAA,CAAQ;AAAA,KACpB;AAAA,EACD,CAAA;AAAA,EACA,MAAA,EAAQ;AAAA,IACP,yBAAA;AAAA,IACA,mBAAA;AAAA,IACA;AAAA;AAEF,CAAC","file":"index.js","sourcesContent":["import type { ConsentSource } from \"../consent\";\nimport { eventConsentSource } from \"../event-consent-source\";\n\ntype CookiebotGlobal = {\n\thasResponse?: boolean;\n\tconsent?: {\n\t\tpreferences: boolean;\n\t\tstatistics: boolean;\n\t\tmarketing: boolean;\n\t\t/** `\"implied\"` where consent isn't required, `\"explicit\"` after the visitor answered, `null` before. */\n\t\tmethod?: string | null;\n\t};\n};\n\n/**\n * Consent from Cookiebot. Answered once the visitor responds, or straight away where Cookiebot implies consent.\n * `statistics` maps to `analytics`.\n *\n * @see https://www.cookiebot.com/en/developer/\n *\n * @example\n * ```ts\n * export const analytics = createAnalytics({ consent: cookiebotConsent(), destinations: [gtmDestination()] });\n * ```\n */\nexport const cookiebotConsent = (): ConsentSource =>\n\teventConsentSource({\n\t\tname: \"cookiebot\",\n\t\tread: () => {\n\t\t\tconst { Cookiebot } = window as { Cookiebot?: CookiebotGlobal };\n\t\t\tconst consent = Cookiebot?.consent;\n\t\t\tif (!consent || (!Cookiebot.hasResponse && consent.method == null)) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tpreferences: consent.preferences,\n\t\t\t\tanalytics: consent.statistics,\n\t\t\t\tmarketing: consent.marketing,\n\t\t\t};\n\t\t},\n\t\tevents: [\n\t\t\t\"CookiebotOnConsentReady\",\n\t\t\t\"CookiebotOnAccept\",\n\t\t\t\"CookiebotOnDecline\",\n\t\t],\n\t});\n"]}
@@ -0,0 +1,23 @@
1
+ import { b as ConsentTarget } from '../../index-Zef1nnTP.js';
2
+
3
+ type GoogleConsentModeOptions = {
4
+ /** Default `"dataLayer"`. */
5
+ dataLayerName?: string;
6
+ /** Milliseconds tags wait for the first answer before firing under the denied defaults. Default 500. */
7
+ waitForUpdate?: number;
8
+ };
9
+ /**
10
+ * Sends Google Consent Mode v2 signals through the dataLayer: every category denied when created, then an update for
11
+ * every answer. Only for consent platforms without a Consent Mode integration, like Shopify's. Create it before GTM or
12
+ * gtag.js loads, so the defaults come first.
13
+ *
14
+ * @see https://developers.google.com/tag-platform/security/guides/consent
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * export const analytics = createAnalytics({ consent: shopifyConsent(), consentTargets: [googleConsentMode()] });
19
+ * ```
20
+ */
21
+ declare const googleConsentMode: ({ dataLayerName, waitForUpdate, }?: GoogleConsentModeOptions) => ConsentTarget;
22
+
23
+ export { type GoogleConsentModeOptions, googleConsentMode };