@weaverse/react 5.23.0 → 5.24.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/dist/index.mjs CHANGED
@@ -1,495 +1,490 @@
1
- // src/index.ts
2
- export * from "@weaverse/core";
3
-
4
- // src/context.ts
5
- import { createContext } from "react";
6
- var WeaverseContext = createContext({});
7
- var WeaverseContextProvider = WeaverseContext.Provider;
8
- var WeaverseItemContext = createContext({ id: "", parentId: "" });
9
-
10
- // src/hooks.ts
11
1
  import { Weaverse } from "@weaverse/core";
12
- import { useContext } from "react";
13
-
14
- // src/utils/data-connector.ts
15
- var TEMPLATE_REGEX = /\{\{([^}]+)\}\}/g;
16
- var ARRAY_INDEX_REGEX = /^(\w+)\[(\d+)\]$/;
17
- var CACHE_EVICTION_RATIO = 0.25;
2
+ import React, { createContext, createElement, memo, useContext, useEffect, useMemo, useRef } from "react";
3
+ import clsx from "clsx";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ export * from "@weaverse/core";
6
+ //#region src/context.ts
7
+ /** Provides the active Weaverse runtime to React components. */
8
+ let WeaverseContext = createContext({});
9
+ /** Provider component for supplying the active Weaverse runtime. */
10
+ let WeaverseContextProvider = WeaverseContext.Provider;
11
+ /** Identifies the current rendered item and its parent item. */
12
+ let WeaverseItemContext = createContext({
13
+ id: "",
14
+ parentId: ""
15
+ });
16
+ //#endregion
17
+ //#region src/utils/data-connector.ts
18
+ const TEMPLATE_REGEX = /\{\{([^}]+)\}\}/g;
19
+ const ARRAY_INDEX_REGEX = /^(\w+)\[(\d+)\]$/;
20
+ const CACHE_EVICTION_RATIO = .25;
21
+ /**
22
+ * Validates that a property name is safe to access (prevents prototype pollution)
23
+ * Enhanced to handle route-specific patterns with special characters
24
+ */
18
25
  function isSafePropertyName(key) {
19
- const dangerousPatterns = ["__proto__", "constructor", "prototype"];
20
- return !dangerousPatterns.includes(key);
26
+ return ![
27
+ "__proto__",
28
+ "constructor",
29
+ "prototype"
30
+ ].includes(key);
21
31
  }
32
+ /**
33
+ * Parses a template path into route key and property path components
34
+ * Handles complex routes like: routes/($locale).blogs.$blogHandle.$articleHandle
35
+ * Enhanced with caching for improved performance
36
+ *
37
+ * @param path - The full path from template (e.g., "routes/($locale)._index.weaverseData.page.name")
38
+ * @param dataContext - Available route data context
39
+ * @returns Object with routeKey and propertyPath
40
+ */
22
41
  function parseRouteAndProperty(path, dataContext) {
23
- if (!dataContext || typeof path !== "string") {
24
- return { routeKey: null, propertyPath: path };
25
- }
26
- const trimmedPath = path.trim();
27
- const dataHash = getDataContextHash(dataContext);
28
- const cacheKey = `${trimmedPath}::${dataHash}`;
29
- const cachedResult = routeParsingCache.get(cacheKey);
30
- if (cachedResult) {
31
- cachedResult.timestamp = Date.now();
32
- return {
33
- routeKey: cachedResult.routeKey,
34
- propertyPath: cachedResult.propertyPath
35
- };
36
- }
37
- const availableRoutes = Object.keys(dataContext).sort(
38
- (a, b) => b.length - a.length
39
- );
40
- let result = {
41
- routeKey: null,
42
- propertyPath: trimmedPath
43
- };
44
- let found = false;
45
- for (const routeKey of availableRoutes) {
46
- if (trimmedPath === routeKey) {
47
- result = { routeKey, propertyPath: "" };
48
- found = true;
49
- break;
50
- }
51
- if (trimmedPath.startsWith(`${routeKey}.`)) {
52
- const propertyPath = trimmedPath.slice(routeKey.length + 1);
53
- result = { routeKey, propertyPath };
54
- found = true;
55
- break;
56
- }
57
- }
58
- if (!found) {
59
- result = { routeKey: null, propertyPath: trimmedPath };
60
- }
61
- if (routeParsingCache.size >= ROUTE_PARSING_CACHE_MAX_SIZE) {
62
- const entries = Array.from(routeParsingCache.entries());
63
- entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
64
- const toRemove = Math.floor(entries.length * CACHE_EVICTION_RATIO);
65
- for (let i = 0; i < toRemove; i++) {
66
- routeParsingCache.delete(entries[i][0]);
67
- }
68
- }
69
- routeParsingCache.set(cacheKey, {
70
- routeKey: result.routeKey,
71
- propertyPath: result.propertyPath,
72
- timestamp: Date.now()
73
- });
74
- return result;
42
+ if (!dataContext || typeof path !== "string") return {
43
+ routeKey: null,
44
+ propertyPath: path
45
+ };
46
+ const trimmedPath = path.trim();
47
+ const cacheKey = `${trimmedPath}::${getDataContextHash(dataContext)}`;
48
+ const cachedResult = routeParsingCache.get(cacheKey);
49
+ if (cachedResult) {
50
+ cachedResult.timestamp = Date.now();
51
+ return {
52
+ routeKey: cachedResult.routeKey,
53
+ propertyPath: cachedResult.propertyPath
54
+ };
55
+ }
56
+ const availableRoutes = Object.keys(dataContext).sort((a, b) => b.length - a.length);
57
+ let result = {
58
+ routeKey: null,
59
+ propertyPath: trimmedPath
60
+ };
61
+ let found = false;
62
+ for (const routeKey of availableRoutes) {
63
+ if (trimmedPath === routeKey) {
64
+ result = {
65
+ routeKey,
66
+ propertyPath: ""
67
+ };
68
+ found = true;
69
+ break;
70
+ }
71
+ if (trimmedPath.startsWith(`${routeKey}.`)) {
72
+ result = {
73
+ routeKey,
74
+ propertyPath: trimmedPath.slice(routeKey.length + 1)
75
+ };
76
+ found = true;
77
+ break;
78
+ }
79
+ }
80
+ if (!found) result = {
81
+ routeKey: null,
82
+ propertyPath: trimmedPath
83
+ };
84
+ if (routeParsingCache.size >= ROUTE_PARSING_CACHE_MAX_SIZE) {
85
+ const entries = Array.from(routeParsingCache.entries());
86
+ entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
87
+ const toRemove = Math.floor(entries.length * CACHE_EVICTION_RATIO);
88
+ for (let i = 0; i < toRemove; i++) routeParsingCache.delete(entries[i][0]);
89
+ }
90
+ routeParsingCache.set(cacheKey, {
91
+ routeKey: result.routeKey,
92
+ propertyPath: result.propertyPath,
93
+ timestamp: Date.now()
94
+ });
95
+ return result;
75
96
  }
