@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.
- package/README.md +24 -1
- package/dist/analytics/google/index.d.ts +158 -0
- package/dist/analytics/google/index.js +274 -0
- package/dist/analytics/google/index.js.map +1 -0
- package/dist/analytics/index.d.ts +101 -0
- package/dist/analytics/index.js +384 -0
- package/dist/analytics/index.js.map +1 -0
- package/dist/analytics/shopify/index.d.ts +186 -0
- package/dist/analytics/shopify/index.js +175 -0
- package/dist/analytics/shopify/index.js.map +1 -0
- package/dist/{chunk-IAAQVL6M.js → chunk-FJ7SNQQI.js} +24 -5
- package/dist/chunk-FJ7SNQQI.js.map +1 -0
- package/dist/chunk-G2BKC4BU.js +8 -0
- package/dist/chunk-G2BKC4BU.js.map +1 -0
- package/dist/chunk-GQVN2ECX.js +14 -0
- package/dist/chunk-GQVN2ECX.js.map +1 -0
- package/dist/chunk-LL3VTFNY.js +22 -0
- package/dist/chunk-LL3VTFNY.js.map +1 -0
- package/dist/chunk-M3HNM5HC.js +48 -0
- package/dist/chunk-M3HNM5HC.js.map +1 -0
- package/dist/chunk-MHFGMH3G.js +11 -0
- package/dist/chunk-MHFGMH3G.js.map +1 -0
- package/dist/chunk-OS2T6P2T.js +50 -0
- package/dist/chunk-OS2T6P2T.js.map +1 -0
- package/dist/chunk-QIJGG2F4.js +51 -0
- package/dist/chunk-QIJGG2F4.js.map +1 -0
- package/dist/consent/cookiebot/index.d.ts +16 -0
- package/dist/consent/cookiebot/index.js +27 -0
- package/dist/consent/cookiebot/index.js.map +1 -0
- package/dist/consent/google/index.d.ts +23 -0
- package/dist/consent/google/index.js +48 -0
- package/dist/consent/google/index.js.map +1 -0
- package/dist/consent/index.d.ts +1 -0
- package/dist/consent/index.js +3 -0
- package/dist/consent/index.js.map +1 -0
- package/dist/consent/onetrust/index.d.ts +23 -0
- package/dist/consent/onetrust/index.js +39 -0
- package/dist/consent/onetrust/index.js.map +1 -0
- package/dist/consent/shopify/index.d.ts +44 -0
- package/dist/consent/shopify/index.js +84 -0
- package/dist/consent/shopify/index.js.map +1 -0
- package/dist/cookies/index.d.ts +230 -0
- package/dist/cookies/index.js +188 -0
- package/dist/cookies/index.js.map +1 -0
- package/dist/define-events-CxZjoUlG.d.ts +255 -0
- package/dist/destination-5KJ5PeR3.d.ts +122 -0
- package/dist/index-Zef1nnTP.d.ts +36 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +64 -1
- package/dist/index.js.map +1 -1
- package/dist/listeners-DkU-vBPa.d.ts +15 -0
- package/dist/navigation/index.d.ts +1 -1
- package/dist/navigation/index.js +1 -1
- package/dist/schema/index.d.ts +16 -9
- package/dist/schema/index.js +1 -1
- package/dist/schema-CfQvhbuU.d.ts +87 -0
- package/dist/store/index.js +1 -1
- package/dist/tracking/cart/index.d.ts +74 -0
- package/dist/tracking/cart/index.js +118 -0
- package/dist/tracking/cart/index.js.map +1 -0
- package/dist/tracking/dom/index.d.ts +320 -0
- package/dist/tracking/dom/index.js +1196 -0
- package/dist/tracking/dom/index.js.map +1 -0
- package/docs/analytics.md +883 -0
- package/docs/cookies.md +354 -0
- package/docs/decisions/001-analytics-project-events.md +31 -0
- package/docs/decisions/002-tracking-dom-opt-in.md +29 -0
- package/docs/decisions/003-tracking-element-data.md +27 -0
- package/docs/decisions/004-cookies-explicit-store.md +26 -0
- package/docs/decisions/005-cookies-invalid-reads-fall-back.md +27 -0
- package/docs/decisions/006-cookies-server-only-types.md +19 -0
- package/docs/decisions/007-cookies-options-on-definition.md +25 -0
- package/docs/decisions/008-cookies-in-vertical-slices.md +24 -0
- package/docs/decisions/009-analytics-consent-targets.md +29 -0
- package/docs/decisions/010-analytics-destination-contract.md +29 -0
- package/docs/decisions/011-tracking-decoupled.md +30 -0
- package/docs/tracking.md +221 -0
- package/package.json +84 -3
- package/dist/chunk-IAAQVL6M.js.map +0 -1
- package/dist/chunk-SIU7SKJK.js +0 -139
- package/dist/chunk-SIU7SKJK.js.map +0 -1
- package/dist/schema-iy92gbTC.d.ts +0 -36
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { S as SetStateAction, A as AnyObject } from './state-BnfG-Qk9.js';
|
|
2
2
|
export { a as AnyFunction, B as BivariantCallback, E as EmptyObject, L as LooseObject, b as SetState } from './state-BnfG-Qk9.js';
|
|
3
|
-
import
|
|
3
|
+
import { ClassValue } from 'clsx';
|
|
4
4
|
import { ImageProps } from 'next/image';
|
|
5
5
|
|
|
6
6
|
declare const IS_DEVELOPMENT: boolean;
|
|
@@ -147,7 +147,7 @@ declare const transform: (value: number, originRange: [number, number], targetRa
|
|
|
147
147
|
*/
|
|
148
148
|
declare const round: (value: number, decimalPlaces?: number) => number;
|
|
149
149
|
|
|
150
|
-
declare const mergeClassNames: (...classNames:
|
|
150
|
+
declare const mergeClassNames: (...classNames: ClassValue[]) => string;
|
|
151
151
|
|
|
152
152
|
/**
|
|
153
153
|
* Detects if the device has touch capabilities.
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,66 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { shallowEqual } from './chunk-LL3VTFNY.js';
|
|
2
|
+
export { applyState, defaultValue, hasOwnProperty, invariant, isHTMLElement, isInArray, isObject, isTruthy, notEmpty } from './chunk-OS2T6P2T.js';
|
|
3
|
+
import { IS_IN_BROWSER } from './chunk-GQVN2ECX.js';
|
|
4
|
+
export { IS_BUILD, IS_DEVELOPMENT, IS_IN_BROWSER } from './chunk-GQVN2ECX.js';
|
|
5
|
+
import clsx from 'clsx';
|
|
6
|
+
|
|
7
|
+
// src/functions/identity.ts
|
|
8
|
+
function identity(value) {
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// src/functions/math.ts
|
|
13
|
+
var wrap = (value, [min, max]) => {
|
|
14
|
+
const range = max - min + 1;
|
|
15
|
+
return ((value - min) % range + range) % range + min;
|
|
16
|
+
};
|
|
17
|
+
var clamp = (value, [min, max]) => {
|
|
18
|
+
return Math.min(Math.max(value, min), max);
|
|
19
|
+
};
|
|
20
|
+
var transform = (value, originRange, targetRange) => {
|
|
21
|
+
const [oMin, oMax] = originRange;
|
|
22
|
+
const [tMin, tMax] = targetRange;
|
|
23
|
+
const ratio = (value - oMin) / (oMax - oMin);
|
|
24
|
+
const transformedValue = tMin + ratio * (tMax - tMin);
|
|
25
|
+
return transformedValue;
|
|
26
|
+
};
|
|
27
|
+
var round = (value, decimalPlaces = 0) => {
|
|
28
|
+
const factor = Math.pow(10, decimalPlaces);
|
|
29
|
+
return Math.round((value + Number.EPSILON) * factor) / factor;
|
|
30
|
+
};
|
|
31
|
+
var mergeClassNames = (...classNames) => {
|
|
32
|
+
try {
|
|
33
|
+
return clsx(...classNames);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
"mergeClassNames: Invalid input provided. Ensure all class names are strings, undefined, or null."
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// src/functions/platform.ts
|
|
42
|
+
var isTouchDevice = () => {
|
|
43
|
+
return IS_IN_BROWSER && !!navigator.maxTouchPoints;
|
|
44
|
+
};
|
|
45
|
+
var isApple = () => {
|
|
46
|
+
if (!IS_IN_BROWSER) return false;
|
|
47
|
+
return /mac|iphone|ipad|ipod/i.test(navigator.platform);
|
|
48
|
+
};
|
|
49
|
+
var isMac = () => {
|
|
50
|
+
return IS_IN_BROWSER && navigator.platform.startsWith("Mac") && !isTouchDevice();
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// src/functions/remove-undefined-values.ts
|
|
54
|
+
function removeUndefinedValues(obj) {
|
|
55
|
+
const result = {};
|
|
56
|
+
for (const key in obj) {
|
|
57
|
+
if (obj[key] !== void 0) {
|
|
58
|
+
result[key] = obj[key];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { clamp, identity, isApple, isMac, isTouchDevice, mergeClassNames, removeUndefinedValues, round, transform, wrap };
|
|
2
65
|
//# sourceMappingURL=index.js.map
|
|
3
66
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
|
|
1
|
+
{"version":3,"sources":["../src/functions/identity.ts","../src/functions/math.ts","../src/functions/merge-class-names.ts","../src/functions/platform.ts","../src/functions/remove-undefined-values.ts"],"names":[],"mappings":";;;;;;;AAGO,SAAS,SAAY,KAAA,EAAU;AACrC,EAAA,OAAO,KAAA;AACR;;;ACQO,IAAM,OAAO,CAAC,KAAA,EAAe,CAAC,GAAA,EAAK,GAAG,CAAA,KAAkC;AAC9E,EAAA,MAAM,KAAA,GAAQ,MAAM,GAAA,GAAM,CAAA;AAC1B,EAAA,OAAA,CAAA,CAAW,KAAA,GAAQ,GAAA,IAAO,KAAA,GAAS,KAAA,IAAS,KAAA,GAAS,GAAA;AACtD;AAgBO,IAAM,QAAQ,CACpB,KAAA,EACA,CAAC,GAAA,EAAK,GAAG,CAAA,KACL;AACJ,EAAA,OAAO,KAAK,GAAA,CAAI,IAAA,CAAK,IAAI,KAAA,EAAO,GAAG,GAAG,GAAG,CAAA;AAC1C;AAcO,IAAM,SAAA,GAAY,CACxB,KAAA,EACA,WAAA,EACA,WAAA,KACI;AACJ,EAAA,MAAM,CAAC,IAAA,EAAM,IAAI,CAAA,GAAI,WAAA;AACrB,EAAA,MAAM,CAAC,IAAA,EAAM,IAAI,CAAA,GAAI,WAAA;AAErB,EAAA,MAAM,KAAA,GAAA,CAAS,KAAA,GAAQ,IAAA,KAAS,IAAA,GAAO,IAAA,CAAA;AACvC,EAAA,MAAM,gBAAA,GAAmB,IAAA,GAAO,KAAA,IAAS,IAAA,GAAO,IAAA,CAAA;AAEhD,EAAA,OAAO,gBAAA;AACR;AAQO,IAAM,KAAA,GAAQ,CAAC,KAAA,EAAe,aAAA,GAAwB,CAAA,KAAM;AAClE,EAAA,MAAM,MAAA,GAAS,IAAA,CAAK,GAAA,CAAI,EAAA,EAAI,aAAa,CAAA;AACzC,EAAA,OAAO,KAAK,KAAA,CAAA,CAAO,KAAA,GAAQ,MAAA,CAAO,OAAA,IAAW,MAAM,CAAA,GAAI,MAAA;AACxD;ACxEA,IAAM,eAAA,GAAkB,IAAI,UAAA,KAA6B;AACxD,EAAA,IAAI;AACH,IAAA,OAAO,IAAA,CAAK,GAAG,UAAU,CAAA;AAAA,EAC1B,SAAS,KAAA,EAAO;AACf,IAAA,MAAM,IAAI,KAAA;AAAA,MACT;AAAA,KACD;AAAA,EACD;AACD;;;ACLO,IAAM,gBAAgB,MAAM;AAClC,EAAA,OAAO,aAAA,IAAiB,CAAC,CAAC,SAAA,CAAU,cAAA;AACrC;AAKO,IAAM,UAAU,MAAM;AAC5B,EAAA,IAAI,CAAC,eAAe,OAAO,KAAA;AAC3B,EAAA,OAAO,uBAAA,CAAwB,IAAA,CAAK,SAAA,CAAU,QAAQ,CAAA;AACvD;AAKO,IAAM,QAAQ,MAAM;AAC1B,EAAA,OACC,iBAAiB,SAAA,CAAU,QAAA,CAAS,WAAW,KAAK,CAAA,IAAK,CAAC,aAAA,EAAc;AAE1E;;;ACnBO,SAAS,sBAA2C,GAAA,EAAQ;AAClE,EAAA,MAAM,SAAS,EAAC;AAChB,EAAA,KAAA,MAAW,OAAO,GAAA,EAAK;AACtB,IAAA,IAAI,GAAA,CAAI,GAAG,CAAA,KAAM,MAAA,EAAW;AAC3B,MAAA,MAAA,CAAO,GAAG,CAAA,GAAI,GAAA,CAAI,GAAG,CAAA;AAAA,IACtB;AAAA,EACD;AACA,EAAA,OAAO,MAAA;AACR","file":"index.js","sourcesContent":["/**\n * Returns the same argument.\n */\nexport function identity<T>(value: T) {\n\treturn value;\n}\n","/**\n * Wraps a number within a specified range.\n * @param value - The number to wrap.\n * @param min - The minimum value of the range (inclusive).\n * @param max - The maximum value of the range (inclusive).\n * @returns The wrapped number within the range [min, max].\n *\n * @example\n * wrap(5, [0, 3]); // Returns 1\n * wrap(-1, [0, 3]); // Returns 2\n * wrap(-3, [0, 3]); // Returns 1\n * wrap(7, [0, 3]); // Returns 3\n */\nexport const wrap = (value: number, [min, max]: [min: number, max: number]) => {\n\tconst range = max - min + 1;\n\treturn ((((value - min) % range) + range) % range) + min;\n};\n\n/**\n * Limits a value between a specified range.\n * @param value - The number to limit.\n * @param min - The minimum value of the range (inclusive).\n * @param max - The maximum value of the range (inclusive).\n * @returns The wrapped number within the range [min, max].\n *\n * @example\n * clamp(2, [0, 3]); // Returns 2\n * clamp(5, [0, 3]); // Returns 3\n * clamp(-1, [0, 3]); // Returns 0\n * clamp(-3, [0, 3]); // Returns 0\n * clamp(7, [0, 3]); // Returns 3\n */\nexport const clamp = (\n\tvalue: number,\n\t[min, max]: [min: number, max: number],\n) => {\n\treturn Math.min(Math.max(value, min), max);\n};\n\n/**\n * Maps a number from one range to another.\n * @param value The number to map.\n * @param originRange The original range of the number.\n * @param targetRange The target range to map the number to.\n * @returns The mapped number within the target range.\n *\n * @example\n * transform(5, [0, 10], [0, 100]); // Returns 50\n * transform(0.5, [0, 1], [0, 10]); // Returns 5\n * transform(-5, [-10, 0], [0, 100]); // Returns 50\n */\nexport const transform = (\n\tvalue: number,\n\toriginRange: [number, number],\n\ttargetRange: [number, number],\n) => {\n\tconst [oMin, oMax] = originRange;\n\tconst [tMin, tMax] = targetRange;\n\n\tconst ratio = (value - oMin) / (oMax - oMin);\n\tconst transformedValue = tMin + ratio * (tMax - tMin);\n\n\treturn transformedValue;\n};\n\n/**\n * Rounds a number to a specified number of decimal places.\n * @param value - The number to round.\n * @param decimalPlaces - The number of decimal places to round to. (default is 0)\n * @returns The rounded number.\n */\nexport const round = (value: number, decimalPlaces: number = 0) => {\n\tconst factor = Math.pow(10, decimalPlaces);\n\treturn Math.round((value + Number.EPSILON) * factor) / factor;\n};\n","import clsx, { type ClassValue } from \"clsx\";\n\nconst mergeClassNames = (...classNames: ClassValue[]) => {\n\ttry {\n\t\treturn clsx(...classNames);\n\t} catch (error) {\n\t\tthrow new Error(\n\t\t\t\"mergeClassNames: Invalid input provided. Ensure all class names are strings, undefined, or null.\",\n\t\t);\n\t}\n};\n\n// Name isn't very memorable, so we export it in this file to alias it\nexport { mergeClassNames };\n","import { IS_IN_BROWSER } from \"src/config\";\n\n/**\n * Detects if the device has touch capabilities.\n */\nexport const isTouchDevice = () => {\n\treturn IS_IN_BROWSER && !!navigator.maxTouchPoints;\n};\n\n/**\n * Detects Apple device.\n */\nexport const isApple = () => {\n\tif (!IS_IN_BROWSER) return false;\n\treturn /mac|iphone|ipad|ipod/i.test(navigator.platform);\n};\n\n/**\n * Detects Mac computer.\n */\nexport const isMac = () => {\n\treturn (\n\t\tIS_IN_BROWSER && navigator.platform.startsWith(\"Mac\") && !isTouchDevice()\n\t);\n};\n","import type { AnyObject } from \"../types\";\n\n/**\n * Removes undefined values from an object.\n */\nexport function removeUndefinedValues<T extends AnyObject>(obj: T) {\n\tconst result = {} as T;\n\tfor (const key in obj) {\n\t\tif (obj[key] !== undefined) {\n\t\t\tresult[key] = obj[key];\n\t\t}\n\t}\n\treturn result;\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type Named = {
|
|
2
|
+
name: string;
|
|
3
|
+
};
|
|
4
|
+
/**
|
|
5
|
+
* Subscribes `listener` to every event, or only to those named. A listener that throws is logged, so it doesn't break
|
|
6
|
+
* the others. Returns a function that unsubscribes. Does nothing on the server.
|
|
7
|
+
*/
|
|
8
|
+
type Subscribe<Event extends Named> = {
|
|
9
|
+
(listener: (event: Event) => void): () => void;
|
|
10
|
+
<Name extends Event["name"]>(names: Name | readonly Name[], listener: (event: Extract<Event, {
|
|
11
|
+
name: Name;
|
|
12
|
+
}>) => void): () => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type { Subscribe as S };
|
package/dist/navigation/index.js
CHANGED
package/dist/schema/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { S as Schema } from '../schema-
|
|
2
|
-
export { b as SchemaSerialized, a as SchemaState,
|
|
1
|
+
import { S as Schema } from '../schema-CfQvhbuU.js';
|
|
2
|
+
export { c as SchemaSerializableState, b as SchemaSerialized, a as SchemaState, d as createSchema } from '../schema-CfQvhbuU.js';
|
|
3
3
|
|
|
4
|
-
type StringSchema<State = never> = {
|
|
5
|
-
parse: Schema<State, string>["parse"];
|
|
6
|
-
stringify: Schema<State, string>["serialize"];
|
|
7
|
-
parseStringify: Schema<State, string>["parserialize"];
|
|
4
|
+
type StringSchema<State = never, SerializableState = State> = {
|
|
5
|
+
parse: Schema<State, string, SerializableState>["parse"];
|
|
6
|
+
stringify: Schema<State, string, SerializableState>["serialize"];
|
|
7
|
+
parseStringify: Schema<State, string, SerializableState>["parserialize"];
|
|
8
8
|
};
|
|
9
9
|
/**
|
|
10
10
|
* Helper function to create a `StringSchema` which converts between strings and state objects.
|
|
@@ -13,7 +13,7 @@ type StringSchema<State = never> = {
|
|
|
13
13
|
* - `parse`: A function that takes a string and returns the corresponding state object.
|
|
14
14
|
* - `stringify`: A function that takes a state object and returns a string that represents the given state.
|
|
15
15
|
*/
|
|
16
|
-
declare const createStringSchema: <
|
|
16
|
+
declare const createStringSchema: <State = never, SerializableState = State>(template: Omit<StringSchema<State, SerializableState>, "parseStringify">) => StringSchema<State, SerializableState>;
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Utility schema for parsing and stringifying href strings into structured state objects that include the pathname and URL search parameters.
|
|
@@ -30,7 +30,7 @@ declare const createStringSchema: <S = never>(template: Omit<StringSchema<S>, "p
|
|
|
30
30
|
* parse: (href) => {
|
|
31
31
|
* const { searchParams } = hrefSchema.parse(href);
|
|
32
32
|
* return {
|
|
33
|
-
* page: parseInt(searchParams.get("page")
|
|
33
|
+
* page: parseInt(searchParams.get("page") || "1", 10),
|
|
34
34
|
* _href: href,
|
|
35
35
|
* }
|
|
36
36
|
* },
|
|
@@ -40,14 +40,21 @@ declare const createStringSchema: <S = never>(template: Omit<StringSchema<S>, "p
|
|
|
40
40
|
* if (state.page > 1) {
|
|
41
41
|
* searchParams.set("page", state.page.toString());
|
|
42
42
|
* }
|
|
43
|
-
*
|
|
43
|
+
*
|
|
44
|
+
* // note that `searchParams` are optional for serialization
|
|
45
|
+
* return hrefSchema.stringify({ pathname, searchParams });
|
|
44
46
|
* }
|
|
45
47
|
* });
|
|
46
48
|
* ```
|
|
47
49
|
*/
|
|
48
50
|
declare const hrefSchema: StringSchema<{
|
|
49
51
|
pathname: string;
|
|
52
|
+
hash: string;
|
|
50
53
|
searchParams: URLSearchParams;
|
|
54
|
+
}, {
|
|
55
|
+
pathname: string;
|
|
56
|
+
hash?: string;
|
|
57
|
+
searchParams?: URLSearchParams;
|
|
51
58
|
}>;
|
|
52
59
|
|
|
53
60
|
export { Schema, type StringSchema, createStringSchema, hrefSchema };
|
package/dist/schema/index.js
CHANGED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defines the schema for parsing and stringifying states to and from primitive values.
|
|
3
|
+
*
|
|
4
|
+
* The `Schema` type is a generic type that takes:
|
|
5
|
+
* - `State`: A state object type.
|
|
6
|
+
* - `Serialized`: The raw value type
|
|
7
|
+
* - `SerializableState?`: (optional) A (partial) state object type that can be used for serialization. Defaults to `State` if not provided.
|
|
8
|
+
*
|
|
9
|
+
* It returns the following methods:
|
|
10
|
+
* - `parse`: A function that takes a raw value and returns the corresponding state object.
|
|
11
|
+
* - `serialize`: A function that takes a (serializable) state object and returns a raw value that represents the given state.
|
|
12
|
+
* - `parserialize`: A function that takes a raw value and a reducer function - it returns a new raw value that represents the reduced state.
|
|
13
|
+
*
|
|
14
|
+
* @see createSchema for a helper function to create a `Schema` from simpler parsing and serializing functions.
|
|
15
|
+
*/
|
|
16
|
+
type Schema<State = any, Serialized = any, SerializableState = State> = {
|
|
17
|
+
/** Parse a raw value into the state shape */
|
|
18
|
+
parse: (value: Serialized) => State;
|
|
19
|
+
/** Serialize a state shape into a raw value */
|
|
20
|
+
serialize: (state: SerializableState) => Serialized;
|
|
21
|
+
/** Compute a new serialized value based on a given serialized value and a reducer of the current state. */
|
|
22
|
+
parserialize: (value: Serialized, next: (state: State) => SerializableState) => Serialized;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* A utility type to extract the state shape from a given `Schema`.
|
|
26
|
+
*/
|
|
27
|
+
type SchemaState<T extends Schema> = T extends Schema<infer U> ? U : never;
|
|
28
|
+
/**
|
|
29
|
+
* A utility type to extract the serialized value type from a given `Schema`.
|
|
30
|
+
*/
|
|
31
|
+
type SchemaSerialized<T extends Schema> = T extends Schema<any, infer U> ? U : never;
|
|
32
|
+
/**
|
|
33
|
+
* A utility type to extract the serializable state shape from a given `Schema`.
|
|
34
|
+
*/
|
|
35
|
+
type SchemaSerializableState<T extends Schema> = T extends Schema<any, any, infer U> ? U : never;
|
|
36
|
+
/**
|
|
37
|
+
* Low-level helper function to create a basic `Schema` that receives the config for converting between serialized values and state objects.
|
|
38
|
+
*
|
|
39
|
+
* The template requires two methods:
|
|
40
|
+
* - `parse`: A method that takes a serialized value and returns the corresponding state object.
|
|
41
|
+
* - `serialize`: A method that takes a state object and returns a serialized value that represents the given state.
|
|
42
|
+
*
|
|
43
|
+
* 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.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* import { createSchema } from "@buildinams/core/schema";
|
|
48
|
+
*
|
|
49
|
+
* const numberSchema = createSchema<
|
|
50
|
+
* number, // State
|
|
51
|
+
* string // Serialized
|
|
52
|
+
* >({
|
|
53
|
+
* parse: (value) => parseInt(value, 10),
|
|
54
|
+
* serialize: (state) => state.toString(),
|
|
55
|
+
* });
|
|
56
|
+
*
|
|
57
|
+
* const result = numberSchema.parserialize("42", (state) => state + 1);
|
|
58
|
+
* console.log(result); // "43"
|
|
59
|
+
* ```
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* import { createSchema } from "@buildinams/core/schema";
|
|
64
|
+
*
|
|
65
|
+
* const hrefSchema: createSchema<
|
|
66
|
+
* { pathname: string, searchParams: URLSearchParams },
|
|
67
|
+
* string,
|
|
68
|
+
* { 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
|
|
69
|
+
* >({
|
|
70
|
+
* parse: (href) => {
|
|
71
|
+
* const [pathname = "", search = ""] = href.split("?");
|
|
72
|
+
* return {
|
|
73
|
+
* pathname,
|
|
74
|
+
* searchParams: new URLSearchParams(search),
|
|
75
|
+
* };
|
|
76
|
+
* },
|
|
77
|
+
* serialize: (serializableState) => {
|
|
78
|
+
* if (!serializableState.searchParams) return serializableState.pathname;
|
|
79
|
+
* const paramsString = serializableState.searchParams.toString();
|
|
80
|
+
* return serializableState.pathname + (paramsString ? `?${paramsString}` : "");
|
|
81
|
+
* },
|
|
82
|
+
* })
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
declare const createSchema: <State = never, Serialized = never, SerializableState = State>(template: Omit<Schema<State, Serialized, SerializableState>, "parserialize">) => Schema<State, Serialized, SerializableState>;
|
|
86
|
+
|
|
87
|
+
export { type Schema as S, type SchemaState as a, type SchemaSerialized as b, type SchemaSerializableState as c, createSchema as d };
|
package/dist/store/index.js
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { q as CartOccurrences, d as EventCatalog } from '../../define-events-CxZjoUlG.js';
|
|
2
|
+
import { S as Subscribe } from '../../listeners-DkU-vBPa.js';
|
|
3
|
+
|
|
4
|
+
/** A line in a `CartSnapshot`. */
|
|
5
|
+
type CartLine<Item> = {
|
|
6
|
+
/** Stable for the line's lifetime, e.g. Shopify's cart line id. Lines are matched by it. */
|
|
7
|
+
id: string;
|
|
8
|
+
quantity: number;
|
|
9
|
+
/** Unit price, after discounts. */
|
|
10
|
+
price: number;
|
|
11
|
+
/** The project's data for the line, e.g. its product and variant. */
|
|
12
|
+
item: Item;
|
|
13
|
+
};
|
|
14
|
+
/** The cart as the server confirmed it. */
|
|
15
|
+
type CartSnapshot<Item> = {
|
|
16
|
+
id: string;
|
|
17
|
+
/** ISO 4217, e.g. `"EUR"`. */
|
|
18
|
+
currency: string;
|
|
19
|
+
lines: CartLine<Item>[];
|
|
20
|
+
};
|
|
21
|
+
/** What `createCartChanges()` reports, as its listeners receive it. */
|
|
22
|
+
type CartEvent<Item> = {
|
|
23
|
+
[K in keyof CartOccurrences<Item>]: {
|
|
24
|
+
name: K;
|
|
25
|
+
payload: CartOccurrences<Item>[K];
|
|
26
|
+
};
|
|
27
|
+
}[keyof CartOccurrences<Item>];
|
|
28
|
+
type CartChanges<Item> = {
|
|
29
|
+
/**
|
|
30
|
+
* Reports what changed since the last confirmed cart: `addedToCart` for new lines and higher quantities,
|
|
31
|
+
* `removedFromCart` for removed lines and lower quantities. Pass `null` while there's no cart, so the first lines
|
|
32
|
+
* of a new one count as added. Pass only carts the server confirmed, not optimistic ones. The last cart passed
|
|
33
|
+
* before the first `subscribe()` is held until then, e.g. from a child component's effect, which runs before its
|
|
34
|
+
* parent's. Does nothing on the server.
|
|
35
|
+
*/
|
|
36
|
+
setCart: (cart: CartSnapshot<Item> | null) => void;
|
|
37
|
+
/**
|
|
38
|
+
* Hands what `setCart()` reports to `listener`: every event, or only those named. Pass it to `createAnalytics()`'s
|
|
39
|
+
* `sources` instead to track them.
|
|
40
|
+
*/
|
|
41
|
+
subscribe: Subscribe<CartEvent<Item>>;
|
|
42
|
+
/** Catalog entries for what it reports, to spread into `defineEvents()`. */
|
|
43
|
+
events: EventCatalog<CartOccurrences<Item>>;
|
|
44
|
+
};
|
|
45
|
+
type CartChangesOptions = {
|
|
46
|
+
/**
|
|
47
|
+
* Where the last confirmed cart is kept, so other tabs and later page loads don't track it again. Change it when the
|
|
48
|
+
* item's shape changes, so items stored before don't reach the mappings. Default `"analytics-cart"`.
|
|
49
|
+
*/
|
|
50
|
+
storageKey?: string;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Reports `addedToCart` and `removedFromCart` to its listeners by comparing confirmed carts, so add and remove
|
|
54
|
+
* buttons, quantity pickers and cart drawers don't each have to. `Item` is the project's cart line item. One per page;
|
|
55
|
+
* pass it every cart the server confirms.
|
|
56
|
+
*
|
|
57
|
+
* The first cart on a page load counts as the starting point, unless the last confirmed cart is stored from an
|
|
58
|
+
* earlier page, e.g. after a form that adds to the cart and redirects. A cart another tab already tracked isn't
|
|
59
|
+
* tracked again. See `docs/analytics.md`.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* export const cartChanges = createCartChanges<CartItem>();
|
|
64
|
+
*
|
|
65
|
+
* export const events = defineEvents({ ...cartChanges.events });
|
|
66
|
+
* export const analytics = createAnalytics<EventsOf<typeof events>>({ events, sources: [cartChanges], consent });
|
|
67
|
+
*
|
|
68
|
+
* // Wherever a confirmed cart arrives, e.g. a server action's response, as `CartSnapshot<CartItem>`:
|
|
69
|
+
* cartChanges.setCart(toCartSnapshot(cart));
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
declare const createCartChanges: <Item>({ storageKey, }?: CartChangesOptions) => CartChanges<Item>;
|
|
73
|
+
|
|
74
|
+
export { type CartChanges, type CartChangesOptions, type CartLine, type CartSnapshot, createCartChanges };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { createListeners } from '../../chunk-M3HNM5HC.js';
|
|
2
|
+
import { shape } from '../../chunk-G2BKC4BU.js';
|
|
3
|
+
|
|
4
|
+
// src/tracking/cart/cart-changes.ts
|
|
5
|
+
var EMPTY = { id: null, lines: [] };
|
|
6
|
+
var toKey = ({ id, lines }) => JSON.stringify([id, lines.map((line) => [line.id, line.quantity]).sort()]);
|
|
7
|
+
var isLine = (line) => {
|
|
8
|
+
if (typeof line !== "object" || line === null) return false;
|
|
9
|
+
const { id, quantity, price } = line;
|
|
10
|
+
return typeof id === "string" && typeof quantity === "number" && typeof price === "number" && "item" in line;
|
|
11
|
+
};
|
|
12
|
+
var read = (key) => {
|
|
13
|
+
try {
|
|
14
|
+
const value = localStorage.getItem(key);
|
|
15
|
+
const stored = value ? JSON.parse(value) : void 0;
|
|
16
|
+
if (!stored || !(typeof stored.id === "string" || stored.id === null) || !Array.isArray(stored.lines) || !stored.lines.every(isLine)) {
|
|
17
|
+
return void 0;
|
|
18
|
+
}
|
|
19
|
+
return stored;
|
|
20
|
+
} catch {
|
|
21
|
+
return void 0;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
var write = (key, value) => {
|
|
25
|
+
try {
|
|
26
|
+
localStorage.setItem(key, JSON.stringify(value));
|
|
27
|
+
} catch {
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
var toPayload = (currency, changes) => {
|
|
31
|
+
const items = changes.map(([{ item, price }, quantity]) => ({
|
|
32
|
+
item,
|
|
33
|
+
quantity,
|
|
34
|
+
price
|
|
35
|
+
}));
|
|
36
|
+
return {
|
|
37
|
+
currency,
|
|
38
|
+
value: items.reduce(
|
|
39
|
+
(total, { price, quantity }) => total + price * quantity,
|
|
40
|
+
0
|
|
41
|
+
),
|
|
42
|
+
items
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
var createCartChanges = ({
|
|
46
|
+
storageKey = "analytics-cart"
|
|
47
|
+
} = {}) => {
|
|
48
|
+
let previous;
|
|
49
|
+
let held;
|
|
50
|
+
let isReplaying = false;
|
|
51
|
+
const listeners = createListeners({
|
|
52
|
+
start: () => {
|
|
53
|
+
if (!held) return;
|
|
54
|
+
isReplaying = true;
|
|
55
|
+
queueMicrotask(() => {
|
|
56
|
+
isReplaying = false;
|
|
57
|
+
if (!held || !listeners.hasListeners()) return;
|
|
58
|
+
const { cart } = held;
|
|
59
|
+
held = void 0;
|
|
60
|
+
track(cart);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
const { emit } = listeners;
|
|
65
|
+
const track = (cart) => {
|
|
66
|
+
const current = cart ? { id: cart.id, lines: cart.lines } : EMPTY;
|
|
67
|
+
const stored = read(storageKey);
|
|
68
|
+
const isTracked = stored && toKey(stored) === toKey(current);
|
|
69
|
+
const from = previous ?? stored;
|
|
70
|
+
previous = current;
|
|
71
|
+
write(storageKey, current);
|
|
72
|
+
if (!cart || isTracked || !from) return;
|
|
73
|
+
const before = from.id === cart.id ? from.lines : [];
|
|
74
|
+
const quantities = new Map(before.map((line) => [line.id, line.quantity]));
|
|
75
|
+
const added = [];
|
|
76
|
+
const removed = [];
|
|
77
|
+
for (const line of cart.lines) {
|
|
78
|
+
const delta = line.quantity - (quantities.get(line.id) ?? 0);
|
|
79
|
+
if (delta > 0) added.push([line, delta]);
|
|
80
|
+
if (delta < 0) removed.push([line, -delta]);
|
|
81
|
+
}
|
|
82
|
+
const remaining = new Set(cart.lines.map((line) => line.id));
|
|
83
|
+
for (const line of before) {
|
|
84
|
+
if (!remaining.has(line.id)) removed.push([line, line.quantity]);
|
|
85
|
+
}
|
|
86
|
+
if (removed.length > 0) {
|
|
87
|
+
emit({
|
|
88
|
+
name: "removedFromCart",
|
|
89
|
+
payload: toPayload(cart.currency, removed)
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
if (added.length > 0) {
|
|
93
|
+
emit({ name: "addedToCart", payload: toPayload(cart.currency, added) });
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
return {
|
|
97
|
+
setCart: (cart) => {
|
|
98
|
+
if (typeof window === "undefined") return;
|
|
99
|
+
if (listeners.hasListeners() && !isReplaying) track(cart);
|
|
100
|
+
else held = { cart };
|
|
101
|
+
},
|
|
102
|
+
subscribe: listeners.subscribe,
|
|
103
|
+
events: {
|
|
104
|
+
addedToCart: {
|
|
105
|
+
description: "Lines were added to the cart, or their quantity increased.",
|
|
106
|
+
payload: shape()
|
|
107
|
+
},
|
|
108
|
+
removedFromCart: {
|
|
109
|
+
description: "Lines were removed from the cart, or their quantity decreased.",
|
|
110
|
+
payload: shape()
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export { createCartChanges };
|
|
117
|
+
//# sourceMappingURL=index.js.map
|
|
118
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/tracking/cart/cart-changes.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { type EventCatalog, shape } from \"../../analytics/define-events\";\nimport { createListeners, type Subscribe } from \"../listeners\";\nimport type { CartOccurrences } from \"../occurrences\";\n\n/** A line in a `CartSnapshot`. */\nexport type CartLine<Item> = {\n\t/** Stable for the line's lifetime, e.g. Shopify's cart line id. Lines are matched by it. */\n\tid: string;\n\tquantity: number;\n\t/** Unit price, after discounts. */\n\tprice: number;\n\t/** The project's data for the line, e.g. its product and variant. */\n\titem: Item;\n};\n\n/** The cart as the server confirmed it. */\nexport type CartSnapshot<Item> = {\n\tid: string;\n\t/** ISO 4217, e.g. `\"EUR\"`. */\n\tcurrency: string;\n\tlines: CartLine<Item>[];\n};\n\n/** What `createCartChanges()` reports, as its listeners receive it. */\nexport type CartEvent<Item> = {\n\t[K in keyof CartOccurrences<Item>]: {\n\t\tname: K;\n\t\tpayload: CartOccurrences<Item>[K];\n\t};\n}[keyof CartOccurrences<Item>];\n\nexport type CartChanges<Item> = {\n\t/**\n\t * Reports what changed since the last confirmed cart: `addedToCart` for new lines and higher quantities,\n\t * `removedFromCart` for removed lines and lower quantities. Pass `null` while there's no cart, so the first lines\n\t * of a new one count as added. Pass only carts the server confirmed, not optimistic ones. The last cart passed\n\t * before the first `subscribe()` is held until then, e.g. from a child component's effect, which runs before its\n\t * parent's. Does nothing on the server.\n\t */\n\tsetCart: (cart: CartSnapshot<Item> | null) => void;\n\t/**\n\t * Hands what `setCart()` reports to `listener`: every event, or only those named. Pass it to `createAnalytics()`'s\n\t * `sources` instead to track them.\n\t */\n\tsubscribe: Subscribe<CartEvent<Item>>;\n\t/** Catalog entries for what it reports, to spread into `defineEvents()`. */\n\tevents: EventCatalog<CartOccurrences<Item>>;\n};\n\nexport type CartChangesOptions = {\n\t/**\n\t * Where the last confirmed cart is kept, so other tabs and later page loads don't track it again. Change it when the\n\t * item's shape changes, so items stored before don't reach the mappings. Default `\"analytics-cart\"`.\n\t */\n\tstorageKey?: string;\n};\n\n/** What's kept between page loads: the cart, or `id: null` for no cart. */\ntype Stored = { id: string | null; lines: CartLine<unknown>[] };\n\nconst EMPTY: Stored = { id: null, lines: [] };\n\n// Quantities by line, so an equal cart reads as already tracked, whatever else changed on it.\nconst toKey = ({ id, lines }: Stored) =>\n\tJSON.stringify([id, lines.map((line) => [line.id, line.quantity]).sort()]);\n\nconst isLine = (line: unknown): line is CartLine<unknown> => {\n\tif (typeof line !== \"object\" || line === null) return false;\n\tconst { id, quantity, price } = line as Partial<CartLine<unknown>>;\n\treturn (\n\t\ttypeof id === \"string\" &&\n\t\ttypeof quantity === \"number\" &&\n\t\ttypeof price === \"number\" &&\n\t\t\"item\" in line\n\t);\n};\n\n// Written by an earlier deploy too, so a cart in another shape is ignored rather than tracked.\nconst read = (key: string): Stored | undefined => {\n\ttry {\n\t\tconst value = localStorage.getItem(key);\n\t\tconst stored = value ? (JSON.parse(value) as Partial<Stored>) : undefined;\n\t\tif (\n\t\t\t!stored ||\n\t\t\t!(typeof stored.id === \"string\" || stored.id === null) ||\n\t\t\t!Array.isArray(stored.lines) ||\n\t\t\t!stored.lines.every(isLine)\n\t\t) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn stored as Stored;\n\t} catch {\n\t\treturn undefined;\n\t}\n};\n\nconst write = (key: string, value: Stored) => {\n\ttry {\n\t\tlocalStorage.setItem(key, JSON.stringify(value));\n\t} catch {\n\t\t// Storage blocked or full: only this page's changes are deduplicated.\n\t}\n};\n\nconst toPayload = <Item>(\n\tcurrency: string,\n\tchanges: [CartLine<Item>, number][],\n): CartOccurrences<Item>[\"addedToCart\"] => {\n\tconst items = changes.map(([{ item, price }, quantity]) => ({\n\t\titem,\n\t\tquantity,\n\t\tprice,\n\t}));\n\treturn {\n\t\tcurrency,\n\t\tvalue: items.reduce(\n\t\t\t(total, { price, quantity }) => total + price * quantity,\n\t\t\t0,\n\t\t),\n\t\titems,\n\t};\n};\n\n/**\n * Reports `addedToCart` and `removedFromCart` to its listeners by comparing confirmed carts, so add and remove\n * buttons, quantity pickers and cart drawers don't each have to. `Item` is the project's cart line item. One per page;\n * pass it every cart the server confirms.\n *\n * The first cart on a page load counts as the starting point, unless the last confirmed cart is stored from an\n * earlier page, e.g. after a form that adds to the cart and redirects. A cart another tab already tracked isn't\n * tracked again. See `docs/analytics.md`.\n *\n * @example\n * ```ts\n * export const cartChanges = createCartChanges<CartItem>();\n *\n * export const events = defineEvents({ ...cartChanges.events });\n * export const analytics = createAnalytics<EventsOf<typeof events>>({ events, sources: [cartChanges], consent });\n *\n * // Wherever a confirmed cart arrives, e.g. a server action's response, as `CartSnapshot<CartItem>`:\n * cartChanges.setCart(toCartSnapshot(cart));\n * ```\n */\nexport const createCartChanges = <Item>({\n\tstorageKey = \"analytics-cart\",\n}: CartChangesOptions = {}): CartChanges<Item> => {\n\tlet previous: Stored | undefined;\n\tlet held: { cart: CartSnapshot<Item> | null } | undefined;\n\tlet isReplaying = false;\n\n\tconst listeners = createListeners<CartEvent<Item>>({\n\t\tstart: () => {\n\t\t\tif (!held) return;\n\t\t\tisReplaying = true;\n\t\t\t// So every subscription made in the same task receives it, as reporting it marks it tracked.\n\t\t\tqueueMicrotask(() => {\n\t\t\t\tisReplaying = false;\n\t\t\t\tif (!held || !listeners.hasListeners()) return;\n\t\t\t\tconst { cart } = held;\n\t\t\t\theld = undefined;\n\t\t\t\ttrack(cart);\n\t\t\t});\n\t\t},\n\t});\n\tconst { emit } = listeners;\n\n\tconst track = (cart: CartSnapshot<Item> | null) => {\n\t\tconst current: Stored = cart ? { id: cart.id, lines: cart.lines } : EMPTY;\n\t\tconst stored = read(storageKey);\n\t\t// Another tab or page load tracked it already.\n\t\tconst isTracked = stored && toKey(stored) === toKey(current);\n\t\tconst from = previous ?? stored;\n\t\tprevious = current;\n\t\twrite(storageKey, current);\n\t\tif (!cart || isTracked || !from) return;\n\n\t\t// Stored under this key, so with its item.\n\t\tconst before = (from.id === cart.id ? from.lines : []) as CartLine<Item>[];\n\t\tconst quantities = new Map(before.map((line) => [line.id, line.quantity]));\n\t\tconst added: [CartLine<Item>, number][] = [];\n\t\tconst removed: [CartLine<Item>, number][] = [];\n\t\tfor (const line of cart.lines) {\n\t\t\tconst delta = line.quantity - (quantities.get(line.id) ?? 0);\n\t\t\tif (delta > 0) added.push([line, delta]);\n\t\t\tif (delta < 0) removed.push([line, -delta]);\n\t\t}\n\t\tconst remaining = new Set(cart.lines.map((line) => line.id));\n\t\tfor (const line of before) {\n\t\t\tif (!remaining.has(line.id)) removed.push([line, line.quantity]);\n\t\t}\n\n\t\tif (removed.length > 0) {\n\t\t\temit({\n\t\t\t\tname: \"removedFromCart\",\n\t\t\t\tpayload: toPayload(cart.currency, removed),\n\t\t\t});\n\t\t}\n\t\tif (added.length > 0) {\n\t\t\temit({ name: \"addedToCart\", payload: toPayload(cart.currency, added) });\n\t\t}\n\t};\n\n\treturn {\n\t\tsetCart: (cart) => {\n\t\t\tif (typeof window === \"undefined\") return;\n\t\t\tif (listeners.hasListeners() && !isReplaying) track(cart);\n\t\t\telse held = { cart };\n\t\t},\n\t\tsubscribe: listeners.subscribe,\n\t\tevents: {\n\t\t\taddedToCart: {\n\t\t\t\tdescription:\n\t\t\t\t\t\"Lines were added to the cart, or their quantity increased.\",\n\t\t\t\tpayload: shape(),\n\t\t\t},\n\t\t\tremovedFromCart: {\n\t\t\t\tdescription:\n\t\t\t\t\t\"Lines were removed from the cart, or their quantity decreased.\",\n\t\t\t\tpayload: shape(),\n\t\t\t},\n\t\t},\n\t};\n};\n"]}
|