@builder.io/sdk-svelte 0.0.4 → 0.0.6
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/package/--package.json +3 -2
- package/package/components/render-content/render-content.svelte +13 -7
- package/package/functions/track.d.ts +35 -5
- package/package/functions/track.js +5 -4
- package/package/index.d.ts +1 -0
- package/package/index.js +1 -0
- package/package.json +3 -2
- package/src/blocks/BaseText.svelte +27 -0
- package/src/blocks/button/button.svelte +36 -0
- package/src/blocks/button/component-info.ts +42 -0
- package/src/blocks/columns/columns.svelte +148 -0
- package/src/blocks/columns/component-info.ts +254 -0
- package/src/blocks/custom-code/component-info.ts +31 -0
- package/src/blocks/custom-code/custom-code.svelte +66 -0
- package/src/blocks/embed/component-info.ts +53 -0
- package/src/blocks/embed/embed.svelte +51 -0
- package/src/blocks/embed/helpers.ts +8 -0
- package/src/blocks/form/component-info.ts +282 -0
- package/src/blocks/form/form.svelte +332 -0
- package/src/blocks/fragment/component-info.ts +10 -0
- package/src/blocks/fragment/fragment.svelte +12 -0
- package/src/blocks/image/component-info.ts +170 -0
- package/src/blocks/image/image.helpers.ts +75 -0
- package/src/blocks/image/image.svelte +145 -0
- package/src/blocks/img/component-info.ts +21 -0
- package/src/blocks/img/img.svelte +51 -0
- package/src/blocks/input/component-info.ts +78 -0
- package/src/blocks/input/input.svelte +34 -0
- package/src/blocks/raw-text/component-info.ts +15 -0
- package/src/blocks/raw-text/raw-text.svelte +16 -0
- package/src/blocks/section/component-info.ts +50 -0
- package/src/blocks/section/section.svelte +34 -0
- package/src/blocks/select/component-info.ts +61 -0
- package/src/blocks/select/select.svelte +34 -0
- package/src/blocks/submit-button/component-info.ts +32 -0
- package/src/blocks/submit-button/submit-button.svelte +15 -0
- package/src/blocks/symbol/component-info.ts +43 -0
- package/src/blocks/symbol/symbol.svelte +100 -0
- package/src/blocks/text/component-info.ts +24 -0
- package/src/blocks/text/text.svelte +11 -0
- package/src/blocks/textarea/component-info.ts +48 -0
- package/src/blocks/textarea/textarea.svelte +19 -0
- package/src/blocks/util.ts +6 -0
- package/src/blocks/video/component-info.ts +109 -0
- package/src/blocks/video/video.svelte +102 -0
- package/src/components/render-block/block-styles.svelte +62 -0
- package/src/components/render-block/render-block.helpers.ts +27 -0
- package/src/components/render-block/render-block.svelte +228 -0
- package/src/components/render-block/render-component-with-context.svelte +38 -0
- package/src/components/render-block/render-component.svelte +35 -0
- package/src/components/render-block/render-repeated-block.svelte +53 -0
- package/src/components/render-block/types.ts +7 -0
- package/src/components/render-blocks.svelte +104 -0
- package/src/components/render-content/components/render-styles.svelte +80 -0
- package/src/components/render-content/index.ts +1 -0
- package/src/components/render-content/render-content.svelte +360 -0
- package/src/components/render-inlined-styles.svelte +26 -0
- package/src/constants/builder-registered-components.ts +42 -0
- package/src/constants/device-sizes.ts +28 -0
- package/src/constants/target.ts +2 -0
- package/src/context/builder.context.ts +13 -0
- package/src/context/types.ts +23 -0
- package/src/functions/camel-to-kebab-case.ts +2 -0
- package/src/functions/evaluate.ts +52 -0
- package/src/functions/event-handler-name.ts +6 -0
- package/src/functions/extract-text-styles.ts +39 -0
- package/src/functions/fast-clone.ts +5 -0
- package/src/functions/get-block-actions-handler.ts +19 -0
- package/src/functions/get-block-actions.ts +25 -0
- package/src/functions/get-block-component-options.ts +12 -0
- package/src/functions/get-block-properties.ts +11 -0
- package/src/functions/get-block-tag.ts +7 -0
- package/src/functions/get-builder-search-params/fn.test.ts +19 -0
- package/src/functions/get-builder-search-params/index.ts +48 -0
- package/src/functions/get-content/ab-testing.ts +132 -0
- package/src/functions/get-content/fn.test.ts +35 -0
- package/src/functions/get-content/index.ts +77 -0
- package/src/functions/get-content/types.ts +36 -0
- package/src/functions/get-fetch.ts +14 -0
- package/src/functions/get-global-this.ts +15 -0
- package/src/functions/get-processed-block.test.ts +33 -0
- package/src/functions/get-processed-block.ts +54 -0
- package/src/functions/get-react-native-block-styles.ts +30 -0
- package/src/functions/if-target.ts +20 -0
- package/src/functions/is-browser.ts +3 -0
- package/src/functions/is-editing.ts +7 -0
- package/src/functions/is-iframe.ts +5 -0
- package/src/functions/is-previewing.ts +14 -0
- package/src/functions/mark-mutable.ts +7 -0
- package/src/functions/on-change.test.ts +23 -0
- package/src/functions/on-change.ts +29 -0
- package/src/functions/register-component.ts +67 -0
- package/src/functions/register.ts +46 -0
- package/src/functions/sanitize-react-native-block-styles.ts +65 -0
- package/src/functions/set-editor-settings.ts +16 -0
- package/src/functions/set.test.ts +19 -0
- package/src/functions/set.ts +26 -0
- package/src/functions/track.ts +124 -0
- package/src/functions/transform-block.ts +6 -0
- package/src/helpers/ab-tests.ts +22 -0
- package/src/helpers/cookie.ts +102 -0
- package/src/helpers/css.ts +33 -0
- package/src/helpers/flatten.ts +24 -0
- package/src/helpers/localStorage.ts +38 -0
- package/src/helpers/nullable.ts +4 -0
- package/src/helpers/sessionId.ts +34 -0
- package/src/helpers/time.ts +4 -0
- package/src/helpers/url.test.ts +16 -0
- package/src/helpers/url.ts +12 -0
- package/src/helpers/uuid.ts +17 -0
- package/src/helpers/visitorId.ts +38 -0
- package/src/index-helpers/blocks-exports.ts +10 -0
- package/src/index-helpers/top-of-file.ts +4 -0
- package/src/index.ts +13 -0
- package/src/scripts/init-editing.ts +102 -0
- package/src/types/builder-block.ts +58 -0
- package/src/types/builder-content.ts +39 -0
- package/src/types/can-track.ts +3 -0
- package/src/types/components.ts +188 -0
- package/src/types/deep-partial.ts +7 -0
- package/src/types/element.ts +60 -0
- package/src/types/targets.ts +6 -0
- package/src/types/typescript.ts +7 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { convertSearchParamsToQueryObject, getBuilderSearchParams } from '.';
|
|
2
|
+
|
|
3
|
+
const querystring =
|
|
4
|
+
'someotherValue=jklsjfdal&abc=klfdjklgfds&builder.cachebust=true&builder.preview=page&builder.noCache=true&__builder_editing__=true&builder.overrides.page=037948e52eaf4743afed464f02c70da4&builder.overrides.037948e52eaf4743afed464f02c70da4=037948e52eaf4743afed464f02c70da4&builder.overrides.page%3A%2F=037948e52eaf4743afed464f02c70da4&preview_theme_id=128854393017';
|
|
5
|
+
|
|
6
|
+
const url = new URL(`localhost:3000/about-us?${querystring}`);
|
|
7
|
+
|
|
8
|
+
describe('Get Builder SearchParams', () => {
|
|
9
|
+
test('correctly converts URLSearchParams to object', () => {
|
|
10
|
+
const output = convertSearchParamsToQueryObject(url.searchParams);
|
|
11
|
+
expect(output).toMatchSnapshot();
|
|
12
|
+
});
|
|
13
|
+
test('correctly extracts all builder params from a query object', () => {
|
|
14
|
+
const output = getBuilderSearchParams(
|
|
15
|
+
convertSearchParamsToQueryObject(url.searchParams)
|
|
16
|
+
);
|
|
17
|
+
expect(output).toMatchSnapshot();
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { isBrowser } from '../is-browser.js';
|
|
2
|
+
|
|
3
|
+
const BUILDER_SEARCHPARAMS_PREFIX = 'builder.';
|
|
4
|
+
|
|
5
|
+
type QueryObject = Record<string, string>;
|
|
6
|
+
|
|
7
|
+
export const convertSearchParamsToQueryObject = (
|
|
8
|
+
searchParams: URLSearchParams
|
|
9
|
+
): QueryObject => {
|
|
10
|
+
const options: Record<string, string> = {};
|
|
11
|
+
searchParams.forEach((value, key) => {
|
|
12
|
+
options[key] = value;
|
|
13
|
+
});
|
|
14
|
+
return options;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const getBuilderSearchParams = (
|
|
18
|
+
_options: QueryObject | URLSearchParams | undefined
|
|
19
|
+
) => {
|
|
20
|
+
if (!_options) {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
const options = normalizeSearchParams(_options);
|
|
24
|
+
|
|
25
|
+
const newOptions: QueryObject = {};
|
|
26
|
+
Object.keys(options).forEach((key) => {
|
|
27
|
+
if (key.startsWith(BUILDER_SEARCHPARAMS_PREFIX)) {
|
|
28
|
+
const trimmedKey = key.replace(BUILDER_SEARCHPARAMS_PREFIX, '');
|
|
29
|
+
newOptions[trimmedKey] = options[key];
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
return newOptions;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const getBuilderSearchParamsFromWindow = () => {
|
|
36
|
+
if (!isBrowser()) {
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
40
|
+
return getBuilderSearchParams(searchParams);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const normalizeSearchParams = (
|
|
44
|
+
searchParams: QueryObject | URLSearchParams
|
|
45
|
+
): QueryObject =>
|
|
46
|
+
searchParams instanceof URLSearchParams
|
|
47
|
+
? convertSearchParamsToQueryObject(searchParams)
|
|
48
|
+
: searchParams;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BuilderContent,
|
|
3
|
+
BuilderContentVariation,
|
|
4
|
+
} from '../../types/builder-content.js';
|
|
5
|
+
import {
|
|
6
|
+
getContentVariationCookie,
|
|
7
|
+
setContentVariationCookie,
|
|
8
|
+
} from '../../helpers/ab-tests.js';
|
|
9
|
+
import type { Overwrite } from '../../types/typescript.js';
|
|
10
|
+
import type { CanTrack } from '../../types/can-track.js';
|
|
11
|
+
import { checkIsDefined } from '../../helpers/nullable.js';
|
|
12
|
+
|
|
13
|
+
type BuilderContentWithVariations = Overwrite<
|
|
14
|
+
BuilderContent,
|
|
15
|
+
Required<Pick<BuilderContent, 'variations' | 'id'>>
|
|
16
|
+
>;
|
|
17
|
+
|
|
18
|
+
const checkIsBuilderContentWithVariations = (
|
|
19
|
+
item: BuilderContent
|
|
20
|
+
): item is BuilderContentWithVariations =>
|
|
21
|
+
checkIsDefined(item.id) &&
|
|
22
|
+
checkIsDefined(item.variations) &&
|
|
23
|
+
Object.keys(item.variations).length > 0;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Randomly assign a variation to this user and store it in cookies/storage
|
|
27
|
+
*/
|
|
28
|
+
const getRandomVariationId = ({
|
|
29
|
+
id,
|
|
30
|
+
variations,
|
|
31
|
+
}: BuilderContentWithVariations) => {
|
|
32
|
+
let n = 0;
|
|
33
|
+
const random = Math.random();
|
|
34
|
+
|
|
35
|
+
// loop over variations test ratios, incrementing a counter,
|
|
36
|
+
// until we find the variation that this user should be assigned to
|
|
37
|
+
for (const id in variations) {
|
|
38
|
+
const testRatio = variations[id]?.testRatio;
|
|
39
|
+
n += testRatio!;
|
|
40
|
+
|
|
41
|
+
if (random < n) {
|
|
42
|
+
return id;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// `item.variations` does not include the default variation
|
|
47
|
+
// if we arrive here, then it means that the random number fits in the default variation bucket
|
|
48
|
+
return id;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const getTestFields = ({
|
|
52
|
+
item,
|
|
53
|
+
testGroupId,
|
|
54
|
+
}: {
|
|
55
|
+
item: BuilderContentWithVariations;
|
|
56
|
+
testGroupId: string;
|
|
57
|
+
}): TestFields => {
|
|
58
|
+
const variationValue = item.variations[testGroupId];
|
|
59
|
+
if (
|
|
60
|
+
testGroupId === item.id ||
|
|
61
|
+
// handle edge-case where `testGroupId` points to non-existing variation
|
|
62
|
+
!variationValue
|
|
63
|
+
) {
|
|
64
|
+
return {
|
|
65
|
+
testVariationId: item.id,
|
|
66
|
+
testVariationName: 'Default',
|
|
67
|
+
};
|
|
68
|
+
} else {
|
|
69
|
+
return {
|
|
70
|
+
data: variationValue.data,
|
|
71
|
+
testVariationId: variationValue.id,
|
|
72
|
+
testVariationName:
|
|
73
|
+
variationValue.name || (variationValue.id === item.id ? 'Default' : ''),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
type TestFields = {
|
|
79
|
+
data?: BuilderContentVariation['data'];
|
|
80
|
+
testVariationId?: string;
|
|
81
|
+
testVariationName: string;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const getContentVariation = async ({
|
|
85
|
+
item,
|
|
86
|
+
canTrack,
|
|
87
|
+
}: {
|
|
88
|
+
item: BuilderContentWithVariations;
|
|
89
|
+
} & CanTrack): Promise<TestFields> => {
|
|
90
|
+
// try to find test variation in cookies/storage
|
|
91
|
+
const testGroupId = await getContentVariationCookie({
|
|
92
|
+
canTrack,
|
|
93
|
+
contentId: item.id,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const testFields = testGroupId
|
|
97
|
+
? getTestFields({ item, testGroupId })
|
|
98
|
+
: undefined;
|
|
99
|
+
|
|
100
|
+
if (testFields) {
|
|
101
|
+
return testFields;
|
|
102
|
+
} else {
|
|
103
|
+
// if variation not found in storage, assign a random variation to this user
|
|
104
|
+
const randomVariationId = getRandomVariationId({
|
|
105
|
+
variations: item.variations,
|
|
106
|
+
id: item.id,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// store variation in cookies/storage
|
|
110
|
+
setContentVariationCookie({
|
|
111
|
+
contentId: item.id,
|
|
112
|
+
value: randomVariationId,
|
|
113
|
+
canTrack,
|
|
114
|
+
}).catch((err) => {
|
|
115
|
+
console.error('could not store A/B test variation: ', err);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return getTestFields({ item, testGroupId: randomVariationId });
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export const handleABTesting = async ({
|
|
123
|
+
item,
|
|
124
|
+
canTrack,
|
|
125
|
+
}: { item: BuilderContent } & CanTrack) => {
|
|
126
|
+
if (!checkIsBuilderContentWithVariations(item)) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const variationValue = await getContentVariation({ item, canTrack });
|
|
131
|
+
Object.assign(item, variationValue);
|
|
132
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { generateContentUrl } from '.';
|
|
2
|
+
|
|
3
|
+
const testKey = 'YJIGb4i01jvw0SRdL5Bt';
|
|
4
|
+
const testModel = 'page';
|
|
5
|
+
const testId = 'c1b81bab59704599b997574eb0736def';
|
|
6
|
+
|
|
7
|
+
const options = {
|
|
8
|
+
cachebust: 'true',
|
|
9
|
+
noCache: 'true',
|
|
10
|
+
'overrides.037948e52eaf4743afed464f02c70da4':
|
|
11
|
+
'037948e52eaf4743afed464f02c70da4',
|
|
12
|
+
'overrides.page': '037948e52eaf4743afed464f02c70da4',
|
|
13
|
+
'overrides.page:/': '037948e52eaf4743afed464f02c70da4',
|
|
14
|
+
preview: 'page',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
describe('Generate Content URL', () => {
|
|
18
|
+
test('generates the proper value for a simple query', () => {
|
|
19
|
+
const output = generateContentUrl({
|
|
20
|
+
apiKey: testKey,
|
|
21
|
+
model: testModel,
|
|
22
|
+
query: { id: testId },
|
|
23
|
+
});
|
|
24
|
+
expect(output).toMatchSnapshot();
|
|
25
|
+
});
|
|
26
|
+
test('Handles overrides correctly', () => {
|
|
27
|
+
const output = generateContentUrl({
|
|
28
|
+
apiKey: testKey,
|
|
29
|
+
model: testModel,
|
|
30
|
+
query: { id: testId },
|
|
31
|
+
options,
|
|
32
|
+
});
|
|
33
|
+
expect(output).toMatchSnapshot();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { flatten } from '../../helpers/flatten.js';
|
|
2
|
+
import type { BuilderContent } from '../../types/builder-content.js';
|
|
3
|
+
import {
|
|
4
|
+
getBuilderSearchParamsFromWindow,
|
|
5
|
+
normalizeSearchParams,
|
|
6
|
+
} from '../get-builder-search-params/index.js';
|
|
7
|
+
import { getFetch } from '../get-fetch.js';
|
|
8
|
+
import { handleABTesting } from './ab-testing.js';
|
|
9
|
+
|
|
10
|
+
export type GetContentOptions = import('./types.js').GetContentOptions;
|
|
11
|
+
|
|
12
|
+
export async function getContent(
|
|
13
|
+
options: GetContentOptions
|
|
14
|
+
): Promise<BuilderContent | null> {
|
|
15
|
+
return (await getAllContent({ ...options, limit: 1 })).results[0] || null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const generateContentUrl = (options: GetContentOptions): URL => {
|
|
19
|
+
const {
|
|
20
|
+
limit = 30,
|
|
21
|
+
userAttributes,
|
|
22
|
+
query,
|
|
23
|
+
noTraverse = false,
|
|
24
|
+
model,
|
|
25
|
+
apiKey,
|
|
26
|
+
} = options;
|
|
27
|
+
|
|
28
|
+
const url = new URL(
|
|
29
|
+
`https://cdn.builder.io/api/v2/content/${model}?apiKey=${apiKey}&limit=${limit}&noTraverse=${noTraverse}`
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const queryOptions = {
|
|
33
|
+
...getBuilderSearchParamsFromWindow(),
|
|
34
|
+
...normalizeSearchParams(options.options || {}),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const flattened = flatten(queryOptions);
|
|
38
|
+
for (const key in flattened) {
|
|
39
|
+
url.searchParams.set(key, String(flattened[key]));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (userAttributes) {
|
|
43
|
+
url.searchParams.set('userAttributes', JSON.stringify(userAttributes));
|
|
44
|
+
}
|
|
45
|
+
if (query) {
|
|
46
|
+
const flattened = flatten({ query });
|
|
47
|
+
for (const key in flattened) {
|
|
48
|
+
url.searchParams.set(key, JSON.stringify((flattened as any)[key]));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return url;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
interface ContentResponse {
|
|
56
|
+
results: BuilderContent[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function getAllContent(
|
|
60
|
+
options: GetContentOptions
|
|
61
|
+
): Promise<ContentResponse> {
|
|
62
|
+
const url = generateContentUrl(options);
|
|
63
|
+
|
|
64
|
+
const fetch = await getFetch();
|
|
65
|
+
const content: ContentResponse = await fetch(url.href).then((res) =>
|
|
66
|
+
res.json()
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const canTrack = options.canTrack !== false;
|
|
70
|
+
if (canTrack) {
|
|
71
|
+
for (const item of content.results) {
|
|
72
|
+
await handleABTesting({ item, canTrack });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return content;
|
|
77
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface GetContentOptions {
|
|
2
|
+
/** The model to get content for */
|
|
3
|
+
model: string;
|
|
4
|
+
|
|
5
|
+
/** Your public API key */
|
|
6
|
+
apiKey: string;
|
|
7
|
+
|
|
8
|
+
/** Number of items to fetch. Default is 1 */
|
|
9
|
+
limit?: number;
|
|
10
|
+
|
|
11
|
+
/** User attributes to target on, such as { urlPath: '/foo', device: 'mobile', ...etc } */
|
|
12
|
+
userAttributes?: (Record<string, string> & { urlPath?: string }) | null;
|
|
13
|
+
|
|
14
|
+
/** Custom query */
|
|
15
|
+
query?: Record<string, any>;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Any other API options.
|
|
19
|
+
* Accepts both a key/value object or a `URLSearchParams` instance
|
|
20
|
+
* */
|
|
21
|
+
options?: Record<string, any> | URLSearchParams;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* If set to `true`, it will lazy load symbols/references.
|
|
25
|
+
* If set to `false`, it will render the entire content tree eagerly.
|
|
26
|
+
*/
|
|
27
|
+
noTraverse?: boolean;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* If set to `false`, it will not use cookies to target content. Therefore, A/B Testing will be disabled and
|
|
31
|
+
* only the default variation will be returned to every user.
|
|
32
|
+
*
|
|
33
|
+
* Defaults to `true`.
|
|
34
|
+
*/
|
|
35
|
+
canTrack?: boolean;
|
|
36
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { getGlobalThis } from './get-global-this.js';
|
|
2
|
+
|
|
3
|
+
export async function getFetch(): Promise<typeof global.fetch> {
|
|
4
|
+
const globalFetch: typeof global.fetch = getGlobalThis().fetch;
|
|
5
|
+
|
|
6
|
+
if (typeof globalFetch === 'undefined' && typeof global !== 'undefined') {
|
|
7
|
+
const nodeFetch = import('node-fetch').then((d) => d.default) as Promise<
|
|
8
|
+
typeof global.fetch
|
|
9
|
+
>;
|
|
10
|
+
return (nodeFetch as any).default || nodeFetch;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return (globalFetch as any).default || globalFetch;
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function getGlobalThis(): any {
|
|
2
|
+
if (typeof globalThis !== 'undefined') {
|
|
3
|
+
return globalThis;
|
|
4
|
+
}
|
|
5
|
+
if (typeof window !== 'undefined') {
|
|
6
|
+
return window;
|
|
7
|
+
}
|
|
8
|
+
if (typeof global !== 'undefined') {
|
|
9
|
+
return global;
|
|
10
|
+
}
|
|
11
|
+
if (typeof self !== 'undefined') {
|
|
12
|
+
return self;
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { BuilderBlock } from '../types/builder-block.js';
|
|
2
|
+
import { getProcessedBlock } from './get-processed-block.js';
|
|
3
|
+
|
|
4
|
+
test('Can process bindings', () => {
|
|
5
|
+
const block: BuilderBlock = {
|
|
6
|
+
'@type': '@builder.io/sdk:Element',
|
|
7
|
+
properties: {
|
|
8
|
+
foo: 'bar',
|
|
9
|
+
},
|
|
10
|
+
bindings: {
|
|
11
|
+
'properties.foo': '"baz"',
|
|
12
|
+
'responsiveStyles.large.zIndex': '1 + 1',
|
|
13
|
+
'properties.test': 'state.test',
|
|
14
|
+
'properties.block': `
|
|
15
|
+
const foo = 'bar';
|
|
16
|
+
return foo;
|
|
17
|
+
`,
|
|
18
|
+
'properties.isEditing': 'builder.isEditing',
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
const processed = getProcessedBlock({
|
|
22
|
+
block,
|
|
23
|
+
context: {},
|
|
24
|
+
state: { test: 'hello' },
|
|
25
|
+
shouldEvaluateBindings: true,
|
|
26
|
+
});
|
|
27
|
+
expect(processed).not.toEqual(block);
|
|
28
|
+
expect(processed.properties?.foo).toEqual('baz');
|
|
29
|
+
expect(processed.properties?.test).toEqual('hello');
|
|
30
|
+
expect(processed.properties?.block).toEqual('bar');
|
|
31
|
+
expect(processed.properties?.isEditing).toEqual(false);
|
|
32
|
+
expect(processed.responsiveStyles?.large?.zIndex).toEqual(2);
|
|
33
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { BuilderContextInterface } from '../context/types.js';
|
|
2
|
+
import type { BuilderBlock } from '../types/builder-block.js';
|
|
3
|
+
import { evaluate } from './evaluate.js';
|
|
4
|
+
import { fastClone } from './fast-clone.js';
|
|
5
|
+
import { set } from './set.js';
|
|
6
|
+
import { transformBlock } from './transform-block.js';
|
|
7
|
+
|
|
8
|
+
const evaluateBindings = ({
|
|
9
|
+
block,
|
|
10
|
+
context,
|
|
11
|
+
state,
|
|
12
|
+
}: {
|
|
13
|
+
block: BuilderBlock;
|
|
14
|
+
} & Pick<BuilderContextInterface, 'state' | 'context'>): BuilderBlock => {
|
|
15
|
+
if (!block.bindings) {
|
|
16
|
+
return block;
|
|
17
|
+
}
|
|
18
|
+
const copy = fastClone(block);
|
|
19
|
+
const copied = {
|
|
20
|
+
...copy,
|
|
21
|
+
properties: { ...copy.properties },
|
|
22
|
+
actions: { ...copy.actions },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
for (const binding in block.bindings) {
|
|
26
|
+
const expression = block.bindings[binding];
|
|
27
|
+
const value = evaluate({ code: expression, state, context });
|
|
28
|
+
set(copied, binding, value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return copied;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function getProcessedBlock({
|
|
35
|
+
block,
|
|
36
|
+
context,
|
|
37
|
+
shouldEvaluateBindings,
|
|
38
|
+
state,
|
|
39
|
+
}: {
|
|
40
|
+
block: BuilderBlock;
|
|
41
|
+
/**
|
|
42
|
+
* In some cases, we want to avoid evaluating bindings and only want framework-specific block transformation. It is
|
|
43
|
+
* also sometimes too early to consider bindings, e.g. when we might be looking at a repeated block.
|
|
44
|
+
*/
|
|
45
|
+
shouldEvaluateBindings: boolean;
|
|
46
|
+
} & Pick<BuilderContextInterface, 'state' | 'context'>): BuilderBlock {
|
|
47
|
+
const transformedBlock = transformBlock(block);
|
|
48
|
+
|
|
49
|
+
if (shouldEvaluateBindings) {
|
|
50
|
+
return evaluateBindings({ block: transformedBlock, state, context });
|
|
51
|
+
} else {
|
|
52
|
+
return transformedBlock;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { BuilderContextInterface } from '../context/types.js';
|
|
2
|
+
import type { BuilderBlock } from '../types/builder-block.js';
|
|
3
|
+
import { sanitizeReactNativeBlockStyles } from './sanitize-react-native-block-styles.js';
|
|
4
|
+
|
|
5
|
+
export function getReactNativeBlockStyles({
|
|
6
|
+
block,
|
|
7
|
+
context,
|
|
8
|
+
}: {
|
|
9
|
+
block: BuilderBlock;
|
|
10
|
+
context: BuilderContextInterface;
|
|
11
|
+
}): CSSStyleDeclaration | Record<string, string | undefined> {
|
|
12
|
+
const responsiveStyles = block.responsiveStyles;
|
|
13
|
+
if (!responsiveStyles) {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const styles = {
|
|
18
|
+
// recursively apply inherited styles so that they can be passed down to children `Text` blocks
|
|
19
|
+
...context.inheritedStyles,
|
|
20
|
+
...(responsiveStyles.large || {}),
|
|
21
|
+
...(responsiveStyles.medium || {}),
|
|
22
|
+
...(responsiveStyles.small || {}),
|
|
23
|
+
} as Record<string, string | number>;
|
|
24
|
+
|
|
25
|
+
const newStyles = sanitizeReactNativeBlockStyles(
|
|
26
|
+
styles
|
|
27
|
+
) as any as CSSStyleDeclaration;
|
|
28
|
+
|
|
29
|
+
return newStyles;
|
|
30
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { TARGET } from '../constants/target.js';
|
|
2
|
+
|
|
3
|
+
type Target = import('../types/targets').Target;
|
|
4
|
+
|
|
5
|
+
// TODO: babel compile to include certain code only if the target matches
|
|
6
|
+
export function ifTarget<A, B>({
|
|
7
|
+
targets,
|
|
8
|
+
doThing,
|
|
9
|
+
elseThing,
|
|
10
|
+
}: {
|
|
11
|
+
targets: Target[];
|
|
12
|
+
doThing: () => A;
|
|
13
|
+
elseThing?: () => B;
|
|
14
|
+
}) {
|
|
15
|
+
if (TARGET && targets.includes(TARGET)) {
|
|
16
|
+
return doThing();
|
|
17
|
+
} else {
|
|
18
|
+
return elseThing?.();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { isBrowser } from './is-browser.js';
|
|
2
|
+
import { isEditing } from './is-editing.js';
|
|
3
|
+
|
|
4
|
+
export function isPreviewing() {
|
|
5
|
+
if (!isBrowser()) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (isEditing()) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return Boolean(location.search.indexOf('builder.preview=') !== -1);
|
|
14
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { onChange } from './on-change.js';
|
|
2
|
+
|
|
3
|
+
test('onChange can observe a shallow change', () => {
|
|
4
|
+
let changeHappend = false;
|
|
5
|
+
const obj = onChange({ foo: 'hi' }, () => (changeHappend = true));
|
|
6
|
+
obj.foo = 'yo';
|
|
7
|
+
expect(changeHappend).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test('onChange can observe a deep change', () => {
|
|
11
|
+
let changeHappend = false;
|
|
12
|
+
const obj = onChange({ foo: { bar: 'hi' } }, () => (changeHappend = true));
|
|
13
|
+
obj.foo.bar = 'yo';
|
|
14
|
+
expect(changeHappend).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('Smoke test: callback is not fired if no properties updated', () => {
|
|
18
|
+
let changeHappend = false;
|
|
19
|
+
const obj = onChange({ foo: { bar: 'hi' } }, () => (changeHappend = true));
|
|
20
|
+
// Access some properties
|
|
21
|
+
obj.foo.bar;
|
|
22
|
+
expect(changeHappend).toBe(false);
|
|
23
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export const onChangeProxySymbol = Symbol('onChangeProxySymbol');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Deeply observe an object and run a callback when it changes
|
|
5
|
+
*
|
|
6
|
+
* See ./on-change.test.ts for more info
|
|
7
|
+
*/
|
|
8
|
+
export function onChange<T extends object = any>(obj: T, cb: () => void): T {
|
|
9
|
+
return new Proxy(obj, {
|
|
10
|
+
get(target, key) {
|
|
11
|
+
if (key === onChangeProxySymbol) {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
const value = Reflect.get(target, key);
|
|
15
|
+
if (value && typeof value === 'object') {
|
|
16
|
+
if (value[onChangeProxySymbol]) {
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
return onChange(value, cb);
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
},
|
|
23
|
+
set(target, key, value) {
|
|
24
|
+
const returnValue = Reflect.set(target, key, value);
|
|
25
|
+
cb();
|
|
26
|
+
return returnValue;
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|