@builder.io/sdk-svelte 5.2.16 → 5.3.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 (118) hide show
  1. package/README.md +4 -0
  2. package/lib/browser/components/block/block.helpers.d.ts +1 -3
  3. package/lib/browser/components/block/block.helpers.js +31 -4
  4. package/lib/browser/components/block/block.svelte +14 -1
  5. package/lib/browser/components/content/components/styles.helpers.d.ts +1 -1
  6. package/lib/browser/components/content/components/styles.helpers.js +1 -1
  7. package/lib/browser/components/content/content.helpers.d.ts +5 -1
  8. package/lib/browser/components/content/content.helpers.js +39 -2
  9. package/lib/browser/components/content/content.svelte +4 -17
  10. package/lib/browser/constants/builder-registered-components.d.ts +1 -0
  11. package/lib/browser/constants/builder-registered-components.js +40 -33
  12. package/lib/browser/constants/device-sizes.js +14 -2
  13. package/lib/browser/constants/sdk-version.d.ts +1 -1
  14. package/lib/browser/constants/sdk-version.js +1 -1
  15. package/lib/browser/functions/evaluate/browser-runtime/browser.js +13 -1
  16. package/lib/browser/functions/evaluate/evaluate.js +21 -5
  17. package/lib/browser/functions/evaluate/node-runtime/node-runtime.js +114 -23
  18. package/lib/browser/functions/evaluate/safe-state-expression.d.ts +33 -0
  19. package/lib/browser/functions/evaluate/safe-state-expression.js +236 -0
  20. package/lib/browser/functions/evaluate/should-force-browser-runtime-in-node.js +3 -1
  21. package/lib/browser/functions/extract-localized-values.d.ts +4 -0
  22. package/lib/browser/functions/extract-localized-values.js +65 -26
  23. package/lib/browser/functions/get-block-properties.js +2 -1
  24. package/lib/browser/functions/get-processed-block.d.ts +0 -1
  25. package/lib/browser/functions/get-processed-block.js +45 -57
  26. package/lib/browser/functions/is-editing-or-previewing.d.ts +1 -0
  27. package/lib/browser/functions/is-editing-or-previewing.js +3 -0
  28. package/lib/browser/functions/set.d.ts +29 -1
  29. package/lib/browser/functions/set.js +85 -4
  30. package/lib/browser/helpers/bounded-cache.d.ts +4 -0
  31. package/lib/browser/helpers/bounded-cache.js +12 -0
  32. package/lib/browser/helpers/copy-plain-data.d.ts +6 -0
  33. package/lib/browser/helpers/copy-plain-data.js +40 -0
  34. package/lib/edge/components/block/block.helpers.d.ts +1 -3
  35. package/lib/edge/components/block/block.helpers.js +31 -4
  36. package/lib/edge/components/block/block.svelte +14 -1
  37. package/lib/edge/components/content/components/styles.helpers.d.ts +1 -1
  38. package/lib/edge/components/content/components/styles.helpers.js +1 -1
  39. package/lib/edge/components/content/content.helpers.d.ts +5 -1
  40. package/lib/edge/components/content/content.helpers.js +39 -2
  41. package/lib/edge/components/content/content.svelte +4 -17
  42. package/lib/edge/constants/builder-registered-components.d.ts +1 -0
  43. package/lib/edge/constants/builder-registered-components.js +40 -33
  44. package/lib/edge/constants/device-sizes.js +14 -2
  45. package/lib/edge/constants/sdk-version.d.ts +1 -1
  46. package/lib/edge/constants/sdk-version.js +1 -1
  47. package/lib/edge/functions/evaluate/browser-runtime/browser.js +13 -1
  48. package/lib/edge/functions/evaluate/evaluate.js +21 -5
  49. package/lib/edge/functions/evaluate/node-runtime/node-runtime.js +114 -23
  50. package/lib/edge/functions/evaluate/safe-state-expression.d.ts +33 -0
  51. package/lib/edge/functions/evaluate/safe-state-expression.js +236 -0
  52. package/lib/edge/functions/evaluate/should-force-browser-runtime-in-node.js +3 -1
  53. package/lib/edge/functions/extract-localized-values.d.ts +4 -0
  54. package/lib/edge/functions/extract-localized-values.js +65 -26
  55. package/lib/edge/functions/get-block-properties.js +2 -1
  56. package/lib/edge/functions/get-processed-block.d.ts +0 -1
  57. package/lib/edge/functions/get-processed-block.js +45 -57
  58. package/lib/edge/functions/is-editing-or-previewing.d.ts +1 -0
  59. package/lib/edge/functions/is-editing-or-previewing.js +3 -0
  60. package/lib/edge/functions/set.d.ts +29 -1
  61. package/lib/edge/functions/set.js +85 -4
  62. package/lib/edge/helpers/bounded-cache.d.ts +4 -0
  63. package/lib/edge/helpers/bounded-cache.js +12 -0
  64. package/lib/edge/helpers/copy-plain-data.d.ts +6 -0
  65. package/lib/edge/helpers/copy-plain-data.js +40 -0
  66. package/lib/node/components/block/block.helpers.d.ts +1 -3
  67. package/lib/node/components/block/block.helpers.js +31 -4
  68. package/lib/node/components/block/block.svelte +14 -1
  69. package/lib/node/components/content/components/styles.helpers.d.ts +1 -1
  70. package/lib/node/components/content/components/styles.helpers.js +1 -1
  71. package/lib/node/components/content/content.helpers.d.ts +5 -1
  72. package/lib/node/components/content/content.helpers.js +39 -2
  73. package/lib/node/components/content/content.svelte +4 -17
  74. package/lib/node/constants/builder-registered-components.d.ts +1 -0
  75. package/lib/node/constants/builder-registered-components.js +40 -33
  76. package/lib/node/constants/device-sizes.js +14 -2
  77. package/lib/node/constants/sdk-version.d.ts +1 -1
  78. package/lib/node/constants/sdk-version.js +1 -1
  79. package/lib/node/functions/evaluate/browser-runtime/browser.js +13 -1
  80. package/lib/node/functions/evaluate/evaluate.js +21 -5
  81. package/lib/node/functions/evaluate/node-runtime/node-runtime.js +114 -23
  82. package/lib/node/functions/evaluate/safe-state-expression.d.ts +33 -0
  83. package/lib/node/functions/evaluate/safe-state-expression.js +236 -0
  84. package/lib/node/functions/evaluate/should-force-browser-runtime-in-node.js +3 -1
  85. package/lib/node/functions/extract-localized-values.d.ts +4 -0
  86. package/lib/node/functions/extract-localized-values.js +65 -26
  87. package/lib/node/functions/get-block-properties.js +2 -1
  88. package/lib/node/functions/get-processed-block.d.ts +0 -1
  89. package/lib/node/functions/get-processed-block.js +45 -57
  90. package/lib/node/functions/is-editing-or-previewing.d.ts +1 -0
  91. package/lib/node/functions/is-editing-or-previewing.js +3 -0
  92. package/lib/node/functions/set.d.ts +29 -1
  93. package/lib/node/functions/set.js +85 -4
  94. package/lib/node/helpers/bounded-cache.d.ts +4 -0
  95. package/lib/node/helpers/bounded-cache.js +12 -0
  96. package/lib/node/helpers/copy-plain-data.d.ts +6 -0
  97. package/lib/node/helpers/copy-plain-data.js +40 -0
  98. package/package.json +1 -1
  99. package/src/components/block/block.helpers.ts +38 -5
  100. package/src/components/block/block.svelte +14 -1
  101. package/src/components/content/components/styles.helpers.ts +1 -1
  102. package/src/components/content/content.helpers.ts +45 -4
  103. package/src/components/content/content.svelte +7 -24
  104. package/src/constants/builder-registered-components.ts +40 -34
  105. package/src/constants/device-sizes.ts +14 -3
  106. package/src/constants/sdk-version.ts +1 -1
  107. package/src/functions/evaluate/browser-runtime/browser.ts +14 -1
  108. package/src/functions/evaluate/evaluate.ts +22 -5
  109. package/src/functions/evaluate/node-runtime/node-runtime.ts +116 -25
  110. package/src/functions/evaluate/safe-state-expression.ts +224 -0
  111. package/src/functions/evaluate/should-force-browser-runtime-in-node.ts +3 -1
  112. package/src/functions/extract-localized-values.ts +61 -27
  113. package/src/functions/get-block-properties.ts +5 -1
  114. package/src/functions/get-processed-block.ts +46 -57
  115. package/src/functions/is-editing-or-previewing.ts +3 -0
  116. package/src/functions/set.ts +89 -4
  117. package/src/helpers/bounded-cache.ts +12 -0
  118. package/src/helpers/copy-plain-data.ts +36 -0