76
- var templateCache = /* @__PURE__ */ new Map();
77
- var MAX_CACHE_SIZE = 100;
78
- var fallbackCache = /* @__PURE__ */ new Map();
79
- var FALLBACK_CACHE_MAX_SIZE = 50;
80
- var routeParsingCache = /* @__PURE__ */ new Map();
81
- var ROUTE_PARSING_CACHE_MAX_SIZE = 100;
82
- var dataContextHashCache = /* @__PURE__ */ new WeakMap();
83
- var hashCounter = 0;
97
+ const templateCache = /* @__PURE__ */ new Map();
98
+ const MAX_CACHE_SIZE = 100;
99
+ const fallbackCache = /* @__PURE__ */ new Map();
100
+ const FALLBACK_CACHE_MAX_SIZE = 50;
101
+ const routeParsingCache = /* @__PURE__ */ new Map();
102
+ const ROUTE_PARSING_CACHE_MAX_SIZE = 100;
103
+ const dataContextHashCache = /* @__PURE__ */ new WeakMap();
104
+ let hashCounter = 0;
105
+ /**
106
+ * Safely gets a nested value from an object using dot notation path
107
+ * Supports array indexing with [index] syntax
108
+ * @param obj - The object to traverse
109
+ * @param path - Dot notation path (e.g., "user.name" or "items[0].title")
110
+ * @param visited - Set to track visited objects for circular reference protection
111
+ * @returns The resolved value or undefined if not found
112
+ */
84
113
  function getNestedValue(obj, path, visited = /* @__PURE__ */ new WeakSet()) {
85
- return path.split(".").reduce((current, key) => {
86
- if (!current || typeof current !== "object") {
87
- return;
88
- }
89
- if (visited.has(current)) {
90
- console.warn("Circular reference detected in data connector path:", path);
91
- return;
92
- }
93
- visited.add(current);
94
- const arrayMatch = key.match(ARRAY_INDEX_REGEX);
95
- if (arrayMatch) {
96
- const [, arrayKey, index] = arrayMatch;
97
- const arrayObj = current[arrayKey];
98
- if (Array.isArray(arrayObj)) {
99
- const indexNum = Number.parseInt(index, 10);
100
- if (indexNum >= 0 && indexNum < arrayObj.length) {
101
- return arrayObj[indexNum];
102
- }
103
- }
104
- return;
105
- }
106
- if (!isSafePropertyName(key)) {
107
- console.warn(`Unsafe property access attempted: ${key}`);
108
- return;
109
- }
110
- return current !== null && key in current ? current[key] : void 0;
111
- }, obj);
114
+ return path.split(".").reduce((current, key) => {
115
+ if (!current || typeof current !== "object") return;
116
+ if (visited.has(current)) {
117
+ console.warn("Circular reference detected in data connector path:", path);
118
+ return;
119
+ }
120
+ visited.add(current);
121
+ const arrayMatch = key.match(ARRAY_INDEX_REGEX);
122
+ if (arrayMatch) {
123
+ const [, arrayKey, index] = arrayMatch;
124
+ const arrayObj = current[arrayKey];
125
+ if (Array.isArray(arrayObj)) {
126
+ const indexNum = Number.parseInt(index, 10);
127
+ if (indexNum >= 0 && indexNum < arrayObj.length) return arrayObj[indexNum];
128
+ }
129
+ return;
130
+ }
131
+ if (!isSafePropertyName(key)) {
132
+ console.warn(`Unsafe property access attempted: ${key}`);
133
+ return;
134
+ }
135
+ return current !== null && key in current ? current[key] : void 0;
136
+ }, obj);
112
137
  }
