@builder.io/sdk-svelte 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/package/--package.json +8 -5
  2. package/package/CHANGELOG.md +13 -0
  3. package/package/components/render-block/render-block.svelte +6 -6
  4. package/package/components/render-content/components/render-styles.svelte +1 -1
  5. package/package/components/render-content/render-content.svelte +84 -2
  6. package/package/components/render-content/render-content.svelte.d.ts +5 -0
  7. package/package/components/render-content/render-content.types.d.ts +11 -2
  8. package/package/components/render-content/wrap-component-ref.d.ts +6 -0
  9. package/package/components/render-content/wrap-component-ref.js +6 -0
  10. package/package/components/render-content-variants/helpers.d.ts +17 -0
  11. package/package/components/render-content-variants/helpers.js +182 -0
  12. package/package/components/render-content-variants/render-content-variants.svelte +70 -0
  13. package/package/components/render-content-variants/render-content-variants.svelte.d.ts +23 -0
  14. package/package/components/render-inlined-styles.svelte +3 -15
  15. package/package/components/render-inlined-styles.svelte.d.ts +2 -0
  16. package/package/constants/sdk-version.d.ts +1 -1
  17. package/package/constants/sdk-version.js +1 -1
  18. package/package/functions/get-content/generate-content-url.js +2 -2
  19. package/package/functions/get-content/generate-content-url.test.js +15 -0
  20. package/package/functions/get-content/index.d.ts +7 -2
  21. package/package/functions/get-content/index.js +43 -20
  22. package/package/functions/get-content/types.d.ts +6 -0
  23. package/package/helpers/ab-tests.d.ts +8 -7
  24. package/package/helpers/ab-tests.js +103 -4
  25. package/package/helpers/canTrack.d.ts +1 -0
  26. package/package/helpers/canTrack.js +2 -0
  27. package/package/helpers/cookie.d.ts +7 -3
  28. package/package/helpers/cookie.js +9 -6
  29. package/package/helpers/logger.d.ts +1 -0
  30. package/package/helpers/logger.js +1 -0
  31. package/package/index-helpers/blocks-exports.d.ts +1 -1
  32. package/package/index-helpers/blocks-exports.js +1 -1
  33. package/package/index.d.ts +8 -7
  34. package/package/index.js +6 -7
  35. package/package/scripts/init-editing.d.ts +1 -0
  36. package/package/types/builder-content.d.ts +1 -3
  37. package/package.json +4 -5
  38. package/src/components/render-block/render-block.svelte +6 -6
  39. package/src/components/render-content/components/render-styles.svelte +1 -1
  40. package/src/components/render-content/render-content.svelte +86 -2
  41. package/src/components/render-content/render-content.types.ts +11 -2
  42. package/src/components/render-content/wrap-component-ref.ts +6 -0
  43. package/src/components/render-content-variants/helpers.ts +245 -0
  44. package/src/components/render-content-variants/render-content-variants.svelte +83 -0
  45. package/src/components/render-inlined-styles.svelte +3 -16
  46. package/src/constants/sdk-version.ts +1 -1
  47. package/src/functions/get-content/generate-content-url.test.ts +17 -0
  48. package/src/functions/get-content/generate-content-url.ts +2 -1
  49. package/src/functions/get-content/index.ts +57 -26
  50. package/src/functions/get-content/types.ts +7 -0
  51. package/src/helpers/ab-tests.ts +164 -11
  52. package/src/helpers/canTrack.ts +4 -0
  53. package/src/helpers/cookie.ts +16 -11
  54. package/src/helpers/logger.ts +1 -0
  55. package/src/index-helpers/blocks-exports.ts +1 -1
  56. package/src/index.ts +17 -8
  57. package/src/scripts/init-editing.ts +1 -0
  58. package/src/types/builder-content.ts +1 -3
  59. package/package/functions/get-content/ab-testing.d.ts +0 -5
  60. package/package/functions/get-content/ab-testing.js +0 -78
  61. package/src/functions/get-content/ab-testing.ts +0 -132