@@ -0,0 +1,236 @@
1
+ import { createBoundedCache } from '../../helpers/bounded-cache';
2
+ /**
3
+ * Recognizes binding expressions that only read `state` and combine the values
4
+ * with operators and literals, e.g. `!state.open`, `state.$index + 1` or
5
+ * `state.plan === "pro" ? "Upgrade" : "Manage"`. Such an expression cannot call
6
+ * functions, assign, loop or reach any global, so it can run as a plain function
7
+ * instead of in a sandbox. Anything else returns `null` and is sandboxed as usual.
8
+ */
9
+ const IDENTIFIER = /^[A-Za-z_$][\w$]*/;
10
+ const NUMBER = /^(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/;
11
+ /**
12
+ * The length of a single- or double-quoted string literal at the start of `code`
13
+ * (no escapes or line breaks), or 0 when there is none.
14
+ */
15
+ const getStringLiteralLength = (code) => {
16
+ const quote = code[0];
17
+ if (quote !== '"' && quote !== "'")
18
+ return 0;
19
+ for (let i = 1; i < code.length; i++) {
20
+ const char = code[i];
21
+ if (char === quote)
22
+ return i + 1;
23
+ if (char === '\\' || char === '\n' || char === '\r')
24
+ return 0;
25
+ }
26
+ return 0;
27
+ };
28
+ const OPERATORS = ['===', '!==', '==', '!=', '<=', '>=', '&&', '||', '??', '<', '>', '!', '+', '-', '*', '/', '%', '?', ':'];
29
+ const LITERAL_KEYWORDS = new Set(['true', 'false', 'null', 'undefined']);
30
+ // Reading these is harmless on data, but they lead to functions and prototypes.
31
+ const BLOCKED_PROPERTIES = new Set(['constructor', '__proto__', 'prototype', '__defineGetter__', '__defineSetter__', '__lookupGetter__', '__lookupSetter__']);
32
+ const MAX_EXPRESSION_LENGTH = 500;
33
+ /**
34
+ * Returns `expression` when it only uses the allowed grammar, otherwise `null`.
35
+ */
36
+ export const getSafeStateExpression = (expression) => {
37
+ if (expression.length > MAX_EXPRESSION_LENGTH)
38
+ return null;
39
+ let rest = expression.trim();
40
+ if (!rest)
41
+ return null;
42
+ let depth = 0;
43
+ // A value (state path, literal or closing paren) was just read: `(` here would be a call.
44
+ let afterValue = false;
45
+ let readsState = false;
46
+ while (rest) {
47
+ const whitespace = /^\s+/.exec(rest);
48
+ if (whitespace) {
49
+ rest = rest.slice(whitespace[0].length);
50
+ continue;
51
+ }
52
+ const identifier = IDENTIFIER.exec(rest);
53
+ if (identifier) {
54
+ if (afterValue)
55
+ return null;
56
+ const word = identifier[0];
57
+ rest = rest.slice(word.length);
58
+ if (LITERAL_KEYWORDS.has(word)) {
59
+ afterValue = true;
60
+ continue;
61
+ }
62
+ if (word !== 'state')
63
+ return null;
64
+ let hasProperty = false;
65
+ let property = /^\s*\.\s*([A-Za-z_$][\w$]*)/.exec(rest);
66
+ while (property) {
67
+ if (BLOCKED_PROPERTIES.has(property[1]))
68
+ return null;
69
+ hasProperty = true;
70
+ rest = rest.slice(property[0].length);
71
+ property = /^\s*\.\s*([A-Za-z_$][\w$]*)/.exec(rest);
72
+ }
73
+ if (!hasProperty)
74
+ return null;
75
+ readsState = true;
76
+ afterValue = true;
77
+ continue;
78
+ }
79
+ const literalLength = NUMBER.exec(rest)?.[0].length || getStringLiteralLength(rest);
80
+ if (literalLength) {
81
+ if (afterValue)
82
+ return null;
83
+ rest = rest.slice(literalLength);
84
+ afterValue = true;
85
+ continue;
86
+ }
87
+ if (rest[0] === '(') {
88
+ if (afterValue)
89
+ return null;
90
+ depth++;
91
+ rest = rest.slice(1);
92
+ continue;
93
+ }
94
+ if (rest[0] === ')') {
95
+ if (depth === 0)
96
+ return null;
97
+ depth--;
98
+ rest = rest.slice(1);
99
+ afterValue = true;
100
+ continue;
101
+ }
102
+ const operator = OPERATORS.find(op => rest.startsWith(op));
103
+ if (!operator)
104
+ return null;
105
+ // `?.` is optional chaining (a property access we do not handle)
106
+ if (operator === '?' && rest[1] === '.')
107
+ return null;
108
+ // `++` / `--` would write to state
109
+ if ((operator === '+' || operator === '-') && rest[1] === operator) {
110
+ return null;
111
+ }
112
+ rest = rest.slice(operator.length);
113
+ afterValue = false;
114
+ }
115
+ if (depth !== 0 || !readsState || !afterValue)
116
+ return null;
117
+ return expression.trim();
118
+ };
119
+ const RETURN_EXPRESSION = /^\s*return\s*\(([\s\S]*)\);\s*$/;
120
+ const VIRTUAL_INDEX_EXPRESSION = /^\s*var\s+_virtual_index\s*=\s*([\s\S]*?);?\s*return\s+_virtual_index\s*;?\s*$/;
121
+ /**
122
+ * Extracts the expression from code produced by `parseCode` (`return (expr);`) or
123
+ * from the editor's transpiled form (`var _virtual_index=expr;` followed by a return of it).
124
+ * (Mitosis treats any file containing a literal "return" + " _virtual_index" as a function body.)
125
+ */
126
+ export const getSafeStateExpressionFromCode = (code) => {
127
+ const match = RETURN_EXPRESSION.exec(code) || VIRTUAL_INDEX_EXPRESSION.exec(code);
128
+ return match ? getSafeStateExpression(match[1]) : null;
129
+ };
130
+ const isPrimitiveValue = (value) => value === null || typeof value !== 'object' && typeof value !== 'function';
131
+ const MAX_CACHED_EXPRESSIONS = 5000;
132
+ const COMPILED_EXPRESSIONS = createBoundedCache(MAX_CACHED_EXPRESSIONS);
133
+ /**
134
+ * The compiled expression for `code`, or `null` when `code` is not a safe state expression.
135
+ */
136
+ export const getSafeStateExpressionFn = (code) => {
137
+ let fn = COMPILED_EXPRESSIONS.get(code);
138
+ if (fn === undefined) {
139
+ const expression = getSafeStateExpressionFromCode(code);
140
+ fn = null;
141
+ if (expression) {
142
+ try {
143
+ fn = new Function('state', `return (${expression});`);
144
+ }
145
+ catch (e) {
146
+ fn = null;
147
+ }
148
+ }
149
+ COMPILED_EXPRESSIONS.set(code, fn);
150
+ }
151
+ return fn;
152
+ };
153
+ const UNSAFE_STATE_READ = Symbol('unsafe state read');
154
+ const isPlainContainer = (value) => {
155
+ const prototype = Object.getPrototypeOf(value);
156
+ return prototype === Object.prototype || prototype === Array.prototype || prototype === null;
157
+ };
158
+ const READ_ONLY_TRAPS = {
159
+ set: () => false,
160
+ deleteProperty: () => false
161
+ };
162
+ const findDescriptor = (target, prop) => {
163
+ for (let o = target; o; o = Object.getPrototypeOf(o)) {
164
+ const descriptor = Object.getOwnPropertyDescriptor(o, prop);
165
+ if (descriptor)
166
+ return descriptor;
167
+ }
168
+ return undefined;
169
+ };
170
+ /**
171
+ * A read-only view of local state over root state, without copying either.
172
+ *
173
+ * Plain objects and arrays are wrapped so every nested read is checked: accessors
174
+ * anywhere on the prototype chain, class instances and functions (which could run
175
+ * getters or custom `valueOf`/`toString`) abort the read. Each source object maps to
176
+ * one wrapper for the life of the view, so `===` still compares the underlying objects.
177
+ * Wrappers target a fresh empty container rather than the object itself, because proxy
178
+ * invariants forbid returning a wrapper for a frozen (non-configurable) property.
179
+ */
180
+ export const getReadOnlyStateView = (rootState, localState, isProxy) => {
181
+ // Reflecting on a Proxy (even `Object.getPrototypeOf`) runs its traps.
182
+ const isInspectable = (value) => !isProxy(value) && isPlainContainer(value);
183
+ if (!isInspectable(rootState) || localState && isProxy(localState)) {
184
+ throw UNSAFE_STATE_READ;
185
+ }
186
+ const wrappers = new WeakMap();
187
+ const guardStateValue = (value) => {
188
+ if (value === null || typeof value !== 'object') {
189
+ if (typeof value === 'function')
190
+ throw UNSAFE_STATE_READ;
191
+ return value;
192
+ }
193
+ if (!isInspectable(value))
194
+ throw UNSAFE_STATE_READ;
195
+ let wrapper = wrappers.get(value);
196
+ if (!wrapper) {
197
+ wrapper = new Proxy(Array.isArray(value) ? [] : {}, {
198
+ get: (_, prop) => readDataProperty(value, prop),
199
+ ...READ_ONLY_TRAPS
200
+ });
201
+ wrappers.set(value, wrapper);
202
+ }
203
+ return wrapper;
204
+ };
205
+ const readDataProperty = (target, prop) => {
206
+ const descriptor = findDescriptor(target, prop);
207
+ if (descriptor && (descriptor.get || descriptor.set)) {
208
+ throw UNSAFE_STATE_READ;
209
+ }
210
+ return guardStateValue(descriptor?.value);
211
+ };
212
+ return new Proxy({}, {
213
+ get: (_, prop) => localState && Object.prototype.hasOwnProperty.call(localState, prop) ? readDataProperty(localState, prop) : readDataProperty(rootState, prop),
214
+ ...READ_ONLY_TRAPS
215
+ });
216
+ };
217
+ export const NOT_EVALUATED = Symbol('not evaluated');
218
+ /**
219
+ * Evaluates `code` outside the sandbox when it is a safe state expression over plain
220
+ * data and produces a primitive. Returns `NOT_EVALUATED` when the sandbox must run it.
221
+ */
222
+ export const evaluateSafeStateExpression = (code, rootState, localState, isProxy) => {
223
+ const fn = getSafeStateExpressionFn(code);
224
+ if (!fn)
225
+ return NOT_EVALUATED;
226
+ let value;
227
+ try {
228
+ value = fn(getReadOnlyStateView(rootState, localState, isProxy));
229
+ }
230
+ catch {
231
+ // Unsafe reads and ordinary errors (e.g. a null-prototype object in `+`) both
232
+ // go to the regular evaluator, so behavior matches evaluation without the fast path.
233
+ return NOT_EVALUATED;
234
+ }
235
+ return isPrimitiveValue(value) ? value : NOT_EVALUATED;
236
+ };
@@ -1,5 +1,6 @@
1
1
  import { logger } from '../../helpers/logger';
2
2
  import { isNodeRuntime } from '../is-node-runtime';
3
+ let hasLoggedWarning = false;
3
4
  export const shouldForceBrowserRuntimeInNode = ({ shouldLogWarning }) => {
4
5
  if (!isNodeRuntime())
5
6
  return false;
@@ -7,7 +8,8 @@ export const shouldForceBrowserRuntimeInNode = ({ shouldLogWarning }) => {
7
8
  const isNode20 = process.version.startsWith('v20');
8
9
  const hasNoNodeSnapshotNodeOption = process.env.NODE_OPTIONS?.includes('--no-node-snapshot');
9
10
  if (isArm64 && isNode20 && !hasNoNodeSnapshotNodeOption) {
10
- if (shouldLogWarning) {
11
+ if (shouldLogWarning && !hasLoggedWarning) {
12
+ hasLoggedWarning = true;
11
13
  logger.log(`Skipping usage of \`isolated-vm\` to avoid crashes in Node v20 on an arm64 machine.
12
14
  If you would like to use the \`isolated-vm\` package on this machine, please provide the \`NODE_OPTIONS=--no-node-snapshot\` config to your Node process.
13
15
  See https://github.com/BuilderIO/builder/blob/main/packages/sdks/README.md#node-v20--m1-macs-apple-silicon-support for more information.
@@ -1,2 +1,6 @@
1
1
  import type { BuilderBlock } from '../types/builder-block';
2
+ /**
3
+ * Returns the block with localized values in `component.options` resolved.
4
+ * The block is returned unchanged (same object) when it has none.
5
+ */
2
6
  export declare function resolveLocalizedValues(block: BuilderBlock, locale: string | undefined): BuilderBlock;
@@ -1,37 +1,76 @@
1
- import { traverse } from './traverse';
2
1
  function isLocalizedField(value) {
3
2
  return value && typeof value === 'object' && value['@type'] === '@builder.io/core:LocalizedValue';
4
3
  }
5
- function containsLocalizedValues(data) {
6
- if (!data || !Object.getOwnPropertyNames(data).length) {
7
- return false;
4
+ const isPlainObjectOrArray = (value) => {
5
+ if (Array.isArray(value))
6
+ return true;
7
+ const proto = Object.getPrototypeOf(value);
8
+ return proto === Object.prototype || proto === null;
9
+ };
10
+ /**
11
+ * Returns `value` with every localized value replaced by its `locale` entry.
12
+ * Only the objects and arrays on the way to a replaced value are copied: the
13
+ * input is never mutated, and a value with nothing localized is returned as-is.
14
+ */
15
+ function resolveLocalized(value, locale, seen) {
16
+ if (value === null || typeof value !== 'object')
17
+ return value;
18
+ if (isLocalizedField(value)) {
19
+ return resolveLocalized(value[locale] ?? undefined, locale, seen);
8
20
  }
9
- let hasLocalizedValues = false;
10
- traverse(data, value => {
11
- if (isLocalizedField(value)) {
12
- hasLocalizedValues = true;
13
- return;
21
+ if (!isPlainObjectOrArray(value))
22
+ return value;
23
+ if (seen.has(value))
24
+ return seen.get(value);
25
+ seen.set(value, value);
26
+ let copy = null;
27
+ if (Array.isArray(value)) {
28
+ for (let i = 0; i < value.length; i++) {
29
+ const next = resolveLocalized(value[i], locale, seen);
30
+ if (next !== value[i]) {
31
+ if (!copy)
32
+ copy = value.slice();
33
+ copy[i] = next;
34
+ }
14
35
  }
15
- });
16
- return hasLocalizedValues;
17
- }
18
- function extractLocalizedValues(data, locale) {
19
- if (!data || !Object.getOwnPropertyNames(data).length) {
20
- return {};
21
36
  }
22
- traverse(data, (value, update) => {
23
- if (isLocalizedField(value)) {
24
- update(value[locale] ?? undefined);
37
+ else {
38
+ for (const key of Object.keys(value)) {
39
+ const next = resolveLocalized(value[key], locale, seen);
40
+ if (next !== value[key]) {
41
+ if (!copy)
42
+ copy = {
43
+ ...value
44
+ };
45
+ copy[key] = next;
46
+ }
25
47
  }
26
- });
27
- return data;
48
+ }
49
+ const result = copy ?? value;
50
+ seen.set(value, result);
51
+ return result;
28
52
  }
53
+ /**
54
+ * Returns the block with localized values in `component.options` resolved.
55
+ * The block is returned unchanged (same object) when it has none.
56
+ */
29
57
  export function resolveLocalizedValues(block, locale) {
30
- if (block.component?.options && containsLocalizedValues(block.component?.options)) {
31
- if (!locale) {
32
- console.warn('[Builder.io] In order to use localized fields in Builder, you must pass a locale prop to the BuilderComponent or to options object while fetching the content to resolve localized fields. Learn more: https://www.builder.io/c/docs/localization-inline#targeting-and-inline-localization');
33
- }
34
- block.component.options = extractLocalizedValues(block.component.options, locale ?? 'Default');
58
+ const options = block.component?.options;
59
+ if (!options || typeof options !== 'object' || isLocalizedField(options)) {
60
+ return block;
35
61
  }
36
- return block;
62
+ const resolvedOptions = resolveLocalized(options, locale ?? 'Default', new WeakMap());
63
+ if (resolvedOptions === options) {
64
+ return block;
65
+ }
66
+ if (!locale) {
67
+ console.warn('[Builder.io] In order to use localized fields in Builder, you must pass a locale prop to the BuilderComponent or to options object while fetching the content to resolve localized fields. Learn more: https://www.builder.io/c/docs/localization-inline#targeting-and-inline-localization');
68
+ }
69
+ return {
70
+ ...block,
71
+ component: {
72
+ ...block.component,
73
+ options: resolvedOptions
74
+ }
75
+ };
37
76
  }
@@ -34,9 +34,10 @@ const extractRelevantRootBlockProperties = (block) => {
34
34
  };
35
35
  };
36
36
  export function getBlockProperties({ block, context }) {
37
+ const { _newProperty, ...blockProperties } = block.properties || {};
37
38
  const properties = {
38
39
  ...extractRelevantRootBlockProperties(block),
39
- ...block.properties,
40
+ ...blockProperties,
40
41
  'builder-id': block.id,
41
42
  style: getStyle({
42
43
  block,
@@ -1,6 +1,5 @@
1
1
  import type { BuilderContextInterface } from '../context/types';
2
2
  import type { BuilderBlock } from '../types/builder-block';
3
- export declare function deepCloneWithConditions<T = any>(obj: T): T;
4
3
  export declare function getProcessedBlock({ block, context, localState, rootState, rootSetState }: {
5
4
  block: BuilderBlock;
6
5
  } & Pick<BuilderContextInterface, 'localState' | 'context' | 'rootState' | 'rootSetState'>): BuilderBlock;
@@ -1,64 +1,25 @@
1
- import { TARGET } from '../constants/target';
2
- import { omit } from '../helpers/omit';
3
1
  import { evaluate } from './evaluate/index';
4
2
  import { resolveLocalizedValues } from './extract-localized-values';
5
- import { fastClone } from './fast-clone';
6
- import { set } from './set';
3
+ import { isEditingOrPreviewing } from './is-editing-or-previewing';
4
+ import { setCopyOnWrite } from './set';
7
5
  import { transformBlock } from './transform-block';
8
- // Deep clone a block but without cloning any child blocks
9
- export function deepCloneWithConditions(obj) {
10
- if (obj === null || typeof obj !== 'object') {
11
- return obj;
12
- }
13
- if (Array.isArray(obj)) {
14
- return obj.map((item) => deepCloneWithConditions(item));
15
- }
16
- if (obj['@type'] === '@builder.io/sdk:Element') {
17
- return obj;
18
- }
19
- const clonedObj = {};
20
- for (const key in obj) {
21
- if (key !== 'meta' && Object.prototype.hasOwnProperty.call(obj, key)) {
22
- clonedObj[key] = deepCloneWithConditions(obj[key]);
23
- }
24
- }
25
- return clonedObj;
26
- }
27
- const IS_SDK_WITHOUT_CACHED_PROCESSED_BLOCK = ['svelte', 'vue', 'angular', 'qwik', 'solid'].includes(TARGET);
28
- const getCopy = (block) => {
29
- if (IS_SDK_WITHOUT_CACHED_PROCESSED_BLOCK) {
30
- const copy = fastClone(block);
31
- const copied = {
32
- ...copy,
33
- properties: {
34
- ...copy.properties
35
- },
36
- actions: {
37
- ...copy.actions
38
- }
39
- };
40
- return copied;
41
- }
42
- else {
43
- const copy = deepCloneWithConditions(omit(block, 'children', 'meta'));
44
- return {
45
- ...copy,
46
- properties: {
47
- ...copy.properties
48
- },
49
- actions: {
50
- ...copy.actions
51
- },
52
- children: block.children,
53
- meta: block.meta
54
- };
55
- }
56
- };
57
6
  const evaluateBindings = ({ block, context, localState, rootState, rootSetState }) => {
58
7
  if (!block.bindings) {
59
8
  return block;
60
9
  }
61
- const copied = getCopy(block);
10
+ // Only objects along a bound path are copied: the block (and content) passed in is never mutated.
11
+ const copied = new WeakSet();
12
+ const copy = {
13
+ ...block,
14
+ properties: {
15
+ ...block.properties
16
+ },
17
+ actions: {
18
+ ...block.actions
19
+ }
20
+ };
21
+ copied.add(copy.properties);
22
+ copied.add(copy.actions);
62
23
  for (const binding in block.bindings) {
63
24
  const expression = block.bindings[binding];
64
25
  const value = evaluate({
@@ -68,11 +29,32 @@ const evaluateBindings = ({ block, context, localState, rootState, rootSetState
68
29
  rootSetState,
69
30
  context
70
31
  });
71
- set(copied, binding, value);
32
+ setCopyOnWrite(copy, binding, value, copied);
72
33
  }
73
- return copied;
34
+ return copy;
35
+ };
36
+ /**
37
+ * A block without bindings processes to the same result for a given locale, so
38
+ * it is computed once per block object instead of on every render.
39
+ * Only the latest locale is kept, so a block holds at most one cached result.
40
+ */
41
+ const PROCESSED_BLOCKS_WITHOUT_BINDINGS = new WeakMap();
42
+ const hasBindings = (block) => {
43
+ if (!block.bindings)
44
+ return false;
45
+ for (const _ in block.bindings)
46
+ return true;
47
+ return false;
74
48
  };
75
49
  export function getProcessedBlock({ block, context, localState, rootState, rootSetState }) {
50
+ const locale = rootState.locale;
51
+ const canUseCache = !hasBindings(block) && typeof block === 'object' && block !== null && !isEditingOrPreviewing();
52
+ const cacheKey = locale ?? '';
53
+ if (canUseCache) {
54
+ const cached = PROCESSED_BLOCKS_WITHOUT_BINDINGS.get(block);
55
+ if (cached && cached.locale === cacheKey)
56
+ return cached.block;
57
+ }
76
58
  let transformedBlock = transformBlock(block);
77
59
  transformedBlock = evaluateBindings({
78
60
  block: transformedBlock,
@@ -81,6 +63,12 @@ export function getProcessedBlock({ block, context, localState, rootState, rootS
81
63
  rootSetState,
82
64
  context
83
65
  });
84
- transformedBlock = resolveLocalizedValues(transformedBlock, rootState.locale);
66
+ transformedBlock = resolveLocalizedValues(transformedBlock, locale);
67
+ if (canUseCache) {
68
+ PROCESSED_BLOCKS_WITHOUT_BINDINGS.set(block, {
69
+ locale: cacheKey,
70
+ block: transformedBlock
71
+ });
72
+ }
85
73
  return transformedBlock;
86
74
  }
@@ -0,0 +1 @@
1
+ export declare const isEditingOrPreviewing: () => boolean;
@@ -0,0 +1,3 @@
1
+ import { isEditing } from './is-editing';
2
+ import { isPreviewing } from './is-previewing';
3
+ export const isEditingOrPreviewing = () => isEditing() || isPreviewing();
@@ -2,6 +2,34 @@
2
2
  * Minimal implementation of lodash's _.set
3
3
  * https://lodash.com/docs/4.17.15#set
4
4
  *
5
- * See ./set.test.ts for usage examples
5
+ * Mutates `obj` in place, creating missing objects/arrays along the path:
6
+ *
7
+ * const obj = { a: { b: 1 } };
8
+ * set(obj, 'a.c', 2); // obj is { a: { b: 1, c: 2 } }
9
+ * set(obj, 'list.0', 'x'); // obj.list is ['x']
10
+ *
11
+ * See ./set.test.ts for more usage examples
6
12
  */
7
13
  export declare const set: (obj: any, _path: string | string[], value: any) => any;
14
+ /**
15
+ * Like `set`, but every object or array along `path` that is not in `copied`
16
+ * is shallow-copied before being written to, so objects shared with the
17
+ * original are never mutated. `obj` itself must already be a copy.
18
+ *
19
+ * const block = { options: { text: 'a' }, style: { color: 'red' } };
20
+ * const copy = { ...block };
21
+ * setCopyOnWrite(copy, 'options.text', 'b', new WeakSet());
22
+ * // copy.options is a new object: { text: 'b' }
23
+ * // block.options.text is still 'a'
24
+ * // copy.style === block.style (untouched branches stay shared)
25
+ */
26
+ export declare const setCopyOnWrite: (obj: any, _path: string, value: any, copied: WeakSet<object>) => any;
27
+ /**
28
+ * Deletes the property at `path`, the inverse of `set` for an array path.
29
+ * Mutates `obj` in place and does nothing when a parent is missing:
30
+ *
31
+ * const obj = { a: { b: 1, c: 2 } };
32
+ * unset(obj, ['a', 'b']); // obj is { a: { c: 2 } }
33
+ * unset(obj, ['x', 'y']); // no-op
34
+ */
35
+ export declare const unset: (obj: any, path: string[]) => any;
@@ -1,17 +1,98 @@
1
+ const parsePath = (path) => path.toString().match(/[^.[\]]+/g);
2
+ const isArrayIndex = (key) => Math.abs(Number(key)) >> 0 === +key;
3
+ /**
4
+ * `__proto__` is never allowed. `constructor` and `prototype` are allowed only as
5
+ * the final key, where they are written as ordinary own properties.
6
+ */
7
+ const hasUnsafeSegment = (path) => path.some((key, i) => key === '__proto__' || i < path.length - 1 && (key === 'constructor' || key === 'prototype'));
8
+ /**
9
+ * Own properties only, so a path can never walk into a value shared through a
10
+ * prototype (e.g. `Object.prototype.toString`) and mutate it.
11
+ */
12
+ const getOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key) ? obj[key] : undefined;
1
13
  /**
2
14
  * Minimal implementation of lodash's _.set
3
15
  * https://lodash.com/docs/4.17.15#set
4
16
  *
5
- * See ./set.test.ts for usage examples
17
+ * Mutates `obj` in place, creating missing objects/arrays along the path:
18
+ *
19
+ * const obj = { a: { b: 1 } };
20
+ * set(obj, 'a.c', 2); // obj is { a: { b: 1, c: 2 } }
21
+ * set(obj, 'list.0', 'x'); // obj.list is ['x']
22
+ *
23
+ * See ./set.test.ts for more usage examples
6
24
  */
7
25
  export const set = (obj, _path, value) => {
8
26
  if (Object(obj) !== obj) {
9
27
  return obj;
10
28
  }
11
- const path = Array.isArray(_path) ? _path : _path.toString().match(/[^.[\]]+/g);
12
- if (!path || path.some((key, i) => key === '__proto__' || i < path.length - 1 && (key === 'constructor' || key === 'prototype'))) {
29
+ const path = Array.isArray(_path) ? _path : parsePath(_path);
30
+ if (!path || hasUnsafeSegment(path)) {
13
31
  return obj;
14
32
  }
15
- path.slice(0, -1).reduce((a, c, i) => Object(a[c]) === a[c] ? a[c] : a[c] = Math.abs(Number(path[i + 1])) >> 0 === +path[i + 1] ? [] : {}, obj)[path[path.length - 1]] = value;
33
+ path.slice(0, -1).reduce((a, c, i) => {
34
+ const next = getOwn(a, c);
35
+ return Object(next) === next ? next : a[c] = isArrayIndex(path[i + 1]) ? [] : {};
36
+ }, obj)[path[path.length - 1]] = value;
37
+ return obj;
38
+ };
39
+ /**
40
+ * Like `set`, but every object or array along `path` that is not in `copied`
41
+ * is shallow-copied before being written to, so objects shared with the
42
+ * original are never mutated. `obj` itself must already be a copy.
43
+ *
44
+ * const block = { options: { text: 'a' }, style: { color: 'red' } };
45
+ * const copy = { ...block };
46
+ * setCopyOnWrite(copy, 'options.text', 'b', new WeakSet());
47
+ * // copy.options is a new object: { text: 'b' }
48
+ * // block.options.text is still 'a'
49
+ * // copy.style === block.style (untouched branches stay shared)
50
+ */
51
+ export const setCopyOnWrite = (obj, _path, value, copied) => {
52
+ if (Object(obj) !== obj) {
53
+ return obj;
54
+ }
55
+ const path = parsePath(_path);
56
+ if (!path || hasUnsafeSegment(path)) {
57
+ return obj;
58
+ }
59
+ copied.add(obj);
60
+ let current = obj;
61
+ for (let i = 0; i < path.length - 1; i++) {
62
+ const key = path[i];
63
+ let next = getOwn(current, key);
64
+ if (Object(next) !== next) {
65
+ next = isArrayIndex(path[i + 1]) ? [] : {};
66
+ copied.add(next);
67
+ current[key] = next;
68
+ }
69
+ else if (typeof next === 'object' && !copied.has(next)) {
70
+ next = Array.isArray(next) ? next.slice() : {
71
+ ...next
72
+ };
73
+ copied.add(next);
74
+ current[key] = next;
75
+ }
76
+ current = next;
77
+ }
78
+ current[path[path.length - 1]] = value;
79
+ return obj;
80
+ };
81
+ /**
82
+ * Deletes the property at `path`, the inverse of `set` for an array path.
83
+ * Mutates `obj` in place and does nothing when a parent is missing:
84
+ *
85
+ * const obj = { a: { b: 1, c: 2 } };
86
+ * unset(obj, ['a', 'b']); // obj is { a: { c: 2 } }
87
+ * unset(obj, ['x', 'y']); // no-op
88
+ */
89
+ export const unset = (obj, path) => {
90
+ if (Object(obj) !== obj || !path.length || hasUnsafeSegment(path)) {
91
+ return obj;
92
+ }
93
+ const parent = path.slice(0, -1).reduce((a, c) => Object(a) === a ? getOwn(a, c) : undefined, obj);
94
+ if (Object(parent) === parent) {
95
+ delete parent[path[path.length - 1]];
96
+ }
16
97
  return obj;
17
98
  };
@@ -0,0 +1,4 @@
1
+ export declare const createBoundedCache: <V>(max: number) => {
2
+ get: (key: string) => V | undefined;
3
+ set: (key: string, value: V) => void;
4
+ };