138
+ /**
139
+ * Sanitizes a string value to prevent XSS attacks
140
+ * @param value - The value to sanitize
141
+ * @returns Sanitized string safe for HTML rendering
142
+ */
113
143
  function sanitizeValue(value) {
114
- if (value === null || value === void 0) {
115
- return "";
116
- }
117
- const stringValue = String(value);
118
- return stringValue.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;").replace(/\//g, "&#x2F;");
144
+ if (value === null || value === void 0) return "";
145
+ return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;").replace(/\//g, "&#x2F;");
119
146
  }
147
+ /**
148
+ * Creates a stable hash for dataContext object
149
+ * Uses WeakMap to cache hash per object reference (no serialization needed)
150
+ */
120
151
  function getDataContextHash(data) {
121
- if (!dataContextHashCache.has(data)) {
122
- dataContextHashCache.set(data, `ctx_${++hashCounter}`);
123
- }
124
- const hash = dataContextHashCache.get(data);
125
- return hash || `ctx_${++hashCounter}`;
152
+ if (!dataContextHashCache.has(data)) dataContextHashCache.set(data, `ctx_${++hashCounter}`);
153
+ return dataContextHashCache.get(data) || `ctx_${++hashCounter}`;
126
154
  }
155
+ /**
156
+ * Creates a safe cache key from content and data
157
+ * Uses stable hashing instead of JSON.stringify for massive performance gain
158
+ */
127
159
  function createCacheKey(content, data) {
128
- const dataHash = getDataContextHash(data);
129
- return `${content}:${dataHash}`;
160
+ return `${content}:${getDataContextHash(data)}`;
130
161
  }
162
+ /**
163
+ * Resolves data from the flat route-keyed data context
164
+ * Enhanced to support complex route patterns with special characters
165
+ * Supports: {{root.layout.shop.name}}, {{routes/($locale)._index.data}}, {{routes/($locale).blogs.$handle.$article.content}}
166
+ */
131
167
  function resolveDataFromContext(path, dataContext) {
132
- if (!dataContext) {
133
- return;
134
- }
135
- const trimmedPath = path.trim();
136
- const { routeKey, propertyPath } = parseRouteAndProperty(
137
- trimmedPath,
138
- dataContext
139
- );
140
- if (routeKey && dataContext[routeKey]) {
141
- const routeData = dataContext[routeKey];
142
- if (typeof routeData === "object" && routeData !== null) {
143
- if (propertyPath) {
144
- return getNestedValue(routeData, propertyPath);
145
- }
146
- return routeData;
147
- }
148
- }
149
- const cachedResult = fallbackCache.get(trimmedPath);
150
- if (cachedResult && dataContext[cachedResult.routeKey]) {
151
- const value = getNestedValue(
152
- dataContext[cachedResult.routeKey],
153
- trimmedPath
154
- );
155
- if (value !== void 0) {
156
- cachedResult.timestamp = Date.now();
157
- return value;
158
- }
159
- }
160
- const priorityRoutes = ["root", "routes/product", "routes/collection"];
161
- const allRoutes = [
162
- ...priorityRoutes,
163
- ...Object.keys(dataContext).filter((k) => !priorityRoutes.includes(k))
164
- ];
165
- for (const routeKey2 of allRoutes) {
166
- const routeData = dataContext[routeKey2];
167
- if (routeData && typeof routeData === "object" && routeData !== null) {
168
- const value = getNestedValue(routeData, trimmedPath);
169
- if (value !== void 0) {
170
- if (fallbackCache.size >= FALLBACK_CACHE_MAX_SIZE) {
171
- const entries = Array.from(fallbackCache.entries());
172
- entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
173
- const toRemove = Math.floor(entries.length * CACHE_EVICTION_RATIO);
174
- for (let i = 0; i < toRemove; i++) {
175
- fallbackCache.delete(entries[i][0]);
176
- }
177
- }
178
- fallbackCache.set(trimmedPath, {
179
- routeKey: routeKey2,
180
- value,
181
- timestamp: Date.now()
182
- });
183
- return value;
184
- }
185
- }
186
- }
187
- return getNestedValue(dataContext, trimmedPath);
168
+ if (!dataContext) return;
169
+ const trimmedPath = path.trim();
170
+ const { routeKey, propertyPath } = parseRouteAndProperty(trimmedPath, dataContext);
171
+ if (routeKey && dataContext[routeKey]) {
172
+ const routeData = dataContext[routeKey];
173
+ if (typeof routeData === "object" && routeData !== null) {
174
+ if (propertyPath) return getNestedValue(routeData, propertyPath);
175
+ return routeData;
176
+ }
177
+ }
178
+ const cachedResult = fallbackCache.get(trimmedPath);
179
+ if (cachedResult && dataContext[cachedResult.routeKey]) {
180
+ const value = getNestedValue(dataContext[cachedResult.routeKey], trimmedPath);
181
+ if (value !== void 0) {
182
+ cachedResult.timestamp = Date.now();
183
+ return value;
184
+ }
185
+ }
186
+ const priorityRoutes = [
187
+ "root",
188
+ "routes/product",
189
+ "routes/collection"
190
+ ];
191
+ const allRoutes = [...priorityRoutes, ...Object.keys(dataContext).filter((k) => !priorityRoutes.includes(k))];
192
+ for (const routeKey of allRoutes) {
193
+ const routeData = dataContext[routeKey];
194
+ if (routeData && typeof routeData === "object" && routeData !== null) {
195
+ const value = getNestedValue(routeData, trimmedPath);
196
+ if (value !== void 0) {
197
+ if (fallbackCache.size >= FALLBACK_CACHE_MAX_SIZE) {
198
+ const entries = Array.from(fallbackCache.entries());
199
+ entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
200
+ const toRemove = Math.floor(entries.length * CACHE_EVICTION_RATIO);
201
+ for (let i = 0; i < toRemove; i++) fallbackCache.delete(entries[i][0]);
202
+ }
203
+ fallbackCache.set(trimmedPath, {
204
+ routeKey,
205
+ value,
206
+ timestamp: Date.now()
207
+ });
208
+ return value;
209
+ }
210
+ }
211
+ }
212
+ return getNestedValue(dataContext, trimmedPath);
188
213
  }
