@builder.io/sdk-svelte 0.0.4 → 0.0.5

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 (118) hide show
  1. package/package/--package.json +3 -2
  2. package/package.json +3 -2
  3. package/src/blocks/BaseText.svelte +27 -0
  4. package/src/blocks/button/button.svelte +36 -0
  5. package/src/blocks/button/component-info.ts +42 -0
  6. package/src/blocks/columns/columns.svelte +148 -0
  7. package/src/blocks/columns/component-info.ts +254 -0
  8. package/src/blocks/custom-code/component-info.ts +31 -0
  9. package/src/blocks/custom-code/custom-code.svelte +66 -0
  10. package/src/blocks/embed/component-info.ts +53 -0
  11. package/src/blocks/embed/embed.svelte +51 -0
  12. package/src/blocks/embed/helpers.ts +8 -0
  13. package/src/blocks/form/component-info.ts +282 -0
  14. package/src/blocks/form/form.svelte +332 -0
  15. package/src/blocks/fragment/component-info.ts +10 -0
  16. package/src/blocks/fragment/fragment.svelte +12 -0
  17. package/src/blocks/image/component-info.ts +170 -0
  18. package/src/blocks/image/image.helpers.ts +75 -0
  19. package/src/blocks/image/image.svelte +145 -0
  20. package/src/blocks/img/component-info.ts +21 -0
  21. package/src/blocks/img/img.svelte +51 -0
  22. package/src/blocks/input/component-info.ts +78 -0
  23. package/src/blocks/input/input.svelte +34 -0
  24. package/src/blocks/raw-text/component-info.ts +15 -0
  25. package/src/blocks/raw-text/raw-text.svelte +16 -0
  26. package/src/blocks/section/component-info.ts +50 -0
  27. package/src/blocks/section/section.svelte +34 -0
  28. package/src/blocks/select/component-info.ts +61 -0
  29. package/src/blocks/select/select.svelte +34 -0
  30. package/src/blocks/submit-button/component-info.ts +32 -0
  31. package/src/blocks/submit-button/submit-button.svelte +15 -0
  32. package/src/blocks/symbol/component-info.ts +43 -0
  33. package/src/blocks/symbol/symbol.svelte +100 -0
  34. package/src/blocks/text/component-info.ts +24 -0
  35. package/src/blocks/text/text.svelte +11 -0
  36. package/src/blocks/textarea/component-info.ts +48 -0
  37. package/src/blocks/textarea/textarea.svelte +19 -0
  38. package/src/blocks/util.ts +6 -0
  39. package/src/blocks/video/component-info.ts +109 -0
  40. package/src/blocks/video/video.svelte +102 -0
  41. package/src/components/render-block/block-styles.svelte +62 -0
  42. package/src/components/render-block/render-block.helpers.ts +27 -0
  43. package/src/components/render-block/render-block.svelte +228 -0
  44. package/src/components/render-block/render-component-with-context.svelte +38 -0
  45. package/src/components/render-block/render-component.svelte +35 -0
  46. package/src/components/render-block/render-repeated-block.svelte +53 -0
  47. package/src/components/render-block/types.ts +7 -0
  48. package/src/components/render-blocks.svelte +104 -0
  49. package/src/components/render-content/components/render-styles.svelte +80 -0
  50. package/src/components/render-content/index.ts +1 -0
  51. package/src/components/render-content/render-content.svelte +354 -0
  52. package/src/components/render-inlined-styles.svelte +26 -0
  53. package/src/constants/builder-registered-components.ts +42 -0
  54. package/src/constants/device-sizes.ts +28 -0
  55. package/src/constants/target.ts +2 -0
  56. package/src/context/builder.context.ts +13 -0
  57. package/src/context/types.ts +23 -0
  58. package/src/functions/camel-to-kebab-case.ts +2 -0
  59. package/src/functions/evaluate.ts +52 -0
  60. package/src/functions/event-handler-name.ts +6 -0
  61. package/src/functions/extract-text-styles.ts +39 -0
  62. package/src/functions/fast-clone.ts +5 -0
  63. package/src/functions/get-block-actions-handler.ts +19 -0
  64. package/src/functions/get-block-actions.ts +25 -0
  65. package/src/functions/get-block-component-options.ts +12 -0
  66. package/src/functions/get-block-properties.ts +11 -0
  67. package/src/functions/get-block-tag.ts +7 -0
  68. package/src/functions/get-builder-search-params/fn.test.ts +19 -0
  69. package/src/functions/get-builder-search-params/index.ts +48 -0
  70. package/src/functions/get-content/ab-testing.ts +132 -0
  71. package/src/functions/get-content/fn.test.ts +35 -0
  72. package/src/functions/get-content/index.ts +77 -0
  73. package/src/functions/get-content/types.ts +36 -0
  74. package/src/functions/get-fetch.ts +14 -0
  75. package/src/functions/get-global-this.ts +15 -0
  76. package/src/functions/get-processed-block.test.ts +33 -0
  77. package/src/functions/get-processed-block.ts +54 -0
  78. package/src/functions/get-react-native-block-styles.ts +30 -0
  79. package/src/functions/if-target.ts +20 -0
  80. package/src/functions/is-browser.ts +3 -0
  81. package/src/functions/is-editing.ts +7 -0
  82. package/src/functions/is-iframe.ts +5 -0
  83. package/src/functions/is-previewing.ts +14 -0
  84. package/src/functions/mark-mutable.ts +7 -0
  85. package/src/functions/on-change.test.ts +23 -0
  86. package/src/functions/on-change.ts +29 -0
  87. package/src/functions/register-component.ts +67 -0
  88. package/src/functions/register.ts +46 -0
  89. package/src/functions/sanitize-react-native-block-styles.ts +65 -0
  90. package/src/functions/set-editor-settings.ts +16 -0
  91. package/src/functions/set.test.ts +19 -0
  92. package/src/functions/set.ts +26 -0
  93. package/src/functions/track.ts +90 -0
  94. package/src/functions/transform-block.ts +6 -0
  95. package/src/helpers/ab-tests.ts +22 -0
  96. package/src/helpers/cookie.ts +102 -0
  97. package/src/helpers/css.ts +33 -0
  98. package/src/helpers/flatten.ts +24 -0
  99. package/src/helpers/localStorage.ts +38 -0
  100. package/src/helpers/nullable.ts +4 -0
  101. package/src/helpers/sessionId.ts +34 -0
  102. package/src/helpers/time.ts +4 -0
  103. package/src/helpers/url.test.ts +16 -0
  104. package/src/helpers/url.ts +12 -0
  105. package/src/helpers/uuid.ts +17 -0
  106. package/src/helpers/visitorId.ts +38 -0
  107. package/src/index-helpers/blocks-exports.ts +10 -0
  108. package/src/index-helpers/top-of-file.ts +4 -0
  109. package/src/index.ts +12 -0
  110. package/src/scripts/init-editing.ts +102 -0
  111. package/src/types/builder-block.ts +58 -0
  112. package/src/types/builder-content.ts +39 -0
  113. package/src/types/can-track.ts +3 -0
  114. package/src/types/components.ts +188 -0
  115. package/src/types/deep-partial.ts +7 -0
  116. package/src/types/element.ts +60 -0
  117. package/src/types/targets.ts +6 -0
  118. package/src/types/typescript.ts +7 -0
