@builder.io/sdk-react 0.2.3 → 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 +72 -3
- package/dist/sdk/blocks/BaseText.js +1 -1
- package/dist/sdk/blocks/button/button.js +1 -1
- package/dist/sdk/blocks/columns/columns.js +6 -6
- package/dist/sdk/blocks/custom-code/custom-code.js +1 -1
- package/dist/sdk/blocks/embed/embed.js +1 -1
- package/dist/sdk/blocks/fragment/fragment.js +1 -1
- package/dist/sdk/blocks/image/image.js +1 -1
- package/dist/sdk/blocks/img/img.js +1 -1
- package/dist/sdk/blocks/input/input.js +1 -1
- package/dist/sdk/blocks/raw-text/raw-text.js +1 -1
- package/dist/sdk/blocks/section/section.js +1 -1
- package/dist/sdk/blocks/select/select.js +1 -1
- package/dist/sdk/blocks/submit-button/submit-button.js +1 -1
- package/dist/sdk/blocks/symbol/symbol.js +2 -2
- package/dist/sdk/blocks/text/text.js +1 -1
- package/dist/sdk/blocks/textarea/textarea.js +1 -1
- package/dist/sdk/blocks/video/video.js +1 -1
- package/dist/sdk/components/render-block/block-styles.js +4 -2
- package/dist/sdk/components/render-block/render-block.helpers.d.ts +0 -1
- package/dist/sdk/components/render-block/render-block.helpers.js +8 -20
- package/dist/sdk/components/render-block/render-block.js +25 -20
- package/dist/sdk/components/render-block/render-component.js +2 -3
- package/dist/sdk/components/render-block/render-repeated-block.js +4 -3
- package/dist/sdk/components/render-blocks.js +1 -1
- package/dist/sdk/components/render-content/builder-editing.js +1 -1
- package/dist/sdk/components/render-content/components/render-styles.js +2 -2
- package/dist/sdk/components/render-content/render-content.js +45 -11
- package/dist/sdk/components/render-content/render-content.types.d.ts +11 -2
- package/dist/sdk/components/render-content/wrap-component-ref.d.ts +6 -0
- package/dist/sdk/components/render-content/wrap-component-ref.js +6 -0
- package/dist/sdk/components/render-content-variants/helpers.d.ts +17 -0
- package/dist/sdk/components/render-content-variants/helpers.js +182 -0
- package/dist/sdk/components/render-content-variants/render-content-variants.d.ts +5 -0
- package/dist/sdk/components/render-content-variants/render-content-variants.js +37 -0
- package/dist/sdk/components/render-inlined-styles.d.ts +1 -0
- package/dist/sdk/components/render-inlined-styles.js +2 -13
- package/dist/sdk/constants/sdk-version.d.ts +1 -0
- package/dist/sdk/constants/sdk-version.js +1 -0
- package/dist/sdk/context/builder.context.js +3 -2
- package/dist/sdk/context/types.d.ts +17 -2
- package/dist/sdk/functions/evaluate.d.ts +4 -3
- package/dist/sdk/functions/evaluate.js +23 -2
- package/dist/sdk/functions/evaluate.test.d.ts +1 -0
- package/dist/sdk/functions/evaluate.test.js +17 -0
- package/dist/sdk/functions/get-block-actions-handler.d.ts +1 -1
- package/dist/sdk/functions/get-block-actions-handler.js +3 -1
- package/dist/sdk/functions/get-block-actions.d.ts +1 -1
- package/dist/sdk/functions/get-content/generate-content-url.js +2 -2
- package/dist/sdk/functions/get-content/generate-content-url.test.js +15 -0
- package/dist/sdk/functions/get-content/index.d.ts +7 -2
- package/dist/sdk/functions/get-content/index.js +43 -20
- package/dist/sdk/functions/get-content/types.d.ts +6 -0
- package/dist/sdk/functions/get-processed-block.d.ts +2 -2
- package/dist/sdk/functions/get-processed-block.js +16 -4
- package/dist/sdk/functions/get-processed-block.test.js +3 -1
- package/dist/sdk/helpers/ab-tests.d.ts +8 -7
- package/dist/sdk/helpers/ab-tests.js +103 -4
- package/dist/sdk/helpers/canTrack.d.ts +1 -0
- package/dist/sdk/helpers/canTrack.js +2 -0
- package/dist/sdk/helpers/cookie.d.ts +7 -3
- package/dist/sdk/helpers/cookie.js +9 -6
- package/dist/sdk/helpers/logger.d.ts +1 -0
- package/dist/sdk/helpers/logger.js +1 -0
- package/dist/sdk/index-helpers/blocks-exports.d.ts +1 -1
- package/dist/sdk/index-helpers/blocks-exports.js +1 -1
- package/dist/sdk/index.d.ts +8 -7
- package/dist/sdk/index.js +6 -7
- package/dist/sdk/scripts/init-editing.d.ts +1 -0
- package/dist/sdk/scripts/init-editing.js +2 -0
- package/dist/sdk/types/builder-content.d.ts +1 -3
- package/package.json +2 -5
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Nullable } from '../../helpers/nullable';
|
|
2
|
+
import type { BuilderContent } from '../../types/builder-content';
|
|
3
|
+
export declare const getVariants: (content: Nullable<BuilderContent>) => import("../../types/builder-content").BuilderContentVariation[];
|
|
4
|
+
export declare const checkShouldRunVariants: ({ canTrack, content, }: {
|
|
5
|
+
canTrack: Nullable<boolean>;
|
|
6
|
+
content: Nullable<BuilderContent>;
|
|
7
|
+
}) => boolean;
|
|
8
|
+
type VariantData = {
|
|
9
|
+
id: string;
|
|
10
|
+
testRatio?: number;
|
|
11
|
+
};
|
|
12
|
+
export declare const getVariantsScriptString: (variants: VariantData[], contentId: string) => string;
|
|
13
|
+
export declare const getRenderContentScriptString: ({ parentContentId, contentId, }: {
|
|
14
|
+
contentId: string;
|
|
15
|
+
parentContentId: string;
|
|
16
|
+
}) => string;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { isBrowser } from '../../functions/is-browser';
|
|
2
|
+
export const getVariants = (content) => Object.values(content?.variations || {});
|
|
3
|
+
export const checkShouldRunVariants = ({ canTrack, content, }) => {
|
|
4
|
+
const hasVariants = getVariants(content).length > 0;
|
|
5
|
+
if (!hasVariants) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
if (!canTrack) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
if (isBrowser()) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
return true;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* NOTE: when this function is stringified, single-line comments can cause weird issues when compiled by Sveltekit.
|
|
18
|
+
* Make sure to write multi-line comments only.
|
|
19
|
+
*/
|
|
20
|
+
function bldrAbTest(contentId, variants, isHydrationTarget) {
|
|
21
|
+
function getAndSetVariantId() {
|
|
22
|
+
function setCookie(name, value, days) {
|
|
23
|
+
let expires = '';
|
|
24
|
+
if (days) {
|
|
25
|
+
const date = new Date();
|
|
26
|
+
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
|
|
27
|
+
expires = '; expires=' + date.toUTCString();
|
|
28
|
+
}
|
|
29
|
+
document.cookie =
|
|
30
|
+
name +
|
|
31
|
+
'=' +
|
|
32
|
+
(value || '') +
|
|
33
|
+
expires +
|
|
34
|
+
'; path=/' +
|
|
35
|
+
'; Secure; SameSite=None';
|
|
36
|
+
}
|
|
37
|
+
function getCookie(name) {
|
|
38
|
+
const nameEQ = name + '=';
|
|
39
|
+
const ca = document.cookie.split(';');
|
|
40
|
+
for (let i = 0; i < ca.length; i++) {
|
|
41
|
+
let c = ca[i];
|
|
42
|
+
while (c.charAt(0) === ' ')
|
|
43
|
+
c = c.substring(1, c.length);
|
|
44
|
+
if (c.indexOf(nameEQ) === 0)
|
|
45
|
+
return c.substring(nameEQ.length, c.length);
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const cookieName = `builder.tests.${contentId}`;
|
|
50
|
+
const variantInCookie = getCookie(cookieName);
|
|
51
|
+
const availableIDs = variants.map((vr) => vr.id).concat(contentId);
|
|
52
|
+
/**
|
|
53
|
+
* cookie already exists
|
|
54
|
+
*/
|
|
55
|
+
if (variantInCookie && availableIDs.includes(variantInCookie)) {
|
|
56
|
+
return variantInCookie;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* no cookie exists, find variant
|
|
60
|
+
*/
|
|
61
|
+
let n = 0;
|
|
62
|
+
const random = Math.random();
|
|
63
|
+
for (let i = 0; i < variants.length; i++) {
|
|
64
|
+
const variant = variants[i];
|
|
65
|
+
const testRatio = variant.testRatio;
|
|
66
|
+
n += testRatio;
|
|
67
|
+
if (random < n) {
|
|
68
|
+
setCookie(cookieName, variant.id);
|
|
69
|
+
return variant.id;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* no variant found, assign default content
|
|
74
|
+
*/
|
|
75
|
+
setCookie(cookieName, contentId);
|
|
76
|
+
return contentId;
|
|
77
|
+
}
|
|
78
|
+
const winningVariantId = getAndSetVariantId();
|
|
79
|
+
const styleEl = document.getElementById(`variants-styles-${contentId}`);
|
|
80
|
+
/**
|
|
81
|
+
* For React to work, we need hydration to match SSR, so we completely remove this node and the styles tag.
|
|
82
|
+
*/
|
|
83
|
+
if (isHydrationTarget) {
|
|
84
|
+
styleEl.remove();
|
|
85
|
+
const thisScriptEl = document.getElementById(`variants-script-${contentId}`);
|
|
86
|
+
thisScriptEl?.remove();
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
/* update styles to hide all variants except the winning variant */
|
|
90
|
+
const newStyleStr = variants
|
|
91
|
+
.concat({ id: contentId })
|
|
92
|
+
.filter((variant) => variant.id !== winningVariantId)
|
|
93
|
+
.map((value) => {
|
|
94
|
+
return `.variant-${value.id} { display: none; }
|
|
95
|
+
`;
|
|
96
|
+
})
|
|
97
|
+
.join('');
|
|
98
|
+
/* TO-DO: check if this actually updates the style */
|
|
99
|
+
styleEl.innerHTML = newStyleStr;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* NOTE: when this function is stringified, single-line comments can cause weird issues when compiled by Sveltekit.
|
|
104
|
+
* Make sure to write multi-line comments only.
|
|
105
|
+
*/
|
|
106
|
+
function bldrCntntScrpt(variantContentId, defaultContentId, isHydrationTarget) {
|
|
107
|
+
if (!navigator.cookieEnabled) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
function getCookie(name) {
|
|
111
|
+
const nameEQ = name + '=';
|
|
112
|
+
const ca = document.cookie.split(';');
|
|
113
|
+
for (let i = 0; i < ca.length; i++) {
|
|
114
|
+
let c = ca[i];
|
|
115
|
+
while (c.charAt(0) === ' ')
|
|
116
|
+
c = c.substring(1, c.length);
|
|
117
|
+
if (c.indexOf(nameEQ) === 0)
|
|
118
|
+
return c.substring(nameEQ.length, c.length);
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
const cookieName = `builder.tests.${defaultContentId}`;
|
|
123
|
+
const variantId = getCookie(cookieName);
|
|
124
|
+
/** get parent div by searching on `builder-content-id` attr */
|
|
125
|
+
const parentDiv = document.querySelector(`[builder-content-id="${variantContentId}"]`);
|
|
126
|
+
const variantIsDefaultContent = variantContentId === defaultContentId;
|
|
127
|
+
if (variantId === variantContentId) {
|
|
128
|
+
if (variantIsDefaultContent) {
|
|
129
|
+
/** the default content is already visible, no need to do anything */
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
/** this is the winning variant and not already visible: remove `hidden` and `aria-hidden` attr */
|
|
133
|
+
parentDiv?.removeAttribute('hidden');
|
|
134
|
+
parentDiv?.removeAttribute('aria-hidden');
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
if (variantIsDefaultContent) {
|
|
138
|
+
if (isHydrationTarget) {
|
|
139
|
+
/**
|
|
140
|
+
* For React to work, we need to support hydration, in which case the first CSR will have none of the hidden variants.
|
|
141
|
+
* So we completely remove that node.
|
|
142
|
+
*/
|
|
143
|
+
parentDiv?.remove();
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
/** this is not the winning variant, add `hidden` attr */
|
|
147
|
+
parentDiv?.setAttribute('hidden', 'true');
|
|
148
|
+
parentDiv?.setAttribute('aria-hidden', 'true');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** This is not the winning variant, and it's not the default content.
|
|
152
|
+
* There's no need to hide it, because it's already hidden.
|
|
153
|
+
*/
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const isHydrationTarget = (target) => target === 'react' ||
|
|
159
|
+
target === 'reactNative' ||
|
|
160
|
+
target === 'vue3' ||
|
|
161
|
+
target === 'vue2';
|
|
162
|
+
/**
|
|
163
|
+
* We hardcode explicit function names here, because the `.toString()` of a function can change depending on the bundler.
|
|
164
|
+
* Some bundlers will minify the fn name, etc.
|
|
165
|
+
*
|
|
166
|
+
* So we hardcode the function names here, and then use those names in the script string to make sure the function names are consistent.
|
|
167
|
+
*/
|
|
168
|
+
const AB_TEST_FN_NAME = 'bldrAbTest';
|
|
169
|
+
const CONTENT_FN_NAME = 'bldrCntntScrpt';
|
|
170
|
+
export const getVariantsScriptString = (variants, contentId) => {
|
|
171
|
+
const fnStr = bldrAbTest.toString().replace(/\s+/g, ' ');
|
|
172
|
+
const fnStr2 = bldrCntntScrpt.toString().replace(/\s+/g, ' ');
|
|
173
|
+
return `
|
|
174
|
+
const ${AB_TEST_FN_NAME} = ${fnStr}
|
|
175
|
+
const ${CONTENT_FN_NAME} = ${fnStr2}
|
|
176
|
+
${AB_TEST_FN_NAME}("${contentId}", ${JSON.stringify(variants)}, ${isHydrationTarget})
|
|
177
|
+
`;
|
|
178
|
+
};
|
|
179
|
+
export const getRenderContentScriptString = ({ parentContentId, contentId, }) => {
|
|
180
|
+
return `
|
|
181
|
+
${CONTENT_FN_NAME}("${contentId}", "${parentContentId}", ${isHydrationTarget})`;
|
|
182
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
type VariantsProviderProps = RenderContentProps;
|
|
3
|
+
import type { RenderContentProps } from "../render-content/render-content.types";
|
|
4
|
+
declare function RenderContentVariants(props: VariantsProviderProps): JSX.Element;
|
|
5
|
+
export default RenderContentVariants;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { checkShouldRunVariants, getVariants, getVariantsScriptString, } from "./helpers";
|
|
5
|
+
import RenderContent from "../render-content/render-content";
|
|
6
|
+
import { getDefaultCanTrack } from "../../helpers/canTrack";
|
|
7
|
+
import RenderInlinedStyles from "../render-inlined-styles";
|
|
8
|
+
import { handleABTestingSync } from "../../helpers/ab-tests";
|
|
9
|
+
function RenderContentVariants(props) {
|
|
10
|
+
const [variantScriptStr, setVariantScriptStr] = useState(() => getVariantsScriptString(getVariants(props.content).map((value) => ({
|
|
11
|
+
id: value.id,
|
|
12
|
+
testRatio: value.testRatio,
|
|
13
|
+
})), props.content?.id || ""));
|
|
14
|
+
const [shouldRenderVariants, setShouldRenderVariants] = useState(() => checkShouldRunVariants({
|
|
15
|
+
canTrack: getDefaultCanTrack(props.canTrack),
|
|
16
|
+
content: props.content,
|
|
17
|
+
}));
|
|
18
|
+
const [hideVariantsStyleString, setHideVariantsStyleString] = useState(() => getVariants(props.content)
|
|
19
|
+
.map((value) => `.variant-${value.id} { display: none; } `)
|
|
20
|
+
.join(""));
|
|
21
|
+
const [contentToRender, setContentToRender] = useState(() => checkShouldRunVariants({
|
|
22
|
+
canTrack: getDefaultCanTrack(props.canTrack),
|
|
23
|
+
content: props.content,
|
|
24
|
+
})
|
|
25
|
+
? props.content
|
|
26
|
+
: handleABTestingSync({
|
|
27
|
+
item: props.content,
|
|
28
|
+
canTrack: getDefaultCanTrack(props.canTrack),
|
|
29
|
+
}));
|
|
30
|
+
return (React.createElement(React.Fragment, null,
|
|
31
|
+
shouldRenderVariants ? (React.createElement(React.Fragment, null,
|
|
32
|
+
React.createElement(RenderInlinedStyles, { id: `variants-styles-${props.content?.id}`, styles: hideVariantsStyleString }),
|
|
33
|
+
React.createElement("script", { id: `variants-script-${props.content?.id}`, dangerouslySetInnerHTML: { __html: variantScriptStr } }),
|
|
34
|
+
getVariants(props.content)?.map((variant) => (React.createElement(RenderContent, { key: variant.id, content: variant, apiKey: props.apiKey, apiVersion: props.apiVersion, canTrack: props.canTrack, customComponents: props.customComponents, hideContent: true, parentContentId: props.content?.id, isSsrAbTest: shouldRenderVariants }))))) : null,
|
|
35
|
+
React.createElement(RenderContent, { model: props.model, content: contentToRender, apiKey: props.apiKey, apiVersion: props.apiVersion, canTrack: props.canTrack, customComponents: props.customComponents, classNameProp: `variant-${props.content?.id}`, parentContentId: props.content?.id, isSsrAbTest: shouldRenderVariants })));
|
|
36
|
+
}
|
|
37
|
+
export default RenderContentVariants;
|
|
@@ -1,17 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
'use client';
|
|
2
2
|
import * as React from "react";
|
|
3
|
-
import { TARGET } from "../constants/target.js";
|
|
4
3
|
function RenderInlinedStyles(props) {
|
|
5
|
-
|
|
6
|
-
// NOTE: we have to obfusctate the name of the tag due to a limitation in the svelte-preprocessor plugin.
|
|
7
|
-
// https://github.com/sveltejs/vite-plugin-svelte/issues/315#issuecomment-1109000027
|
|
8
|
-
return "style";
|
|
9
|
-
}
|
|
10
|
-
function injectedStyleScript() {
|
|
11
|
-
return `<${tag()}>${props.styles}</${tag()}>`;
|
|
12
|
-
}
|
|
13
|
-
const TagRef = tag();
|
|
14
|
-
return (React.createElement(React.Fragment, null, TARGET === "svelte" || TARGET === "qwik" ? (React.createElement(React.Fragment, null,
|
|
15
|
-
React.createElement(TagRef, { dangerouslySetInnerHTML: { __html: props.styles } }))) : (React.createElement(TagRef, null, props.styles))));
|
|
4
|
+
return (React.createElement("style", { dangerouslySetInnerHTML: { __html: props.styles }, id: props.id }));
|
|
16
5
|
}
|
|
17
6
|
export default RenderInlinedStyles;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const SDK_VERSION = "0.4.0";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const SDK_VERSION = "0.4.0";
|
|
@@ -2,8 +2,9 @@ import { createContext } from "react";
|
|
|
2
2
|
export default createContext({
|
|
3
3
|
content: null,
|
|
4
4
|
context: {},
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
localState: undefined,
|
|
6
|
+
rootSetState() { },
|
|
7
|
+
rootState: {},
|
|
7
8
|
apiKey: null,
|
|
8
9
|
apiVersion: undefined,
|
|
9
10
|
registeredComponents: {},
|
|
@@ -11,8 +11,23 @@ export type BuilderRenderContext = Record<string, unknown>;
|
|
|
11
11
|
export interface BuilderContextInterface {
|
|
12
12
|
content: Nullable<BuilderContent>;
|
|
13
13
|
context: BuilderRenderContext;
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* The state of the application.
|
|
16
|
+
*
|
|
17
|
+
* NOTE: see `localState` below to understand how it is different from `rootState`.
|
|
18
|
+
*/
|
|
19
|
+
rootState: BuilderRenderState;
|
|
20
|
+
/**
|
|
21
|
+
* Some frameworks have a `setState` function which needs to be invoked to notify
|
|
22
|
+
* the framework of state change. (other frameworks don't in which case it is `undefined')
|
|
23
|
+
*/
|
|
24
|
+
rootSetState: ((rootState: BuilderRenderState) => void) | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* The local state of the current component. This is different from `rootState` in that
|
|
27
|
+
* it can be a child state created by a repeater containing local state.
|
|
28
|
+
* The `rootState` is where all of the state mutations are actually stored.
|
|
29
|
+
*/
|
|
30
|
+
localState: BuilderRenderState | undefined;
|
|
16
31
|
apiKey: string | null;
|
|
17
32
|
apiVersion: ApiVersion | undefined;
|
|
18
33
|
registeredComponents: RegisteredComponents;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { BuilderContextInterface } from '../context/types.js';
|
|
2
|
-
export declare function evaluate({ code, context,
|
|
1
|
+
import type { BuilderContextInterface, BuilderRenderState } from '../context/types.js';
|
|
2
|
+
export declare function evaluate({ code, context, localState, rootState, rootSetState, event, isExpression, }: {
|
|
3
3
|
code: string;
|
|
4
4
|
event?: Event;
|
|
5
5
|
isExpression?: boolean;
|
|
6
|
-
} & Pick<BuilderContextInterface, '
|
|
6
|
+
} & Pick<BuilderContextInterface, 'localState' | 'context' | 'rootState' | 'rootSetState'>): any;
|
|
7
|
+
export declare function flattenState(rootState: Record<string | symbol, any>, localState: Record<string | symbol, any> | undefined, rootSetState: ((rootState: BuilderRenderState) => void) | undefined): BuilderRenderState;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isBrowser } from './is-browser.js';
|
|
2
2
|
import { isEditing } from './is-editing.js';
|
|
3
|
-
export function evaluate({ code, context,
|
|
3
|
+
export function evaluate({ code, context, localState, rootState, rootSetState, event, isExpression = true, }) {
|
|
4
4
|
if (code === '') {
|
|
5
5
|
console.warn('Skipping evaluation of empty code block.');
|
|
6
6
|
return;
|
|
@@ -20,9 +20,30 @@ export function evaluate({ code, context, state, event, isExpression = true, })
|
|
|
20
20
|
code.trim().startsWith('return '));
|
|
21
21
|
const useCode = useReturn ? `return (${code});` : code;
|
|
22
22
|
try {
|
|
23
|
-
return new Function('builder', 'Builder' /* <- legacy */, 'state', 'context', 'event', useCode)(builder, builder,
|
|
23
|
+
return new Function('builder', 'Builder' /* <- legacy */, 'state', 'context', 'event', useCode)(builder, builder, flattenState(rootState, localState, rootSetState), context, event);
|
|
24
24
|
}
|
|
25
25
|
catch (e) {
|
|
26
26
|
console.warn('Builder custom code error: \n While Evaluating: \n ', useCode, '\n', e);
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
+
export function flattenState(rootState, localState, rootSetState) {
|
|
30
|
+
if (rootState === localState) {
|
|
31
|
+
throw new Error('rootState === localState');
|
|
32
|
+
}
|
|
33
|
+
return new Proxy(rootState, {
|
|
34
|
+
get: (_, prop) => {
|
|
35
|
+
if (localState && prop in localState) {
|
|
36
|
+
return localState[prop];
|
|
37
|
+
}
|
|
38
|
+
return rootState[prop];
|
|
39
|
+
},
|
|
40
|
+
set: (_, prop, value) => {
|
|
41
|
+
if (localState && prop in localState) {
|
|
42
|
+
throw new Error('Writing to local state is not allowed as it is read-only.');
|
|
43
|
+
}
|
|
44
|
+
rootState[prop] = value;
|
|
45
|
+
rootSetState?.(rootState);
|
|
46
|
+
return true;
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { flattenState } from './evaluate';
|
|
2
|
+
describe('flatten state', () => {
|
|
3
|
+
it('should behave normally when no PROTO_STATE', () => {
|
|
4
|
+
const localState = {};
|
|
5
|
+
const rootState = { foo: 'bar' };
|
|
6
|
+
const flattened = flattenState(rootState, localState, undefined);
|
|
7
|
+
expect(flattened.foo).toEqual('bar');
|
|
8
|
+
flattened.foo = 'baz';
|
|
9
|
+
expect(rootState.foo).toEqual('baz');
|
|
10
|
+
});
|
|
11
|
+
it('should shadow write ', () => {
|
|
12
|
+
const rootState = { foo: 'foo' };
|
|
13
|
+
const localState = { foo: 'baz' };
|
|
14
|
+
const flattened = flattenState(rootState, localState, undefined);
|
|
15
|
+
expect(() => (flattened.foo = 'bar')).toThrow('Writing to local state is not allowed as it is read-only.');
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -2,7 +2,7 @@ import type { BuilderContextInterface } from '../context/types.js';
|
|
|
2
2
|
import type { BuilderBlock } from '../types/builder-block.js';
|
|
3
3
|
type Options = {
|
|
4
4
|
block: BuilderBlock;
|
|
5
|
-
} & Pick<BuilderContextInterface, '
|
|
5
|
+
} & Pick<BuilderContextInterface, 'localState' | 'context' | 'rootState' | 'rootSetState'>;
|
|
6
6
|
type EventHandler = (event: Event) => any;
|
|
7
7
|
export declare const createEventHandler: (value: string, options: Options) => EventHandler;
|
|
8
8
|
export {};
|
|
@@ -2,7 +2,9 @@ import { evaluate } from './evaluate.js';
|
|
|
2
2
|
export const createEventHandler = (value, options) => (event) => evaluate({
|
|
3
3
|
code: value,
|
|
4
4
|
context: options.context,
|
|
5
|
-
|
|
5
|
+
localState: options.localState,
|
|
6
|
+
rootState: options.rootState,
|
|
7
|
+
rootSetState: options.rootSetState,
|
|
6
8
|
event,
|
|
7
9
|
isExpression: false,
|
|
8
10
|
});
|
|
@@ -5,5 +5,5 @@ type Actions = {
|
|
|
5
5
|
};
|
|
6
6
|
export declare function getBlockActions(options: {
|
|
7
7
|
block: BuilderBlock;
|
|
8
|
-
} & Pick<BuilderContextInterface, '
|
|
8
|
+
} & Pick<BuilderContextInterface, 'localState' | 'context' | 'rootState' | 'rootSetState'>): Actions;
|
|
9
9
|
export {};
|
|
@@ -2,14 +2,14 @@ import { flatten } from '../../helpers/flatten.js';
|
|
|
2
2
|
import { getBuilderSearchParamsFromWindow, normalizeSearchParams, } from '../get-builder-search-params/index.js';
|
|
3
3
|
import { DEFAULT_API_VERSION } from '../../types/api-version';
|
|
4
4
|
export const generateContentUrl = (options) => {
|
|
5
|
-
const { limit = 30, userAttributes, query, noTraverse = false, model, apiKey, includeRefs = true, locale, apiVersion = DEFAULT_API_VERSION, } = options;
|
|
5
|
+
const { limit = 30, userAttributes, query, noTraverse = false, model, apiKey, includeRefs = true, enrich, locale, apiVersion = DEFAULT_API_VERSION, } = options;
|
|
6
6
|
if (!apiKey) {
|
|
7
7
|
throw new Error('Missing API key');
|
|
8
8
|
}
|
|
9
9
|
if (!['v2', 'v3'].includes(apiVersion)) {
|
|
10
10
|
throw new Error(`Invalid apiVersion: expected 'v2' or 'v3', received '${apiVersion}'`);
|
|
11
11
|
}
|
|
12
|
-
const url = new URL(`https://cdn.builder.io/api/${apiVersion}/content/${model}?apiKey=${apiKey}&limit=${limit}&noTraverse=${noTraverse}&includeRefs=${includeRefs}${locale ? `&locale=${locale}` : ''}`);
|
|
12
|
+
const url = new URL(`https://cdn.builder.io/api/${apiVersion}/content/${model}?apiKey=${apiKey}&limit=${limit}&noTraverse=${noTraverse}&includeRefs=${includeRefs}${locale ? `&locale=${locale}` : ''}${enrich ? `&enrich=${enrich}` : ''}`);
|
|
13
13
|
const queryOptions = {
|
|
14
14
|
...getBuilderSearchParamsFromWindow(),
|
|
15
15
|
...normalizeSearchParams(options.options || {}),
|
|
@@ -79,4 +79,19 @@ describe('Generate Content URL', () => {
|
|
|
79
79
|
});
|
|
80
80
|
}).toThrow(`Invalid apiVersion: expected 'v2' or 'v3', received 'INVALID_API_VERSION'`);
|
|
81
81
|
});
|
|
82
|
+
test('generate content url with enrich option true', () => {
|
|
83
|
+
const output = generateContentUrl({
|
|
84
|
+
apiKey: testKey,
|
|
85
|
+
model: testModel,
|
|
86
|
+
enrich: true,
|
|
87
|
+
});
|
|
88
|
+
expect(output).toMatchSnapshot();
|
|
89
|
+
});
|
|
90
|
+
test('generate content url with enrich option not present', () => {
|
|
91
|
+
const output = generateContentUrl({
|
|
92
|
+
apiKey: testKey,
|
|
93
|
+
model: testModel,
|
|
94
|
+
});
|
|
95
|
+
expect(output).toMatchSnapshot();
|
|
96
|
+
});
|
|
82
97
|
});
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import type { BuilderContent } from '../../types/builder-content.js';
|
|
2
2
|
import type { GetContentOptions } from './types.js';
|
|
3
3
|
export declare function getContent(options: GetContentOptions): Promise<BuilderContent | null>;
|
|
4
|
-
type
|
|
4
|
+
type ContentResults = {
|
|
5
5
|
results: BuilderContent[];
|
|
6
|
-
}
|
|
6
|
+
};
|
|
7
|
+
type ContentResponse = ContentResults | {
|
|
7
8
|
status: number;
|
|
8
9
|
message: string;
|
|
9
10
|
};
|
|
11
|
+
/**
|
|
12
|
+
* Exported only for testing purposes. Should not be used directly.
|
|
13
|
+
*/
|
|
14
|
+
export declare const processContentResult: (options: GetContentOptions, content: ContentResults) => Promise<ContentResults>;
|
|
10
15
|
export declare function getAllContent(options: GetContentOptions): Promise<ContentResponse | null>;
|
|
11
16
|
export {};
|
|
@@ -1,37 +1,60 @@
|
|
|
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 { fetch } from '../get-fetch.js';
|
|
3
|
-
import {
|
|
6
|
+
import { isBrowser } from '../is-browser.js';
|
|
4
7
|
import { generateContentUrl } from './generate-content-url.js';
|
|
8
|
+
const checkContentHasResults = (content) => 'results' in content;
|
|
5
9
|
export async function getContent(options) {
|
|
6
10
|
const allContent = await getAllContent({ ...options, limit: 1 });
|
|
7
|
-
if (allContent &&
|
|
8
|
-
return allContent
|
|
11
|
+
if (allContent && checkContentHasResults(allContent)) {
|
|
12
|
+
return allContent.results[0] || null;
|
|
9
13
|
}
|
|
10
14
|
return null;
|
|
11
15
|
}
|
|
16
|
+
const fetchContent = async (options) => {
|
|
17
|
+
const url = generateContentUrl(options);
|
|
18
|
+
const res = await fetch(url.href);
|
|
19
|
+
const content = await res.json();
|
|
20
|
+
return content;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Exported only for testing purposes. Should not be used directly.
|
|
24
|
+
*/
|
|
25
|
+
export const processContentResult = async (options, content) => {
|
|
26
|
+
const canTrack = getDefaultCanTrack(options.canTrack);
|
|
27
|
+
if (!canTrack)
|
|
28
|
+
return content;
|
|
29
|
+
if (!(isBrowser() || TARGET === 'reactNative'))
|
|
30
|
+
return content;
|
|
31
|
+
/**
|
|
32
|
+
* For client-side navigations, it is ideal to handle AB testing at this point instead of using our
|
|
33
|
+
* complex multi-rendering variants approach, which is only needed for SSR'd content.
|
|
34
|
+
*
|
|
35
|
+
* This is also where react-native would handle AB testing.
|
|
36
|
+
*/
|
|
37
|
+
try {
|
|
38
|
+
const newResults = [];
|
|
39
|
+
for (const item of content.results) {
|
|
40
|
+
newResults.push(await handleABTesting({ item, canTrack }));
|
|
41
|
+
}
|
|
42
|
+
content.results = newResults;
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
logger.error('Could not process A/B tests. ', e);
|
|
46
|
+
}
|
|
47
|
+
return content;
|
|
48
|
+
};
|
|
12
49
|
export async function getAllContent(options) {
|
|
13
50
|
try {
|
|
14
51
|
const url = generateContentUrl(options);
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
if ('status' in content && !('results' in content)) {
|
|
52
|
+
const content = await fetchContent(options);
|
|
53
|
+
if (!checkContentHasResults(content)) {
|
|
18
54
|
logger.error('Error fetching data. ', { url, content, options });
|
|
19
55
|
return content;
|
|
20
56
|
}
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
if (canTrack &&
|
|
24
|
-
// This makes sure we have a non-error response with the results array.
|
|
25
|
-
Array.isArray(content.results)) {
|
|
26
|
-
for (const item of content.results) {
|
|
27
|
-
await handleABTesting({ item, canTrack });
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
catch (e) {
|
|
32
|
-
logger.error('Could not setup A/B testing. ', e);
|
|
33
|
-
}
|
|
34
|
-
return content;
|
|
57
|
+
return processContentResult(options, content);
|
|
35
58
|
}
|
|
36
59
|
catch (error) {
|
|
37
60
|
logger.error('Error fetching data. ', error);
|
|
@@ -19,6 +19,7 @@ export interface GetContentOptions {
|
|
|
19
19
|
/**
|
|
20
20
|
* If set to `true`, it will lazy load symbols/references.
|
|
21
21
|
* If set to `false`, it will render the entire content tree eagerly.
|
|
22
|
+
* @deprecated use `enrich` instead
|
|
22
23
|
*/
|
|
23
24
|
noTraverse?: boolean;
|
|
24
25
|
/**
|
|
@@ -30,8 +31,13 @@ export interface GetContentOptions {
|
|
|
30
31
|
canTrack?: boolean;
|
|
31
32
|
/**
|
|
32
33
|
* Include references in the response. Defaults to `true`.
|
|
34
|
+
* @deprecated use `enrich` instead
|
|
33
35
|
*/
|
|
34
36
|
includeRefs?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Include multilevel references in the response.
|
|
39
|
+
*/
|
|
40
|
+
enrich?: boolean;
|
|
35
41
|
/**
|
|
36
42
|
* If provided, the API will auto-resolve localized objects to the value of this `locale` key.
|
|
37
43
|
*/
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { BuilderContextInterface } from '../context/types.js';
|
|
2
2
|
import type { BuilderBlock } from '../types/builder-block.js';
|
|
3
|
-
export declare function getProcessedBlock({ block, context, shouldEvaluateBindings,
|
|
3
|
+
export declare function getProcessedBlock({ block, context, shouldEvaluateBindings, localState, rootState, rootSetState, }: {
|
|
4
4
|
block: BuilderBlock;
|
|
5
5
|
/**
|
|
6
6
|
* In some cases, we want to avoid evaluating bindings and only want framework-specific block transformation. It is
|
|
7
7
|
* also sometimes too early to consider bindings, e.g. when we might be looking at a repeated block.
|
|
8
8
|
*/
|
|
9
9
|
shouldEvaluateBindings: boolean;
|
|
10
|
-
} & Pick<BuilderContextInterface, '
|
|
10
|
+
} & Pick<BuilderContextInterface, 'localState' | 'context' | 'rootState' | 'rootSetState'>): BuilderBlock;
|