214
+ /**
215
+ * Replaces data connector placeholders in content with actual values from data context
216
+ * Supports {{path}} syntax with dot notation, array indexing, and route prefixes
217
+ *
218
+ * Enhanced syntax:
219
+ * - {{root.siteSettings.title}} - Access root route data
220
+ * - {{layout.navigation.items}} - Access layout route data
221
+ * - {{parent.category.name}} - Access parent route data
222
+ * - {{current.product.title}} - Access current route data
223
+ * - {{user.name}} - Auto-resolved from combined data (backward compatible)
224
+ *
225
+ * @param content - The content string containing {{}} placeholders
226
+ * @param dataContext - The enhanced WeaverseDataContext with route-keyed data
227
+ * @returns The content with placeholders replaced by actual values
228
+ */
189
229
  function replaceContentDataConnectors(content, dataContext) {
190
- if (!dataContext || typeof content !== "string") {
191
- return content;
192
- }
193
- try {
194
- const cacheKey = createCacheKey(content, dataContext);
195
- const cachedContent = templateCache.get(cacheKey);
196
- if (cachedContent) {
197
- return cachedContent;
198
- }
199
- const newContent = content.replace(TEMPLATE_REGEX, (match, path) => {
200
- try {
201
- const value = resolveDataFromContext(path, dataContext);
202
- if (value === void 0 || value === null) {
203
- return match;
204
- }
205
- return sanitizeValue(value);
206
- } catch (pathError) {
207
- console.warn(
208
- `Error resolving data connector path "${path}":`,
209
- pathError
210
- );
211
- return match;
212
- }
213
- });
214
- if (templateCache.size >= MAX_CACHE_SIZE) {
215
- const firstKey = templateCache.keys().next().value;
216
- templateCache.delete(firstKey);
217
- }
218
- templateCache.set(cacheKey, newContent);
219
- return newContent;
220
- } catch (error) {
221
- console.warn("Error processing data connectors:", error, { content });
222
- return content;
223
- }
230
+ if (!dataContext || typeof content !== "string") return content;
231
+ try {
232
+ const cacheKey = createCacheKey(content, dataContext);
233
+ const cachedContent = templateCache.get(cacheKey);
234
+ if (cachedContent) return cachedContent;
235
+ const newContent = content.replace(TEMPLATE_REGEX, (match, path) => {
236
+ try {
237
+ const value = resolveDataFromContext(path, dataContext);
238
+ if (value === void 0 || value === null) return match;
239
+ return sanitizeValue(value);
240
+ } catch (pathError) {
241
+ console.warn(`Error resolving data connector path "${path}":`, pathError);
242
+ return match;
243
+ }
244
+ });
245
+ if (templateCache.size >= MAX_CACHE_SIZE) {
246
+ const firstKey = templateCache.keys().next().value;
247
+ templateCache.delete(firstKey);
248
+ }
249
+ templateCache.set(cacheKey, newContent);
250
+ return newContent;
251
+ } catch (error) {
252
+ console.warn("Error processing data connectors:", error, { content });
253
+ return content;
254
+ }
224
255
  }