@@ -0,0 +1,67 @@
1
+ import type { RegisteredComponent } from '../context/types.js';
2
+ import type { ComponentInfo, Input } from '../types/components.js';
3
+ import { fastClone } from './fast-clone.js';
4
+
5
+ /**
6
+ * @deprecated. Use the `customComponents` prop in RenderContent instead to provide your custom components to the builder SDK.
7
+ */
8
+ export const components: RegisteredComponent[] = [];
9
+
10
+ /**
11
+ * @deprecated. Use the `customComponents` prop in RenderContent instead to provide your custom components to the builder SDK.
12
+ */
13
+ export function registerComponent(component: any, info: ComponentInfo): void {
14
+ components.push({ component, ...info });
15
+
16
+ console.warn(
17
+ 'registerComponent is deprecated. Use the `customComponents` prop in RenderContent instead to provide your custom components to the builder SDK.'
18
+ );
19
+
20
+ return component;
21
+ }
22
+
23
+ export const createRegisterComponentMessage = ({
24
+ component: _,
25
+ ...info
26
+ }: RegisteredComponent) => ({
27
+ type: 'builder.registerComponent',
28
+ data: prepareComponentInfoToSend(info),
29
+ });
30
+
31
+ const serializeValue = (value: object): any =>
32
+ typeof value === 'function' ? serializeFn(value) : fastClone(value);
33
+
34
+ /**
35
+ * Input attributes that are functions must be converted to strings before being serialized to JSON.
36
+ */
37
+ // eslint-disable-next-line @typescript-eslint/ban-types
38
+ const serializeFn = (fnValue: Function) => {
39
+ const fnStr = fnValue.toString().trim();
40
+ // we need to account for a few different fn syntaxes:
41
+ // 1. `function name(args) => {code}`
42
+ // 2. `name(args) => {code}`
43
+ // 3. `(args) => {}`
44
+ const appendFunction =
45
+ !fnStr.startsWith('function') && !fnStr.startsWith('(');
46
+
47
+ return `return (${
48
+ appendFunction ? 'function ' : ''
49
+ }${fnStr}).apply(this, arguments)`;
50
+ };
51
+
52
+ const prepareComponentInfoToSend = ({
53
+ inputs,
54
+ ...info
55
+ }: ComponentInfo): ComponentInfo => ({
56
+ ...fastClone(info),
57
+ inputs: inputs?.map(
58
+ (input): Input =>
59
+ Object.entries(input).reduce(
60
+ (acc, [key, value]) => ({
61
+ ...acc,
62
+ [key]: serializeValue(value),
63
+ }),
64
+ {} as Input
65
+ )
66
+ ),
67
+ });
@@ -0,0 +1,46 @@
1
+ import type { BuilderBlock } from '../types/builder-block.js';
2
+ import type { DeepPartial } from '../types/deep-partial.js';
3
+ import { isBrowser } from './is-browser.js';
4
+
5
+ export interface InsertMenuItem {
6
+ name: string;
7
+ icon?: string;
8
+ item: DeepPartial<BuilderBlock>;
9
+ }
10
+
11
+ export interface InsertMenuConfig {
12
+ name: string;
13
+ priority?: number;
14
+ persist?: boolean;
15
+ advanced?: boolean;
16
+ items: InsertMenuItem[];
17
+ }
18
+
19
+ const registry: { [key: string]: any[] } = {};
20
+
21
+ export function register(type: 'insertMenu', info: InsertMenuConfig): void;
22
+ export function register(type: string, info: any): void;
23
+ export function register(type: string, info: any) {
24
+ let typeList = registry[type];
25
+ if (!typeList) {
26
+ typeList = registry[type] = [];
27
+ }
28
+ typeList.push(info);
29
+ if (isBrowser()) {
30
+ const message = {
31
+ type: 'builder.register',
32
+ data: {
33
+ type,
34
+ info,
35
+ },
36
+ };
37
+ try {
38
+ parent.postMessage(message, '*');
39
+ if (parent !== window) {
40
+ window.postMessage(message, '*');
41
+ }
42
+ } catch (err) {
43
+ console.debug('Could not postmessage', err);
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,65 @@
1
+ const propertiesThatMustBeNumber = new Set(['lineHeight']);
2
+ const displayValues = new Set(['flex', 'none']);
3
+
4
+ const SHOW_WARNINGS = false;
5
+
6
+ type Styles = Record<string, string | number>;
7
+
8
+ const normalizeNumber = (value: number): number | undefined => {
9
+ if (Number.isNaN(value)) {
10
+ return undefined;
11
+ } else if (value < 0) {
12
+ // TODO: why are negative values not allowed?
13
+ return 0;
14
+ } else {
15
+ return value;
16
+ }
17
+ };
18
+
19
+ export const sanitizeReactNativeBlockStyles = (styles: Styles): Styles => {
20
+ return Object.keys(styles).reduce<Styles>((acc, key): Styles => {
21
+ const propertyValue = styles[key];
22
+
23
+ if (key === 'display' && !displayValues.has(propertyValue as string)) {
24
+ if (SHOW_WARNINGS) {
25
+ console.warn(
26
+ `Style value for key "display" must be "flex" or "none" but had ${propertyValue}`
27
+ );
28
+ }
29
+ return acc;
30
+ }
31
+
32
+ if (
33
+ propertiesThatMustBeNumber.has(key) &&
34
+ typeof propertyValue !== 'number'
35
+ ) {
36
+ if (SHOW_WARNINGS) {
37
+ console.warn(
38
+ `Style key ${key} must be a number, but had value \`${styles[key]}\``
39
+ );
40
+ }
41
+ return acc;
42
+ }
43
+
44
+ if (typeof propertyValue === 'string') {
45
+ // `px` units need to be stripped and replaced with numbers
46
+ // https://regexr.com/6ualn
47
+ const isPixelUnit = propertyValue.match(/^-?(\d*)(\.?)(\d*)*px$/);
48
+
49
+ if (isPixelUnit) {
50
+ const newValue = parseFloat(propertyValue);
51
+ const normalizedValue = normalizeNumber(newValue);
52
+ if (normalizedValue) {
53
+ return { ...acc, [key]: normalizedValue };
54
+ } else {
55
+ return acc;
56
+ }
57
+ } else if (propertyValue === '0') {
58
+ // 0 edge case needs to be handled
59
+ return { ...acc, [key]: 0 };
60
+ }
61
+ }
62
+
63
+ return { ...acc, [key]: propertyValue };
64
+ }, {});
65
+ };
@@ -0,0 +1,16 @@
1
+ import { isBrowser } from './is-browser.js';
2
+
3
+ const settings: Settings = {};
4
+
5
+ export type Settings = { customInsertMenu?: boolean };
6
+
7
+ export function setEditorSettings(newSettings: Settings) {
8
+ if (isBrowser()) {
9
+ Object.assign(settings, newSettings);
10
+ const message = {
11
+ type: 'builder.settingsChange',
12
+ data: settings,
13
+ };
14
+ parent.postMessage(message, '*');
15
+ }
16
+ }
@@ -0,0 +1,19 @@
1
+ import { set } from './set.js';
2
+
3
+ test('can shallow set a property', () => {
4
+ const obj = { foo: 'bar' };
5
+ set(obj, 'foo', 'baz');
6
+ expect(obj.foo).toBe('baz');
7
+ });
8
+
9
+ test('can deeply set a property', () => {
10
+ const obj = { foo: 'bar' };
11
+ set(obj, 'foo.bar', 'baz');
12
+ expect((obj.foo as any).bar).toBe('baz');
13
+ });
14
+
15
+ test('can deeply create arrays', () => {
16
+ const obj = { foo: 'bar' };
17
+ set(obj, 'foo.bar.0', 'hi');
18
+ expect((obj.foo as any).bar).toEqual(['hi']);
19
+ });
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Minimal implementation of lodash's _.set
3
+ * https://lodash.com/docs/4.17.15#set
4
+ *
5
+ * See ./set.test.ts for usage examples
6
+ */
7
+ export const set = (obj: any, _path: string | string[], value: any) => {
8
+ if (Object(obj) !== obj) {
9
+ return obj;
10
+ }
11
+ const path: string[] = Array.isArray(_path)
12
+ ? _path
13
+ : (_path.toString().match(/[^.[\]]+/g) as string[]);
14
+
15
+ path
16
+ .slice(0, -1)
17
+ .reduce(
18
+ (a, c, i) =>
19
+ Object(a[c]) === a[c]
20
+ ? a[c]
21
+ : (a[c] =
22
+ Math.abs(Number(path[i + 1])) >> 0 === +path[i + 1] ? [] : {}),
23
+ obj
24
+ )[path[path.length - 1]] = value;
25
+ return obj;
26
+ };
@@ -0,0 +1,90 @@
1
+ import { TARGET } from '../constants/target.js';
2
+ import { getSessionId } from '../helpers/sessionId.js';
3
+ import { getVisitorId } from '../helpers/visitorId.js';
4
+ import type { CanTrack } from '../types/can-track.js';
5
+ import { isBrowser } from './is-browser.js';
6
+ import { isEditing } from './is-editing.js';
7
+
8
+ interface Event {
9
+ type: 'click' | 'impression';
10
+ data: {
11
+ contentId?: string;
12
+ ownerId: string;
13
+ metadata?: string;
14
+ sessionId: string | undefined;
15
+ visitorId: string | undefined;
16
+ [index: string]: any;
17
+ };
18
+ }
19
+
20
+ type TrackingData = {
21
+ visitorId: string | undefined;
22
+ sessionId: string | undefined;
23
+ };
24
+
25
+ const getTrackingEventData = async ({
26
+ canTrack,
27
+ }: CanTrack): Promise<TrackingData> => {
28
+ if (!canTrack) {
29
+ return { visitorId: undefined, sessionId: undefined };
30
+ }
31
+
32
+ const sessionId = await getSessionId({ canTrack });
33
+ const visitorId = getVisitorId({ canTrack });
34
+
35
+ return {
36
+ sessionId,
37
+ visitorId,
38
+ };
39
+ };
40
+
41
+ type EventProperties = {
42
+ type: Event['type'];
43
+ orgId: Event['data']['ownerId'];
44
+ contentId: Event['data']['contentId'];
45
+ [index: string]: any;
46
+ };
47
+
48
+ export type EventProps = EventProperties & CanTrack;
49
+
50
+ const createEvent = async ({
51
+ type: eventType,
52
+ canTrack,
53
+ orgId,
54
+ contentId,
55
+ ...properties
56
+ }: EventProps): Promise<Event> => ({
57
+ type: eventType,
58
+ data: {
59
+ ...properties,
60
+ ...(await getTrackingEventData({ canTrack })),
61
+ ownerId: orgId,
62
+ contentId,
63
+ },
64
+ });
65
+
66
+ export async function track(eventProps: EventProps) {
67
+ if (!eventProps.canTrack) {
68
+ return;
69
+ }
70
+
71
+ if (isEditing()) {
72
+ return;
73
+ }
74
+ if (!(isBrowser() || TARGET === 'reactNative')) {
75
+ return;
76
+ }
77
+
78
+ return fetch(`https://builder.io/api/v1/track`, {
79
+ method: 'POST',
80
+ body: JSON.stringify({
81
+ events: [await createEvent(eventProps)],
82
+ }),
83
+ headers: {
84
+ 'content-type': 'application/json',
85
+ },
86
+ mode: 'cors',
87
+ }).catch((err) => {
88
+ console.error('Failed to track: ', err);
89
+ });
90
+ }
@@ -0,0 +1,6 @@
1
+ import type { BuilderBlock } from '../types/builder-block.js';
2
+
3
+ // Noope way for targets to make modifications to the block object if/as needed
4
+ export function transformBlock(block: BuilderBlock): BuilderBlock {
5
+ return block;
6
+ }
@@ -0,0 +1,22 @@
1
+ import type { CanTrack } from '../types/can-track.js';
2
+ import { getCookie, setCookie } from './cookie.js';
3
+
4
+ const BUILDER_STORE_PREFIX = 'builderio.variations';
5
+
6
+ const getContentTestKey = (id: string) => `${BUILDER_STORE_PREFIX}.${id}`;
7
+
8
+ export const getContentVariationCookie = ({
9
+ contentId,
10
+ canTrack,
11
+ }: { contentId: string } & CanTrack) =>
12
+ getCookie({ name: getContentTestKey(contentId), canTrack });
13
+
14
+ export const setContentVariationCookie = ({
15
+ contentId,
16
+ canTrack,
17
+ value,
18
+ }: {
19
+ contentId: string;
20
+ value: string;
21
+ } & CanTrack) =>
22
+ setCookie({ name: getContentTestKey(contentId), value, canTrack });
@@ -0,0 +1,102 @@
1
+ import { isBrowser } from '../functions/is-browser.js';
2
+ import type { CanTrack } from '../types/can-track.js';
3
+ import { getTopLevelDomain } from './url.js';
4
+
5
+ /**
6
+ * NOTE: This function is `async` because its react-native override is async. Do not remove the `async` keyword!
7
+ */
8
+ export const getCookie = async ({
9
+ name,
10
+ canTrack,
11
+ }: {
12
+ name: string;
13
+ } & CanTrack) => {
14
+ try {
15
+ if (!canTrack) {
16
+ return undefined;
17
+ }
18
+
19
+ /**
20
+ * Extracted from MDN docs
21
+ * https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie#example_2_get_a_sample_cookie_named_test2
22
+ */
23
+ return document.cookie
24
+ .split('; ')
25
+ .find((row) => row.startsWith(`${name}=`))
26
+ ?.split('=')[1];
27
+ } catch (err) {
28
+ console.debug('[COOKIE] GET error: ', err);
29
+ }
30
+ };
31
+
32
+ type CookieConfiguration = Array<
33
+ | ['expires', string]
34
+ | ['path', '/']
35
+ | ['domain', string]
36
+ | ['secure', '']
37
+ | ['SameSite', 'None']
38
+ | [string, string]
39
+ | []
40
+ >;
41
+
42
+ const stringifyCookie = (cookie: CookieConfiguration): string =>
43
+ cookie.map(([key, value]) => (value ? `${key}=${value}` : key)).join('; ');
44
+
45
+ const SECURE_CONFIG: CookieConfiguration = [
46
+ ['secure', ''],
47
+ ['SameSite', 'None'],
48
+ ];
49
+
50
+ const createCookieString = ({
51
+ name,
52
+ value,
53
+ expires,
54
+ }: {
55
+ name: string;
56
+ value: string;
57
+ expires?: Date;
58
+ }) => {
59
+ const secure = isBrowser() ? location.protocol === 'https:' : true;
60
+
61
+ const secureObj: CookieConfiguration = secure ? SECURE_CONFIG : [[]];
62
+
63
+ // TODO: need to know if secure server side
64
+ const expiresObj: CookieConfiguration = expires
65
+ ? [['expires', expires.toUTCString()]]
66
+ : [[]];
67
+
68
+ const cookieValue: CookieConfiguration = [
69
+ [name, value],
70
+ ...expiresObj,
71
+ ['path', '/'],
72
+ ['domain', getTopLevelDomain(window.location.hostname)],
73
+ ...secureObj,
74
+ ];
75
+ const cookie = stringifyCookie(cookieValue);
76
+
77
+ return cookie;
78
+ };
79
+
80
+ /**
81
+ * NOTE: This function is `async` because its react-native override is async. Do not remove the `async` keyword!
82
+ */
83
+ export const setCookie = async ({
84
+ name,
85
+ value,
86
+ expires,
87
+ canTrack,
88
+ }: {
89
+ name: string;
90
+ value: string;
91
+ expires?: Date;
92
+ } & CanTrack) => {
93
+ try {
94
+ if (!canTrack) {
95
+ return undefined;
96
+ }
97
+ const cookie = createCookieString({ name, value, expires });
98
+ document.cookie = cookie;
99
+ } catch (err) {
100
+ console.warn('[COOKIE] SET error: ', err);
101
+ }
102
+ };
@@ -0,0 +1,33 @@
1
+ import { camelToKebabCase } from '../functions/camel-to-kebab-case.js';
2
+
3
+ const convertStyleMaptoCSS = (style: Partial<CSSStyleDeclaration>): string => {
4
+ const cssProps = Object.entries(style).map(([key, value]) => {
5
+ if (typeof value === 'string') {
6
+ return `${camelToKebabCase(key)}: ${value};`;
7
+ }
8
+ });
9
+
10
+ return cssProps.join('\n');
11
+ };
12
+
13
+ export const createCssClass = ({
14
+ mediaQuery,
15
+ className,
16
+ styles,
17
+ }: {
18
+ mediaQuery?: string;
19
+ className: string;
20
+ styles: Partial<CSSStyleDeclaration>;
21
+ }) => {
22
+ const cssClass = `.${className} {
23
+ ${convertStyleMaptoCSS(styles)}
24
+ }`;
25
+
26
+ if (mediaQuery) {
27
+ return `${mediaQuery} {
28
+ ${cssClass}
29
+ }`;
30
+ } else {
31
+ return cssClass;
32
+ }
33
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Convert deep object to a flat object with dots
3
+ *
4
+ * { foo: { bar: 'baz' }} -> { 'foo.bar': 'baz' }
5
+ */
6
+ export function flatten<T extends Record<string, any>>(
7
+ object: T,
8
+ path: string | null = null,
9
+ separator = '.'
10
+ ): T {
11
+ return Object.keys(object).reduce((acc: T, key: string): T => {
12
+ const value = object[key];
13
+ const newPath = [path, key].filter(Boolean).join(separator);
14
+ const isObject = [
15
+ typeof value === 'object',
16
+ value !== null,
17
+ !(Array.isArray(value) && value.length === 0),
18
+ ].every(Boolean);
19
+
20
+ return isObject
21
+ ? { ...acc, ...flatten(value, newPath, separator) }
22
+ : { ...acc, [newPath]: value };
23
+ }, {} as T);
24
+ }
@@ -0,0 +1,38 @@
1
+ import { isBrowser } from '../functions/is-browser.js';
2
+ import type { CanTrack } from '../types/can-track.js';
3
+
4
+ const getLocalStorage = () =>
5
+ isBrowser() && typeof localStorage !== 'undefined' ? localStorage : undefined;
6
+
7
+ export const getLocalStorageItem = ({
8
+ key,
9
+ canTrack,
10
+ }: {
11
+ key: string;
12
+ } & CanTrack) => {
13
+ try {
14
+ if (canTrack) {
15
+ return getLocalStorage()?.getItem(key);
16
+ }
17
+ return undefined;
18
+ } catch (err) {
19
+ console.debug('[LocalStorage] GET error: ', err);
20
+ }
21
+ };
22
+
23
+ export const setLocalStorageItem = ({
24
+ key,
25
+ canTrack,
26
+ value,
27
+ }: {
28
+ key: string;
29
+ value: string;
30
+ } & CanTrack) => {
31
+ try {
32
+ if (canTrack) {
33
+ getLocalStorage()?.setItem(key, value);
34
+ }
35
+ } catch (err) {
36
+ console.debug('[LocalStorage] SET error: ', err);
37
+ }
38
+ };
@@ -0,0 +1,4 @@
1
+ export type Nullable<T> = T | null | undefined;
2
+
3
+ export const checkIsDefined = <T>(maybeT: Nullable<T>): maybeT is T =>
4
+ maybeT !== null && maybeT !== undefined;
@@ -0,0 +1,34 @@
1
+ import type { CanTrack } from '../types/can-track.js';
2
+ import { getCookie, setCookie } from './cookie.js';
3
+ import { checkIsDefined } from './nullable.js';
4
+ import { uuid } from './uuid.js';
5
+
6
+ const SESSION_LOCAL_STORAGE_KEY = 'builderSessionId';
7
+
8
+ export const getSessionId = async ({ canTrack }: CanTrack) => {
9
+ if (!canTrack) {
10
+ return undefined;
11
+ }
12
+
13
+ const sessionId = await getCookie({
14
+ name: SESSION_LOCAL_STORAGE_KEY,
15
+ canTrack,
16
+ });
17
+
18
+ if (checkIsDefined(sessionId)) {
19
+ return sessionId;
20
+ } else {
21
+ const newSessionId = createSessionId();
22
+ setSessionId({ id: newSessionId, canTrack });
23
+ }
24
+ };
25
+
26
+ export const createSessionId = () => uuid();
27
+
28
+ export const setSessionId = ({
29
+ id,
30
+ canTrack,
31
+ }: {
32
+ id: string;
33
+ } & CanTrack) =>
34
+ setCookie({ name: SESSION_LOCAL_STORAGE_KEY, value: id, canTrack });
@@ -0,0 +1,4 @@
1
+ const MINUTE_TO_MILLESECONDS_MULTIPLIER = 60000;
2
+
3
+ export const getCurrentDatePlusMinutes = (minutes = 30) =>
4
+ new Date(Date.now() + minutes * MINUTE_TO_MILLESECONDS_MULTIPLIER);
@@ -0,0 +1,16 @@
1
+ import { getTopLevelDomain } from './url.js';
2
+
3
+ describe('getTopLevelDomain', () => {
4
+ test('handles root domain', () => {
5
+ const output = getTopLevelDomain('example.com');
6
+ expect(output).toBe('example.com');
7
+ });
8
+ test('handles subdomain', () => {
9
+ const output = getTopLevelDomain('wwww.example.com');
10
+ expect(output).toBe('example.com');
11
+ });
12
+ test('handles subdomain with long suffix', () => {
13
+ const output = getTopLevelDomain('www.example.co.uk');
14
+ expect(output).toBe('example.co.uk');
15
+ });
16
+ });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Only gets one level up from hostname
3
+ * wwww.example.com -> example.com
4
+ * www.example.co.uk -> example.co.uk
5
+ */
6
+ export const getTopLevelDomain = (host: string) => {
7
+ const parts = host.split('.');
8
+ if (parts.length > 2) {
9
+ return parts.slice(1).join('.');
10
+ }
11
+ return host;
12
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @credit https://stackoverflow.com/a/2117523
3
+ */
4
+ export function uuidv4() {
5
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
6
+ const r = (Math.random() * 16) | 0,
7
+ v = c == 'x' ? r : (r & 0x3) | 0x8;
8
+ return v.toString(16);
9
+ });
10
+ }
11
+
12
+ /**
13
+ * Slightly cleaner and smaller UUIDs
14
+ */
15
+ export function uuid() {
16
+ return uuidv4().replace(/-/g, '');
17
+ }