@@ -0,0 +1,245 @@
1
+ import { isBrowser } from '../../functions/is-browser';
2
+ import type { Nullable } from '../../helpers/nullable';
3
+ import type { BuilderContent } from '../../types/builder-content';
4
+ import type { Target } from '../../types/targets';
5
+
6
+ export const getVariants = (content: Nullable<BuilderContent>) =>
7
+ Object.values(content?.variations || {});
8
+
9
+ export const checkShouldRunVariants = ({
10
+ canTrack,
11
+ content,
12
+ }: {
13
+ canTrack: Nullable<boolean>;
14
+ content: Nullable<BuilderContent>;
15
+ }) => {
16
+ const hasVariants = getVariants(content).length > 0;
17
+
18
+ if (!hasVariants) {
19
+ return false;
20
+ }
21
+
22
+ if (!canTrack) {
23
+ return false;
24
+ }
25
+
26
+ if (isBrowser()) {
27
+ return false;
28
+ }
29
+
30
+ return true;
31
+ };
32
+
33
+ type VariantData = {
34
+ id: string;
35
+ testRatio?: number;
36
+ };
37
+
38
+ /**
39
+ * NOTE: when this function is stringified, single-line comments can cause weird issues when compiled by Sveltekit.
40
+ * Make sure to write multi-line comments only.
41
+ */
42
+ function bldrAbTest(
43
+ contentId: string,
44
+ variants: VariantData[],
45
+ isHydrationTarget: boolean
46
+ ) {
47
+ function getAndSetVariantId(): string {
48
+ function setCookie(name: string, value: string, days?: number) {
49
+ let expires = '';
50
+ if (days) {
51
+ const date = new Date();
52
+ date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
53
+ expires = '; expires=' + date.toUTCString();
54
+ }
55
+ document.cookie =
56
+ name +
57
+ '=' +
58
+ (value || '') +
59
+ expires +
60
+ '; path=/' +
61
+ '; Secure; SameSite=None';
62
+ }
63
+ function getCookie(name: string) {
64
+ const nameEQ = name + '=';
65
+ const ca = document.cookie.split(';');
66
+ for (let i = 0; i < ca.length; i++) {
67
+ let c = ca[i];
68
+ while (c.charAt(0) === ' ') c = c.substring(1, c.length);
69
+ if (c.indexOf(nameEQ) === 0)
70
+ return c.substring(nameEQ.length, c.length);
71
+ }
72
+ return null;
73
+ }
74
+ const cookieName = `builder.tests.${contentId}`;
75
+ const variantInCookie = getCookie(cookieName);
76
+ const availableIDs = variants.map((vr) => vr.id).concat(contentId);
77
+ /**
78
+ * cookie already exists
79
+ */
80
+ if (variantInCookie && availableIDs.includes(variantInCookie)) {
81
+ return variantInCookie;
82
+ }
83
+
84
+ /**
85
+ * no cookie exists, find variant
86
+ */
87
+ let n = 0;
88
+ const random = Math.random();
89
+ for (let i = 0; i < variants.length; i++) {
90
+ const variant = variants[i];
91
+ const testRatio = variant.testRatio;
92
+ n += testRatio!;
93
+ if (random < n) {
94
+ setCookie(cookieName, variant.id);
95
+ return variant.id;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * no variant found, assign default content
101
+ */
102
+ setCookie(cookieName, contentId);
103
+
104
+ return contentId;
105
+ }
106
+
107
+ const winningVariantId = getAndSetVariantId();
108
+
109
+ const styleEl = document.getElementById(
110
+ `variants-styles-${contentId}`
111
+ ) as HTMLStyleElement;
112
+
113
+ /**
114
+ * For React to work, we need hydration to match SSR, so we completely remove this node and the styles tag.
115
+ */
116
+ if (isHydrationTarget) {
117
+ styleEl.remove();
118
+ const thisScriptEl = document.getElementById(
119
+ `variants-script-${contentId}`
120
+ );
121
+ thisScriptEl?.remove();
122
+ } else {
123
+ /* update styles to hide all variants except the winning variant */
124
+ const newStyleStr = variants
125
+ .concat({ id: contentId })
126
+ .filter((variant) => variant.id !== winningVariantId)
127
+ .map((value) => {
128
+ return `.variant-${value.id} { display: none; }
129
+ `;
130
+ })
131
+ .join('');
132
+
133
+ /* TO-DO: check if this actually updates the style */
134
+ styleEl.innerHTML = newStyleStr;
135
+ }
136
+ }
137
+
138
+ /**
139
+ * NOTE: when this function is stringified, single-line comments can cause weird issues when compiled by Sveltekit.
140
+ * Make sure to write multi-line comments only.
141
+ */
142
+ function bldrCntntScrpt(
143
+ variantContentId: string,
144
+ defaultContentId: string,
145
+ isHydrationTarget: boolean
146
+ ) {
147
+ if (!navigator.cookieEnabled) {
148
+ return;
149
+ }
150
+
151
+ function getCookie(name: string) {
152
+ const nameEQ = name + '=';
153
+ const ca = document.cookie.split(';');
154
+ for (let i = 0; i < ca.length; i++) {
155
+ let c = ca[i];
156
+ while (c.charAt(0) === ' ') c = c.substring(1, c.length);
157
+ if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
158
+ }
159
+ return null;
160
+ }
161
+ const cookieName = `builder.tests.${defaultContentId}`;
162
+ const variantId = getCookie(cookieName);
163
+
164
+ /** get parent div by searching on `builder-content-id` attr */
165
+ const parentDiv = document.querySelector(
166
+ `[builder-content-id="${variantContentId}"]`
167
+ );
168
+
169
+ const variantIsDefaultContent = variantContentId === defaultContentId;
170
+
171
+ if (variantId === variantContentId) {
172
+ if (variantIsDefaultContent) {
173
+ /** the default content is already visible, no need to do anything */
174
+ return;
175
+ }
176
+
177
+ /** this is the winning variant and not already visible: remove `hidden` and `aria-hidden` attr */
178
+
179
+ parentDiv?.removeAttribute('hidden');
180
+ parentDiv?.removeAttribute('aria-hidden');
181
+ } else {
182
+ if (variantIsDefaultContent) {
183
+ if (isHydrationTarget) {
184
+ /**
185
+ * For React to work, we need to support hydration, in which case the first CSR will have none of the hidden variants.
186
+ * So we completely remove that node.
187
+ */
188
+ parentDiv?.remove();
189
+ } else {
190
+ /** this is not the winning variant, add `hidden` attr */
191
+ parentDiv?.setAttribute('hidden', 'true');
192
+ parentDiv?.setAttribute('aria-hidden', 'true');
193
+ }
194
+ }
195
+
196
+ /** This is not the winning variant, and it's not the default content.
197
+ * There's no need to hide it, because it's already hidden.
198
+ */
199
+ return;
200
+ }
201
+
202
+ return;
203
+ }
204
+
205
+ const isHydrationTarget = (target: Target) =>
206
+ target === 'react' ||
207
+ target === 'reactNative' ||
208
+ target === 'vue3' ||
209
+ target === 'vue2';
210
+
211
+ /**
212
+ * We hardcode explicit function names here, because the `.toString()` of a function can change depending on the bundler.
213
+ * Some bundlers will minify the fn name, etc.
214
+ *
215
+ * So we hardcode the function names here, and then use those names in the script string to make sure the function names are consistent.
216
+ */
217
+ const AB_TEST_FN_NAME = 'bldrAbTest';
218
+ const CONTENT_FN_NAME = 'bldrCntntScrpt';
219
+
220
+ export const getVariantsScriptString = (
221
+ variants: VariantData[],
222
+ contentId: string
223
+ ) => {
224
+ const fnStr = bldrAbTest.toString().replace(/\s+/g, ' ');
225
+ const fnStr2 = bldrCntntScrpt.toString().replace(/\s+/g, ' ');
226
+
227
+ return `
228
+ const ${AB_TEST_FN_NAME} = ${fnStr}
229
+ const ${CONTENT_FN_NAME} = ${fnStr2}
230
+ ${AB_TEST_FN_NAME}("${contentId}", ${JSON.stringify(
231
+ variants
232
+ )}, ${isHydrationTarget})
233
+ `;
234
+ };
235
+
236
+ export const getRenderContentScriptString = ({
237
+ parentContentId,
238
+ contentId,
239
+ }: {
240
+ contentId: string;
241
+ parentContentId: string;
242
+ }) => {
243
+ return `
244
+ ${CONTENT_FN_NAME}("${contentId}", "${parentContentId}", ${isHydrationTarget})`;
245
+ };
@@ -0,0 +1,83 @@
1
+ <script context="module" lang="ts">
2
+ type VariantsProviderProps = RenderContentProps;
3
+ </script>
4
+
5
+ <script lang="ts">
6
+ import {
7
+ checkShouldRunVariants,
8
+ getVariants,
9
+ getVariantsScriptString,
10
+ } from "./helpers";
11
+ import RenderContent from "../render-content/render-content.svelte";
12
+ import type { RenderContentProps } from "../render-content/render-content.types";
13
+ import { getDefaultCanTrack } from "../../helpers/canTrack";
14
+ import RenderInlinedStyles from "../render-inlined-styles.svelte";
15
+ import { handleABTestingSync } from "../../helpers/ab-tests";
16
+
17
+ export let content: VariantsProviderProps["content"];
18
+ export let canTrack: VariantsProviderProps["canTrack"];
19
+ export let apiKey: VariantsProviderProps["apiKey"];
20
+ export let apiVersion: VariantsProviderProps["apiVersion"];
21
+ export let customComponents: VariantsProviderProps["customComponents"];
22
+ export let model: VariantsProviderProps["model"];
23
+
24
+ let variantScriptStr = getVariantsScriptString(
25
+ getVariants(content).map((value) => ({
26
+ id: value.id!,
27
+ testRatio: value.testRatio,
28
+ })),
29
+ content?.id || ""
30
+ );
31
+ let shouldRenderVariants = checkShouldRunVariants({
32
+ canTrack: getDefaultCanTrack(canTrack),
33
+ content: content,
34
+ });
35
+ let hideVariantsStyleString = getVariants(content)
36
+ .map((value) => `.variant-${value.id} { display: none; } `)
37
+ .join("");
38
+ let contentToRender = checkShouldRunVariants({
39
+ canTrack: getDefaultCanTrack(canTrack),
40
+ content: content,
41
+ })
42
+ ? content
43
+ : handleABTestingSync({
44
+ item: content,
45
+ canTrack: getDefaultCanTrack(canTrack),
46
+ });
47
+ </script>
48
+
49
+ {#if shouldRenderVariants}
50
+ <RenderInlinedStyles
51
+ id={`variants-styles-${content?.id}`}
52
+ styles={hideVariantsStyleString}
53
+ />
54
+
55
+ <svelte:element this={"script"} id={`variants-script-${content?.id}`}
56
+ >{@html variantScriptStr}</svelte:element
57
+ >
58
+
59
+ {#each getVariants(content) as variant (variant.id)}
60
+ <RenderContent
61
+ content={variant}
62
+ {apiKey}
63
+ {apiVersion}
64
+ {canTrack}
65
+ {customComponents}
66
+ hideContent={true}
67
+ parentContentId={content?.id}
68
+ isSsrAbTest={shouldRenderVariants}
69
+ />
70
+ {/each}
71
+ {/if}
72
+
73
+ <RenderContent
74
+ {model}
75
+ content={contentToRender}
76
+ {apiKey}
77
+ {apiVersion}
78
+ {canTrack}
79
+ {customComponents}
80
+ classNameProp={`variant-${content?.id}`}
81
+ parentContentId={content?.id}
82
+ isSsrAbTest={shouldRenderVariants}
83
+ />
@@ -1,26 +1,13 @@
1
1
  <script context="module" lang="ts">
2
2
  interface Props {
3
3
  styles: string;
4
+ id?: string;
4
5
  }
5
6
  </script>
6
7
 
7
8
  <script lang="ts">
8
- import { TARGET } from "../constants/target.js";
9
-
10
9
  export let styles: Props["styles"];
11
-
12
- $: tag = () => {
13
- // NOTE: we have to obfuscate the name of the tag due to a limitation in the svelte-preprocessor plugin.
14
- // https://github.com/sveltejs/vite-plugin-svelte/issues/315#issuecomment-1109000027
15
- return ("sty" + "le") as any;
16
- };
17
- $: injectedStyleScript = () => {
18
- return `<${tag()}>${styles}</${tag()}>`;
19
- };
10
+ export let id: Props["id"];
20
11
  </script>
21
12
 
22
- {#if TARGET === "svelte" || TARGET === "qwik"}
23
- {@html injectedStyleScript()}
24
- {:else}
25
- <svelte:element this={tag()}>{styles}</svelte:element>
26
- {/if}
13
+ <svelte:element this={"sty" + "le"} {id}>{@html styles}</svelte:element>
@@ -1 +1 @@
1
- export const SDK_VERSION = "0.3.1"
1
+ export const SDK_VERSION = "0.4.0"
@@ -92,4 +92,21 @@ describe('Generate Content URL', () => {
92
92
  `Invalid apiVersion: expected 'v2' or 'v3', received 'INVALID_API_VERSION'`
93
93
  );
94
94
  });
95
+
96
+ test('generate content url with enrich option true', () => {
97
+ const output = generateContentUrl({
98
+ apiKey: testKey,
99
+ model: testModel,
100
+ enrich: true,
101
+ });
102
+ expect(output).toMatchSnapshot();
103
+ });
104
+
105
+ test('generate content url with enrich option not present', () => {
106
+ const output = generateContentUrl({
107
+ apiKey: testKey,
108
+ model: testModel,
109
+ });
110
+ expect(output).toMatchSnapshot();
111
+ });
95
112
  });
@@ -15,6 +15,7 @@ export const generateContentUrl = (options: GetContentOptions): URL => {
15
15
  model,
16
16
  apiKey,
17
17
  includeRefs = true,
18
+ enrich,
18
19
  locale,
19
20
  apiVersion = DEFAULT_API_VERSION,
20
21
  } = options;
@@ -32,7 +33,7 @@ export const generateContentUrl = (options: GetContentOptions): URL => {
32
33
  const url = new URL(
33
34
  `https://cdn.builder.io/api/${apiVersion}/content/${model}?apiKey=${apiKey}&limit=${limit}&noTraverse=${noTraverse}&includeRefs=${includeRefs}${
34
35
  locale ? `&locale=${locale}` : ''
35
- }`
36
+ }${enrich ? `&enrich=${enrich}` : ''}`
36
37
  );
37
38
 
38
39
  const queryOptions = {
@@ -1,60 +1,91 @@
1
+ import { TARGET } from '../../constants/target.js';
2
+ import { handleABTesting } from '../../helpers/ab-tests.js';
3
+ import { getDefaultCanTrack } from '../../helpers/canTrack.js';
1
4
  import { logger } from '../../helpers/logger.js';
2
5
  import type { BuilderContent } from '../../types/builder-content.js';
3
6
  import { fetch } from '../get-fetch.js';
4
- import { handleABTesting } from './ab-testing.js';
7
+ import { isBrowser } from '../is-browser.js';
5
8
  import { generateContentUrl } from './generate-content-url.js';
6
9
  import type { GetContentOptions } from './types.js';
7
10
 
11
+ const checkContentHasResults = (
12
+ content: ContentResponse
13
+ ): content is ContentResults => 'results' in content;
14
+
8
15
  export async function getContent(
9
16
  options: GetContentOptions
10
17
  ): Promise<BuilderContent | null> {
11
18
  const allContent = await getAllContent({ ...options, limit: 1 });
12
- if (allContent && 'results' in allContent) {
13
- return allContent?.results[0] || null;
19
+ if (allContent && checkContentHasResults(allContent)) {
20
+ return allContent.results[0] || null;
14
21
  }
15
22
 
16
23
  return null;
17
24
  }
18
25
 
26
+ type ContentResults = {
27
+ results: BuilderContent[];
28
+ };
29
+
19
30
  type ContentResponse =
20
- | {
21
- results: BuilderContent[];
22
- }
31
+ | ContentResults
23
32
  | {
24
33
  status: number;
25
34
  message: string;
26
35
  };
27
36
 
37
+ const fetchContent = async (options: GetContentOptions) => {
38
+ const url = generateContentUrl(options);
39
+
40
+ const res = await fetch(url.href);
41
+ const content = await (res.json() as Promise<ContentResponse>);
42
+ return content;
43
+ };
44
+
45
+ /**
46
+ * Exported only for testing purposes. Should not be used directly.
47
+ */
48
+ export const processContentResult = async (
49
+ options: GetContentOptions,
50
+ content: ContentResults
51
+ ) => {
52
+ const canTrack = getDefaultCanTrack(options.canTrack);
53
+
54
+ if (!canTrack) return content;
55
+ if (!(isBrowser() || TARGET === 'reactNative')) return content;
56
+
57
+ /**
58
+ * For client-side navigations, it is ideal to handle AB testing at this point instead of using our
59
+ * complex multi-rendering variants approach, which is only needed for SSR'd content.
60
+ *
61
+ * This is also where react-native would handle AB testing.
62
+ */
63
+ try {
64
+ const newResults: BuilderContent[] = [];
65
+ for (const item of content.results) {
66
+ newResults.push(await handleABTesting({ item, canTrack }));
67
+ }
68
+ content.results = newResults;
69
+ } catch (e) {
70
+ logger.error('Could not process A/B tests. ', e);
71
+ }
72
+
73
+ return content;
74
+ };
75
+
28
76
  export async function getAllContent(
29
77
  options: GetContentOptions
30
78
  ): Promise<ContentResponse | null> {
31
79
  try {
32
80
  const url = generateContentUrl(options);
81
+ const content = await fetchContent(options);
33
82
 
34
- const res = await fetch(url.href);
35
- const content = await (res.json() as Promise<ContentResponse>);
36
-
37
- if ('status' in content && !('results' in content)) {
83
+ if (!checkContentHasResults(content)) {
38
84
  logger.error('Error fetching data. ', { url, content, options });
39
85
  return content;
40
86
  }
41
87
 
42
- const canTrack = options.canTrack !== false;
43
- try {
44
- if (
45
- canTrack &&
46
- // This makes sure we have a non-error response with the results array.
47
- Array.isArray(content.results)
48
- ) {
49
- for (const item of content.results) {
50
- await handleABTesting({ item, canTrack });
51
- }
52
- }
53
- } catch (e) {
54
- logger.error('Could not setup A/B testing. ', e);
55
- }
56
-
57
- return content;
88
+ return processContentResult(options, content);
58
89
  } catch (error) {
59
90
  logger.error('Error fetching data. ', error);
60
91
  return null;
@@ -23,6 +23,7 @@ export interface GetContentOptions {
23
23
  /**
24
24
  * If set to `true`, it will lazy load symbols/references.
25
25
  * If set to `false`, it will render the entire content tree eagerly.
26
+ * @deprecated use `enrich` instead
26
27
  */
27
28
  noTraverse?: boolean;
28
29
 
@@ -36,9 +37,15 @@ export interface GetContentOptions {
36
37
 
37
38
  /**
38
39
  * Include references in the response. Defaults to `true`.
40
+ * @deprecated use `enrich` instead
39
41
  */
40
42
  includeRefs?: boolean;
41
43
 
44
+ /**
45
+ * Include multilevel references in the response.
46
+ */
47
+ enrich?: boolean;
48
+
42
49
  /**
43
50
  * If provided, the API will auto-resolve localized objects to the value of this `locale` key.
44
51
  */