256
+ /**
257
+ * Fast check if data contains any {{...}} placeholders
258
+ * Returns true if any string value contains the pattern
259
+ * This is MUCH faster than processing and allows early exit
260
+ *
261
+ * IMPORTANT: Uses global TEMPLATE_REGEX, must reset lastIndex
262
+ */
225
263
  function hasPlaceholders(data, visited = /* @__PURE__ */ new WeakSet()) {
226
- if (typeof data === "string") {
227
- TEMPLATE_REGEX.lastIndex = 0;
228
- return TEMPLATE_REGEX.test(data);
229
- }
230
- if (typeof data !== "object" || data === null) {
231
- return false;
232
- }
233
- if (data instanceof Date) {
234
- return false;
235
- }
236
- if (visited.has(data)) {
237
- return false;
238
- }
239
- visited.add(data);
240
- try {
241
- if (Array.isArray(data)) {
242
- return data.some((item) => hasPlaceholders(item, visited));
243
- }
244
- for (const key of Object.keys(data)) {
245
- try {
246
- const value = data[key];
247
- if (hasPlaceholders(value, visited)) {
248
- return true;
249
- }
250
- } catch {
251
- }
252
- }
253
- return false;
254
- } finally {
255
- visited.delete(data);
256
- }
264
+ if (typeof data === "string") {
265
+ TEMPLATE_REGEX.lastIndex = 0;
266
+ return TEMPLATE_REGEX.test(data);
267
+ }
268
+ if (typeof data !== "object" || data === null) return false;
269
+ if (data instanceof Date) return false;
270
+ if (visited.has(data)) return false;
271
+ visited.add(data);
272
+ try {
273
+ if (Array.isArray(data)) return data.some((item) => hasPlaceholders(item, visited));
274
+ for (const key of Object.keys(data)) try {
275
+ const value = data[key];
276
+ if (hasPlaceholders(value, visited)) return true;
277
+ } catch {}
278
+ return false;
279
+ } finally {
280
+ visited.delete(data);
281
+ }
257
282
  }
283
+ /**
284
+ * Recursively replaces data connector placeholders in any data structure
285
+ * IMMUTABLE: Creates deep copies during processing to avoid mutating original data
286
+ * Handles strings, objects, arrays, and nested combinations
287
+ *
288
+ * PERFORMANCE: Now includes early exit if no placeholders detected
289
+ *
290
+ * @param data - The data to process (string, object, array, or primitive)
291
+ * @param dataContext - The data context for replacements
292
+ * @param visited - Set to track visited objects for circular reference protection
293
+ * @param forceClone - Internal flag to force cloning even if no placeholders (for parent immutability)
294
+ * @returns IMMUTABLE copy of the data with all string placeholders replaced
295
+ */
258
296
  function replaceContentDataConnectorsDeep(data, dataContext, visited = /* @__PURE__ */ new WeakSet(), forceClone = false) {
259
- if (!dataContext) {
260
- return data;
261
- }
262
- const dataHasPlaceholders = hasPlaceholders(data, /* @__PURE__ */ new WeakSet());
263
- if (!(dataHasPlaceholders || forceClone)) {
264
- return data;
265
- }
266
- if (typeof data === "string") {
267
- return replaceContentDataConnectors(data, dataContext);
268
- }
269
- if (typeof data !== "object" || data === null) {
270
- return data;
271
- }
272
- if (visited.has(data)) {
273
- console.warn(
274
- "Circular reference detected in deep data connector replacement"
275
- );
276
- return data;
277
- }
278
- visited.add(data);
279
- try {
280
- if (data instanceof Date) {
281
- if (forceClone || dataHasPlaceholders) {
282
- return new Date(data.getTime());
283
- }
284
- return data;
285
- }
286
- if (Array.isArray(data)) {
287
- const result2 = data.map(
288
- (item) => replaceContentDataConnectorsDeep(
289
- item,
290
- dataContext,
291
- visited,
292
- dataHasPlaceholders
293
- )
294
- );
295
- return result2;
296
- }
297
- const result = {};
298
- for (const key of Object.keys(data)) {
299
- try {
300
- const value = data[key];
301
- result[key] = replaceContentDataConnectorsDeep(
302
- value,
303
- dataContext,
304
- visited,
305
- dataHasPlaceholders
306
- );
307
- } catch (propertyError) {
308
- console.warn(`Error accessing property "${key}":`, propertyError);
309
- }
310
- }
311
- return result;
312
- } catch (error) {
313
- console.warn("Error in deep data connector replacement:", error);
314
- return data;
315
- } finally {
316
- visited.delete(data);
317
- }
297
+ if (!dataContext) return data;
298
+ const dataHasPlaceholders = hasPlaceholders(data, /* @__PURE__ */ new WeakSet());
299
+ if (!(dataHasPlaceholders || forceClone)) return data;
300
+ if (typeof data === "string") return replaceContentDataConnectors(data, dataContext);
301
+ if (typeof data !== "object" || data === null) return data;
302
+ if (visited.has(data)) {
303
+ console.warn("Circular reference detected in deep data connector replacement");
304
+ return data;
305
+ }
306
+ visited.add(data);
307
+ try {
308
+ if (data instanceof Date) {
309
+ if (forceClone || dataHasPlaceholders) return new Date(data.getTime());
310
+ return data;
311
+ }
312
+ if (Array.isArray(data)) return data.map((item) => replaceContentDataConnectorsDeep(item, dataContext, visited, dataHasPlaceholders));
313
+ const result = {};
314
+ for (const key of Object.keys(data)) try {
315
+ const value = data[key];
316
+ result[key] = replaceContentDataConnectorsDeep(value, dataContext, visited, dataHasPlaceholders);
317
+ } catch (propertyError) {
318
+ console.warn(`Error accessing property "${key}":`, propertyError);
319
+ }
320
+ return result;
321
+ } catch (error) {
322
+ console.warn("Error in deep data connector replacement:", error);
323
+ return data;
324
+ } finally {
325
+ visited.delete(data);
326
+ }
318
327
  }
319
-
320
- // src/hooks.ts
328
+ //#endregion
329
+ //#region src/hooks.ts
330
+ /**
331
+ * Returns the active Weaverse runtime from React context.
332
+ *
333
+ * @typeParam T - Runtime subtype expected by the integration.
334
+ */
321
335
  function useWeaverse() {
322
- let weaverse = useContext(WeaverseContext);
323
- return weaverse;
336
+ return useContext(WeaverseContext);
324
337
  }
325
- var useItemInstance = (id) => {
326
- let { id: currentId } = useContext(WeaverseItemContext);
327
- let instance = Weaverse.itemInstances.get(id || currentId);
328
- if (!instance) {
329
- console.log(`Item instance ${id} not found`);
330
- return null;
331
- }
332
- return instance;
338
+ /**
339
+ * Returns a Weaverse item instance by ID, or the current rendered item when no
340
+ * ID is provided.
341
+ *
342
+ * @param id - Optional item ID to look up.
343
+ * @returns The matching item instance, or `null` when it is unavailable.
344
+ */
345
+ let useItemInstance = (id) => {
346
+ let { id: currentId } = useContext(WeaverseItemContext);
347
+ let instance = Weaverse.itemInstances.get(id || currentId);
348
+ if (!instance) {
349
+ console.log(`Item instance ${id} not found`);
350
+ return null;
351
+ }
352
+ return instance;
333
353
  };
334
- var useParentInstance = () => {
335
- let { parentId } = useContext(WeaverseItemContext);
336
- return useItemInstance(parentId || "");
354
+ /**
355
+ * Returns the parent instance of the current rendered item.
356
+ *
357
+ * @returns The parent item instance. At the root, the empty parent ID falls
358
+ * back to the current item instance; returns `null` only when that lookup is
359
+ * unavailable.
360
+ */
361
+ let useParentInstance = () => {
362
+ let { parentId } = useContext(WeaverseItemContext);
363
+ return useItemInstance(parentId || "");
337
364
  };
338
- var useChildInstances = (id) => {
339
- let currentInstance = useItemInstance(id);
340
- if (!currentInstance) {
341
- return [];
342
- }
343
- let { itemInstances } = Weaverse;
344
- let {
345
- data: { children }
346
- } = currentInstance;
347
- return children.map(({ id: id2 }) => itemInstances.get(id2)).filter(Boolean);
365
+ /**
366
+ * Returns the direct child instances of an item.
367
+ *
368
+ * @param id - Optional parent item ID; defaults to the current rendered item.
369
+ */
370
+ let useChildInstances = (id) => {
371
+ let currentInstance = useItemInstance(id);
372
+ if (!currentInstance) return [];
373
+ let { itemInstances } = Weaverse;
374
+ let { data: { children } } = currentInstance;
375
+ return children.map(({ id }) => itemInstances.get(id)).filter(Boolean);
348
376
  };
349
-
350
- // src/renderer.tsx
351
- import clsx from "clsx";
352
- import React, { memo, useEffect, useMemo, useRef } from "react";
353
- import { jsx, jsxs } from "react/jsx-runtime";
354
- import { createElement } from "react";
355
- var REACT_VERSION_THRESHOLD = 18;
356
- var reactVersion = Number(React.version?.split(".")[0]) || 0;
357
- var useSafeExternalStore = (subscribe, getSnapshot, getServerSnapshot = getSnapshot) => {
358
- if (typeof window === "undefined") {
359
- return getServerSnapshot();
360
- }
361
- if (React.useSyncExternalStore) {
362
- return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
363
- }
364
- return getSnapshot();
377
+ //#endregion
378
+ //#region src/renderer.tsx
379
+ const REACT_VERSION_THRESHOLD = 18;
380
+ const reactVersion = Number(React.version?.split(".")[0]) || 0;
381
+ /**
382
+ * Reads an external store in React while remaining safe during server rendering
383
+ * and on React versions without `useSyncExternalStore`.
384
+ *
385
+ * @param subscribe - Registers a callback for store changes.
386
+ * @param getSnapshot - Returns the current client snapshot.
387
+ * @param getServerSnapshot - Returns the server snapshot; defaults to `getSnapshot`.
388
+ * @returns The current external-store snapshot.
389
+ */
390
+ const useSafeExternalStore = (subscribe, getSnapshot, getServerSnapshot = getSnapshot) => {
391
+ if (typeof window === "undefined") return getServerSnapshot();
392
+ if (React.useSyncExternalStore) return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
393
+ return getSnapshot();
365
394
  };
366
- var getRootId = (context) => {
367
- if (context.data.rootId) {
368
- return context.data.rootId;
369
- }
370
- let mainItem = context.data?.items?.find((item) => item.type === "main");
371
- if (mainItem?.id) {
372
- return mainItem.id;
373
- }
374
- return context.projectId;
395
+ /**
396
+ * Determines the root element ID for the Weaverse component tree.
397
+ * Falls back through multiple strategies to ensure a valid root is always found:
398
+ * 1. Use explicit rootId from context.data if available
399
+ * 2. Find the first element with type 'main' in items array
400
+ * 3. Use projectId as final fallback
401
+ */
402
+ const getRootId = (context) => {
403
+ if (context.data.rootId) return context.data.rootId;
404
+ let mainItem = context.data?.items?.find((item) => item.type === "main");
405
+ if (mainItem?.id) return mainItem.id;
406
+ return context.projectId;
375
407
  };
376
- var WeaverseRoot = memo(({ context }) => {
377
- const data = useSafeExternalStore(
378
- context.subscribe,
379
- context.getSnapShot,
380
- context.getSnapShot
381
- );
382
- const rootRef = useRef(null);
383
- useEffect(() => {
384
- context.contentRootElement = rootRef.current;
385
- }, [context]);
386
- const eventHandlers = context?.studioBridge?.eventHandlers || {};
387
- if (context.projectId) {
388
- return /* @__PURE__ */ jsx(
389
- "div",
390
- {
391
- className: "weaverse-content-root",
392
- ...eventHandlers,
393
- "data-weaverse-project-id": context.projectId,
394
- "data-weaverse-template-id": data.id,
395
- ref: rootRef,
396
- children: /* @__PURE__ */ jsx(WeaverseContextProvider, { value: context, children: /* @__PURE__ */ jsx(ItemInstance, { id: getRootId(context), parentId: "" }) })
397
- }
398
- );
399
- }
400
- return null;
408
+ /**
409
+ * Renders a Weaverse content tree and provides its runtime to descendant
410
+ * components.
411
+ */
412
+ const WeaverseRoot = memo(({ context }) => {
413
+ const data = useSafeExternalStore(context.subscribe, context.getSnapShot, context.getSnapShot);
414
+ const rootRef = useRef(null);
415
+ useEffect(() => {
416
+ context.contentRootElement = rootRef.current;
417
+ }, [context]);
418
+ const eventHandlers = context?.studioBridge?.eventHandlers || {};
419
+ if (context.projectId) return /* @__PURE__ */ jsx("div", {
420
+ className: "weaverse-content-root",
421
+ ...eventHandlers,
422
+ "data-weaverse-project-id": context.projectId,
423
+ "data-weaverse-template-id": data.id,
424
+ ref: rootRef,
425
+ children: /* @__PURE__ */ jsx(WeaverseContextProvider, {
426
+ value: context,
427
+ children: /* @__PURE__ */ jsx(ItemInstance, {
428
+ id: getRootId(context),
429
+ parentId: ""
430
+ })
431
+ })
432
+ });
433
+ return null;
401
434
  });
402
435
  WeaverseRoot.displayName = "WeaverseRoot";
403
- var ItemComponent = memo(({ instance }) => {
404
- const context = useWeaverse();
405
- const { elementRegistry } = context;
406
- const data = useSafeExternalStore(
407
- instance.subscribe,
408
- instance.getSnapShot,
409
- instance.getSnapShot
410
- );
411
- const {
412
- id,
413
- type,
414
- childIds = [],
415
- children = [],
416
- createdAt,
417
- updatedAt,
418
- deletedAt,
419
- css,
420
- __hidden,
421
- // Need to get rid of this `data` property
422
- data: _,
423
- ...rest
424
- } = data;
425
- const processedRest = useMemo(
426
- () => replaceContentDataConnectorsDeep(rest, context.dataContext),
427
- [data, context.dataContext]
428
- );
429
- useEffect(() => {
430
- if (!instance.ref.current) {
431
- Object.assign(instance.ref, {
432
- current: document.querySelector(`[data-wv-id="${id}"]`)
433
- });
434
- }
435
- }, [instance, id]);
436
- if (__hidden) {
437
- return null;
438
- }
439
- const Element = elementRegistry.get(type);
440
- if (Element?.Component) {
441
- const Component = Element.Component;
442
- if (Component.displayName !== type) {
443
- Component.displayName = type;
444
- }
445
- if (Component.$$typeof === /* @__PURE__ */ Symbol.for("react.forward_ref") || reactVersion > REACT_VERSION_THRESHOLD) {
446
- processedRest.ref = instance.ref;
447
- }
448
- const childItems = children?.length ? children : childIds?.length ? childIds.map((cid) => ({ id: cid })) : [];
449
- const renderChildren = childItems.map(
450
- (item, index) => /* @__PURE__ */ jsx(ItemInstance, { id: item.id, parentId: id }, `${item.id}-${index}`)
451
- );
452
- return /* @__PURE__ */ createElement(
453
- Component,
454
- {
455
- ...processedRest,
456
- children: renderChildren.length ? renderChildren : void 0,
457
- className: clsx(processedRest.data?.className),
458
- "data-wv-id": id,
459
- "data-wv-type": type,
460
- key: id
461
- }
462
- );
463
- }
464
- instance._store.deleted = true;
465
- return null;
436
+ const ItemComponent = memo(({ instance }) => {
437
+ const context = useWeaverse();
438
+ const { elementRegistry } = context;
439
+ const data = useSafeExternalStore(instance.subscribe, instance.getSnapShot, instance.getSnapShot);
440
+ const { id, type, childIds = [], children = [], createdAt, updatedAt, deletedAt, css, __hidden, data: _, ...rest } = data;
441
+ const processedRest = useMemo(() => replaceContentDataConnectorsDeep(rest, context.dataContext), [data, context.dataContext]);
442
+ useEffect(() => {
443
+ if (!instance.ref.current) Object.assign(instance.ref, { current: document.querySelector(`[data-wv-id="${id}"]`) });
444
+ }, [instance, id]);
445
+ if (__hidden) return null;
446
+ const Element = elementRegistry.get(type);
447
+ if (Element?.Component) {
448
+ const Component = Element.Component;
449
+ if (Component.displayName !== type) Component.displayName = type;
450
+ if (Component.$$typeof === Symbol.for("react.forward_ref") || reactVersion > REACT_VERSION_THRESHOLD) processedRest.ref = instance.ref;
451
+ const renderChildren = (children?.length ? children : childIds?.length ? childIds.map((cid) => ({ id: cid })) : []).map((item, index) => /* @__PURE__ */ jsx(ItemInstance, {
452
+ id: item.id,
453
+ parentId: id
454
+ }, `${item.id}-${index}`));
455
+ return /* @__PURE__ */ createElement(Component, {
456
+ ...processedRest,
457
+ children: renderChildren.length ? renderChildren : void 0,
458
+ className: clsx(processedRest.data?.className),
459
+ "data-wv-id": id,
460
+ "data-wv-type": type,
461
+ key: id
462
+ });
463
+ }
464
+ instance._store.deleted = true;
465
+ return null;
466
466
  });
467
467
  ItemComponent.displayName = "WeaverseComponent";
468
- var ItemInstance = memo(
469
- ({ id, parentId }) => {
470
- const instance = useItemInstance(id);
471
- if (!instance) {
472
- return /* @__PURE__ */ jsxs("div", { style: { display: "none" }, children: [
473
- "Item instance ",
474
- id,
475
- " not found"
476
- ] });
477
- }
478
- return /* @__PURE__ */ jsx(WeaverseItemContext.Provider, { value: { parentId, id }, children: /* @__PURE__ */ jsx(ItemComponent, { instance }, id) });
479
- }
480
- );
468
+ const ItemInstance = memo(({ id, parentId }) => {
469
+ const instance = useItemInstance(id);
470
+ if (!instance) return /* @__PURE__ */ jsxs("div", {
471
+ style: { display: "none" },
472
+ children: [
473
+ "Item instance ",
474
+ id,
475
+ " not found"
476
+ ]
477
+ });
478
+ return /* @__PURE__ */ jsx(WeaverseItemContext.Provider, {
479
+ value: {
480
+ parentId,
481
+ id
482
+ },
483
+ children: /* @__PURE__ */ jsx(ItemComponent, { instance }, id)
484
+ });
485
+ });
481
486
  ItemInstance.displayName = "WeaverseInstanceProvider";
482
- export {
483
- WeaverseContext,
484
- WeaverseContextProvider,
485
- WeaverseItemContext,
486
- WeaverseRoot,
487
- replaceContentDataConnectors,
488
- replaceContentDataConnectorsDeep,
489
- useChildInstances,
490
- useItemInstance,
491
- useParentInstance,
492
- useSafeExternalStore,
493
- useWeaverse
494
- };
487
+ //#endregion
488
+ export { WeaverseContext, WeaverseContextProvider, WeaverseItemContext, WeaverseRoot, replaceContentDataConnectors, replaceContentDataConnectorsDeep, useChildInstances, useItemInstance, useParentInstance, useSafeExternalStore, useWeaverse };
489
+
495
490
  //# sourceMappingURL=index.mjs.map