@reframejs/unframer 4.2.0-reframe.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 (111) hide show
  1. package/README.md +389 -0
  2. package/bin.js +5 -0
  3. package/dist/babel-jsx.d.ts +24 -0
  4. package/dist/babel-jsx.js +299 -0
  5. package/dist/babel-plugin-imports.d.ts +15 -0
  6. package/dist/babel-plugin-imports.js +211 -0
  7. package/dist/babel-typedoc.d.ts +39 -0
  8. package/dist/babel-typedoc.js +76 -0
  9. package/dist/bin.d.ts +2 -0
  10. package/dist/bin.js +3 -0
  11. package/dist/cli.d.ts +59 -0
  12. package/dist/cli.js +821 -0
  13. package/dist/compat.d.ts +2 -0
  14. package/dist/compat.js +23 -0
  15. package/dist/config.d.ts +18 -0
  16. package/dist/config.js +27 -0
  17. package/dist/css-core.d.ts +50 -0
  18. package/dist/css-core.js +231 -0
  19. package/dist/css.d.ts +15 -0
  20. package/dist/css.js +36 -0
  21. package/dist/esbuild.d.ts +31 -0
  22. package/dist/esbuild.js +317 -0
  23. package/dist/exporter.d.ts +96 -0
  24. package/dist/exporter.js +1151 -0
  25. package/dist/flat-cache-interceptor.d.ts +24 -0
  26. package/dist/flat-cache-interceptor.js +87 -0
  27. package/dist/framer-chunks/chunk-IKQSD2QC.js +10 -0
  28. package/dist/framer-chunks/fontshare-PZLWRK4B-MHMZIGTX.js +7 -0
  29. package/dist/framer-chunks/fontshare-SXU5BGFE-OWTMMPGS.js +7 -0
  30. package/dist/framer-chunks/fontshare-TIA7QUPT-PUDLUTQ7.js +7 -0
  31. package/dist/framer-chunks/framer-font-RD2SUPQH-Q4MS7WS6.js +7 -0
  32. package/dist/framer-chunks/google-EGNT223R-P4DUHBW2.js +7 -0
  33. package/dist/framer-chunks/google-H6SFY4F5-5JSJCGDR.js +7 -0
  34. package/dist/framer-chunks/google-YSYBFRE6-L7YAHH7V.js +7 -0
  35. package/dist/framer-client.server.d.ts +4 -0
  36. package/dist/framer-client.server.js +45 -0
  37. package/dist/framer-package-versions.d.ts +781 -0
  38. package/dist/framer-package-versions.js +782 -0
  39. package/dist/framer-types.d.ts +30 -0
  40. package/dist/framer-types.js +35 -0
  41. package/dist/framer.d.ts +4990 -0
  42. package/dist/framer.js +64296 -0
  43. package/dist/index.d.ts +10 -0
  44. package/dist/index.js +13 -0
  45. package/dist/package-manager.d.ts +10 -0
  46. package/dist/package-manager.js +138 -0
  47. package/dist/plugin-mcp-dist/lib/client-websocket.d.ts +5 -0
  48. package/dist/plugin-mcp-dist/lib/client-websocket.js +103 -0
  49. package/dist/plugin-mcp-dist/lib/cms.d.ts +10 -0
  50. package/dist/plugin-mcp-dist/lib/cms.js +58 -0
  51. package/dist/plugin-mcp-dist/lib/errors.d.ts +5 -0
  52. package/dist/plugin-mcp-dist/lib/errors.js +48 -0
  53. package/dist/plugin-mcp-dist/lib/framer-client.d.ts +2 -0
  54. package/dist/plugin-mcp-dist/lib/framer-client.js +4 -0
  55. package/dist/plugin-mcp-dist/lib/framer-client.server.d.ts +3 -0
  56. package/dist/plugin-mcp-dist/lib/framer-client.server.js +46 -0
  57. package/dist/plugin-mcp-dist/lib/framer.d.ts +30 -0
  58. package/dist/plugin-mcp-dist/lib/framer.js +1084 -0
  59. package/dist/plugin-mcp-dist/lib/hooks.d.ts +6 -0
  60. package/dist/plugin-mcp-dist/lib/hooks.js +46 -0
  61. package/dist/plugin-mcp-dist/lib/mcp-client.d.ts +142 -0
  62. package/dist/plugin-mcp-dist/lib/mcp-client.js +40 -0
  63. package/dist/plugin-mcp-dist/lib/mcp-handlers.d.ts +385 -0
  64. package/dist/plugin-mcp-dist/lib/mcp-handlers.js +1924 -0
  65. package/dist/plugin-mcp-dist/lib/mcp-tools.d.ts +5 -0
  66. package/dist/plugin-mcp-dist/lib/mcp-tools.js +5 -0
  67. package/dist/plugin-mcp-dist/lib/mcp-websocket.d.ts +10 -0
  68. package/dist/plugin-mcp-dist/lib/mcp-websocket.js +88 -0
  69. package/dist/plugin-mcp-dist/lib/plugin-websocket.d.ts +5 -0
  70. package/dist/plugin-mcp-dist/lib/plugin-websocket.js +170 -0
  71. package/dist/plugin-mcp-dist/lib/react-export.d.ts +51 -0
  72. package/dist/plugin-mcp-dist/lib/react-export.js +358 -0
  73. package/dist/plugin-mcp-dist/lib/schema.d.ts +276 -0
  74. package/dist/plugin-mcp-dist/lib/schema.js +907 -0
  75. package/dist/plugin-mcp-dist/lib/store.d.ts +2 -0
  76. package/dist/plugin-mcp-dist/lib/store.js +10 -0
  77. package/dist/plugin-mcp-dist/lib/tree-utils.d.ts +6 -0
  78. package/dist/plugin-mcp-dist/lib/tree-utils.js +141 -0
  79. package/dist/plugin-mcp-dist/lib/tunnel.d.ts +46 -0
  80. package/dist/plugin-mcp-dist/lib/tunnel.js +117 -0
  81. package/dist/plugin-mcp-dist/lib/types.d.ts +110 -0
  82. package/dist/plugin-mcp-dist/lib/types.js +188 -0
  83. package/dist/plugin-mcp-dist/lib/upstream-socket.d.ts +13 -0
  84. package/dist/plugin-mcp-dist/lib/upstream-socket.js +56 -0
  85. package/dist/plugin-mcp-dist/lib/utils.d.ts +13 -0
  86. package/dist/plugin-mcp-dist/lib/utils.js +57 -0
  87. package/dist/plugin-mcp-dist/lib/websocket-server.d.ts +10 -0
  88. package/dist/plugin-mcp-dist/lib/websocket-server.js +88 -0
  89. package/dist/plugin-mcp-dist/lib/xml.d.ts +10 -0
  90. package/dist/plugin-mcp-dist/lib/xml.js +395 -0
  91. package/dist/react.d.ts +76 -0
  92. package/dist/react.js +280 -0
  93. package/dist/renamer.d.ts +11 -0
  94. package/dist/renamer.js +139 -0
  95. package/dist/sentry.d.ts +2 -0
  96. package/dist/sentry.js +5 -0
  97. package/dist/stackblitz.d.ts +9 -0
  98. package/dist/stackblitz.js +179 -0
  99. package/dist/styles/framer.css +920 -0
  100. package/dist/styles/reset.css +37 -0
  101. package/dist/typescript.d.ts +24 -0
  102. package/dist/typescript.js +197 -0
  103. package/dist/undici-dispatcher.d.ts +2 -0
  104. package/dist/undici-dispatcher.js +10 -0
  105. package/dist/unframer-loader.d.ts +2 -0
  106. package/dist/unframer-loader.js +12 -0
  107. package/dist/utils.d.ts +20 -0
  108. package/dist/utils.js +81 -0
  109. package/dist/version.d.ts +2 -0
  110. package/dist/version.js +2 -0
  111. package/package.json +131 -0
@@ -0,0 +1,1084 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import { framer, isImageAsset, isComponentInstanceNode, isComponentNode, isFrameNode, isTextNode, supportsAspectRatio, supportsBackgroundColor, supportsBackgroundImage, supportsBorder, supportsBorderRadius, supportsFont, supportsImageRendering, supportsInlineTextStyle, supportsLayout, supportsLink, supportsLocked, supportsName, supportsOpacity, supportsOverflow, supportsPins, supportsPosition, supportsRotation, supportsSize, supportsSizeConstraints, supportsSVG, supportsTextTruncation, supportsVisible, supportsZIndex, } from '#framer-client';
10
+ import { Sema } from 'sema4';
11
+ import { propCamelCaseJustLikeFramer } from 'unframer';
12
+ import { bfsFramerLayersTree } from './tree-utils.js';
13
+ let cachedPagePaths = [];
14
+ const componentPropertyControlsCache = new Map();
15
+ function isImportableComponentUrl(url) {
16
+ try {
17
+ const parsed = new URL(url);
18
+ if (typeof window === 'undefined') {
19
+ return parsed.protocol === 'file:' || parsed.protocol === 'data:';
20
+ }
21
+ return true;
22
+ }
23
+ catch {
24
+ return true;
25
+ }
26
+ }
27
+ // Helper function to check permissions and throw error if not allowed
28
+ function checkPermissions(...methods) {
29
+ // Cast to the expected tuple type for isAllowedTo
30
+ const [first, ...rest] = methods;
31
+ if (!first)
32
+ return;
33
+ if (!framer.isAllowedTo(first, ...rest)) {
34
+ throw new Error(`Missing permissions: ${methods.join(', ')}`);
35
+ }
36
+ }
37
+ // Generic utility function to sort an array based on the order of IDs in a reference array
38
+ function sortArrayLike(array, getItemId, referenceIds) {
39
+ return array.sort((a, b) => {
40
+ const aIndex = referenceIds.indexOf(getItemId(a));
41
+ const bIndex = referenceIds.indexOf(getItemId(b));
42
+ // If not found in reference, maintain current order
43
+ if (aIndex === -1 && bIndex === -1)
44
+ return 0;
45
+ if (aIndex === -1)
46
+ return 1;
47
+ if (bIndex === -1)
48
+ return -1;
49
+ return aIndex - bIndex;
50
+ });
51
+ }
52
+ // Cache for uploaded images to prevent re-uploading
53
+ const uploadedImagesCache = new Map();
54
+ // Export a function to clear the cache if needed
55
+ export function clearUploadedImagesCache() {
56
+ uploadedImagesCache.clear();
57
+ }
58
+ async function getPagePaths() {
59
+ if (cachedPagePaths?.length)
60
+ return cachedPagePaths;
61
+ const pages = await framer.getNodesWithType('WebPageNode');
62
+ cachedPagePaths = pages
63
+ .map((x) => x.path)
64
+ .filter((val) => val != null)
65
+ .filter((val) => !val?.includes(':'));
66
+ return cachedPagePaths;
67
+ }
68
+ export function replaceEnumIdsForControls(controls, propControls) {
69
+ try {
70
+ if (!controls || !propControls)
71
+ return controls;
72
+ controls = { ...controls };
73
+ for (let [k, value] of Object.entries(propControls || {})) {
74
+ if (!value)
75
+ continue;
76
+ const propName = propCamelCaseJustLikeFramer(value.title) || k;
77
+ switch (value.type) {
78
+ case ControlType.Enum: {
79
+ if (!('optionTitles' in value)) {
80
+ return '';
81
+ }
82
+ const optionTitles = value.optionTitles || value.options;
83
+ let v = controls[propName] || controls[k];
84
+ const optionIndex = value.options.indexOf(v);
85
+ const enumTitle = optionTitles[optionIndex];
86
+ if (optionIndex !== -1 && enumTitle) {
87
+ controls[propName] = enumTitle;
88
+ }
89
+ }
90
+ }
91
+ }
92
+ return controls;
93
+ }
94
+ catch (e) {
95
+ console.error('replaceEnumIdsForControls error:', e);
96
+ return controls;
97
+ }
98
+ }
99
+ export async function getComponentPropertyControls(url) {
100
+ if (!url)
101
+ return { comments: undefined, propertyControls: undefined };
102
+ const cachedResult = componentPropertyControlsCache.get(url);
103
+ if (cachedResult) {
104
+ return cachedResult;
105
+ }
106
+ if (!isImportableComponentUrl(url)) {
107
+ const result = { comments: undefined, propertyControls: undefined };
108
+ componentPropertyControlsCache.set(url, result);
109
+ return result;
110
+ }
111
+ try {
112
+ // Race the dynamic import against a 3s timeout so broken URLs don't
113
+ // block the entire export for seconds while the fetch times out.
114
+ const importPromise = import(__rewriteRelativeImportExtension(/* @vite-ignore */ url));
115
+ const timeoutPromise = new Promise((_, reject) => {
116
+ setTimeout(() => {
117
+ reject(new Error(`import timeout for ${url}`));
118
+ }, 3000);
119
+ });
120
+ const [res, paths] = await Promise.all([
121
+ Promise.race([importPromise, timeoutPromise]),
122
+ getPagePaths(),
123
+ ]);
124
+ const propertyControls = res.default?.propertyControls;
125
+ const comments = getAttributeComments(propertyControls, paths);
126
+ const result = {
127
+ comments,
128
+ propertyControls,
129
+ };
130
+ componentPropertyControlsCache.set(url, result);
131
+ return result;
132
+ }
133
+ catch (e) {
134
+ console.log('failed to import component schema', e);
135
+ const result = { comments: undefined, propertyControls: undefined };
136
+ componentPropertyControlsCache.set(url, result);
137
+ return result;
138
+ }
139
+ }
140
+ export var ControlType;
141
+ (function (ControlType) {
142
+ ControlType["Boolean"] = "boolean";
143
+ ControlType["Number"] = "number";
144
+ ControlType["String"] = "string";
145
+ ControlType["RichText"] = "richtext";
146
+ ControlType["FusedNumber"] = "fusednumber";
147
+ ControlType["Enum"] = "enum";
148
+ ControlType["SegmentedEnum"] = "segmentedenum";
149
+ ControlType["Color"] = "color";
150
+ ControlType["Image"] = "image";
151
+ ControlType["ResponsiveImage"] = "responsiveimage";
152
+ ControlType["File"] = "file";
153
+ ControlType["ComponentInstance"] = "componentinstance";
154
+ ControlType["Array"] = "array";
155
+ ControlType["EventHandler"] = "eventhandler";
156
+ ControlType["Transition"] = "transition";
157
+ ControlType["BoxShadow"] = "boxshadow";
158
+ ControlType["Link"] = "link";
159
+ ControlType["Date"] = "date";
160
+ ControlType["Object"] = "object";
161
+ ControlType["Font"] = "font";
162
+ ControlType["PageScope"] = "pagescope";
163
+ ControlType["ScrollSectionRef"] = "scrollsectionref";
164
+ ControlType["CustomCursor"] = "customcursor";
165
+ ControlType["Border"] = "border";
166
+ ControlType["Cursor"] = "cursor";
167
+ ControlType["Padding"] = "padding";
168
+ ControlType["BorderRadius"] = "borderradius";
169
+ ControlType["CollectionReference"] = "collectionreference";
170
+ ControlType["MultiCollectionReference"] = "multicollectionreference";
171
+ })(ControlType || (ControlType = {}));
172
+ export function getAttributeComments(controls, availablePagePaths = ['/']) {
173
+ if (!controls) {
174
+ return {};
175
+ }
176
+ const result = {};
177
+ Object.entries(controls || {}).forEach(([key, value]) => {
178
+ if (!value) {
179
+ return;
180
+ }
181
+ const typescriptType = (value) => {
182
+ switch (value.type) {
183
+ case ControlType.Color:
184
+ return 'color value';
185
+ case ControlType.Boolean:
186
+ return 'boolean';
187
+ case ControlType.Number:
188
+ return 'number';
189
+ case ControlType.String:
190
+ return '';
191
+ case ControlType.Enum: {
192
+ if (!('optionTitles' in value)) {
193
+ return '';
194
+ }
195
+ const options = value.optionTitles || value.options;
196
+ return options
197
+ .map((x, i) => `'${x}' is ${value.options[i]}`)
198
+ .join(', ');
199
+ }
200
+ case ControlType.File:
201
+ return 'file';
202
+ case ControlType.Image:
203
+ return 'image';
204
+ case ControlType.ComponentInstance:
205
+ return 'component instance';
206
+ return 'React.ReactNode';
207
+ case ControlType.Array:
208
+ return `${typescriptType(value.control)}[]`;
209
+ case ControlType.Object:
210
+ return `{${Object.entries(value.controls)
211
+ .map(([k, v]) => {
212
+ return `${k}: ${typescriptType(v)}`;
213
+ })
214
+ .join(', ')}}`;
215
+ case ControlType.Date:
216
+ return 'DateString';
217
+ case ControlType.Link:
218
+ return `URL string, page path amongst ${JSON.stringify(availablePagePaths)}, or JSON link object {"type":"webPage","webPageId":"<pageNodeId>","scrollSection":{"targetNodeId":"<sectionNodeId>"}} for page+section links`;
219
+ case ControlType.ScrollSectionRef:
220
+ return `JSON object {"targetNodeId":"<sectionNodeId>"} for scroll section target`;
221
+ case ControlType.ResponsiveImage:
222
+ return 'responsive image';
223
+ return `{src: string, srcSet?: string, alt?: string}`;
224
+ case ControlType.FusedNumber:
225
+ return 'number';
226
+ case ControlType.Transition:
227
+ return 'transition';
228
+ return 'any';
229
+ case ControlType.EventHandler:
230
+ return 'event handler';
231
+ return 'Function';
232
+ case ControlType.RichText:
233
+ return 'rich text';
234
+ case ControlType.Font:
235
+ return 'font';
236
+ case ControlType.BoxShadow:
237
+ return 'box shadow';
238
+ case ControlType.Padding:
239
+ return 'padding';
240
+ case ControlType.Border:
241
+ return 'border';
242
+ case ControlType.BorderRadius:
243
+ return 'border radius, four px values delimited by space';
244
+ default:
245
+ return 'any';
246
+ }
247
+ };
248
+ result[key] = typescriptType(value);
249
+ const propName = propCamelCaseJustLikeFramer(value.title);
250
+ if (propName)
251
+ result[propName] = typescriptType(value);
252
+ });
253
+ return result;
254
+ }
255
+ Object.assign(globalThis, {
256
+ getComponentSchema: getComponentPropertyControls,
257
+ });
258
+ export function getInstanceComponentId(componentInstance) {
259
+ if (!isComponentInstanceNode(componentInstance)) {
260
+ return;
261
+ }
262
+ if (!componentInstance.componentIdentifier.startsWith('local-module:')) {
263
+ console.log(`component ${componentInstance.name} is not a local module: ${componentInstance.componentIdentifier} `);
264
+ return;
265
+ }
266
+ const regex = /local-module:.*\/(.*):.*/;
267
+ const match = componentInstance.componentIdentifier.match(regex);
268
+ if (!match) {
269
+ console.log(`component ${componentInstance.name} does not match regex to get component id: ${componentInstance.componentIdentifier} `);
270
+ return;
271
+ }
272
+ return match[1];
273
+ }
274
+ async function getInstanceComponent(componentInstance) {
275
+ const componentId = getInstanceComponentId(componentInstance);
276
+ if (!componentId) {
277
+ return;
278
+ }
279
+ const componentNode = await framer.getNode(componentId);
280
+ if (!componentNode || !isComponentNode(componentNode)) {
281
+ console.log(`could not find component node for ${componentId}`);
282
+ return;
283
+ }
284
+ return componentNode;
285
+ }
286
+ async function getComponentCodeUrl(componentNode) {
287
+ if (isComponentInstanceNode(componentNode)) {
288
+ return await getComponentCodeUrl(await getInstanceComponent(componentNode));
289
+ }
290
+ if (isComponentNode(componentNode)) {
291
+ let nameEncoding = componentNode.name || '';
292
+ if (!nameEncoding) {
293
+ return;
294
+ }
295
+ nameEncoding = nameEncoding.replace(/ +/g, '-');
296
+ nameEncoding = encodeURIComponent(nameEncoding);
297
+ let id = componentNode.id;
298
+ return `https://framer.com/m/${nameEncoding}-${id}.js`;
299
+ }
300
+ }
301
+ Object.assign(globalThis, { getComponentCodeUrl });
302
+ async function* recurseIntoComponent(componentInstance, encounteredIds) {
303
+ const componentNode = await getInstanceComponent(componentInstance);
304
+ if (!componentNode) {
305
+ return;
306
+ }
307
+ const primary = (await componentNode.getChildren()).find((x) => isFrameNode(x) && !x.isReplica);
308
+ if (!primary) {
309
+ console.log('no primary child for component found');
310
+ return;
311
+ }
312
+ for await (let child of primary.walk()) {
313
+ if (!encounteredIds.has(child.id)) {
314
+ encounteredIds.add(child.id);
315
+ yield child;
316
+ yield* recurseIntoComponent(child, encounteredIds);
317
+ }
318
+ }
319
+ }
320
+ async function isNodeVisible(node) {
321
+ const parents = await getParentNodesArray(node);
322
+ const isVisible = parents.every((parent) => {
323
+ if (supportsVisible(parent)) {
324
+ return parent.visible;
325
+ }
326
+ return true;
327
+ });
328
+ return isVisible && (!supportsVisible(node) || node.visible);
329
+ }
330
+ export async function isNodeZoomable(node) {
331
+ if (!(await isNodeVisible(node))) {
332
+ return false;
333
+ }
334
+ return true;
335
+ }
336
+ async function push({ node, tree, text, nodeId, isRootNode = false, visitedComponents, }) {
337
+ const parents = (await getParentNodesArray(node)).reverse();
338
+ let currentLevel = tree;
339
+ let currentParent = null;
340
+ for (let i = 0; i < parents.length; i++) {
341
+ const parent = parents[i];
342
+ let existingNode = currentLevel.find((item) => item.nodeId === parent.id);
343
+ if (!existingNode) {
344
+ existingNode = {
345
+ nodeId: parent.id,
346
+ name: supportsName(parent) ? parent.name || '' : '',
347
+ isReplica: parent.isReplica,
348
+ children: [],
349
+ };
350
+ currentLevel.push(existingNode);
351
+ // Sort siblings based on parent's children order if we have a parent
352
+ if (currentParent) {
353
+ const parentChildren = await currentParent.getChildren();
354
+ const childIds = parentChildren.map((child) => child.id);
355
+ sortArrayLike(currentLevel, (item) => item.nodeId || '', childIds);
356
+ }
357
+ }
358
+ if (!existingNode.children) {
359
+ existingNode.children = [];
360
+ }
361
+ currentParent = parent;
362
+ currentLevel = existingNode.children;
363
+ }
364
+ let { attributes, attrControlsComments } = await getNodeAttributesForXml(node, visitedComponents);
365
+ // Add comment for root replica nodes
366
+ if (isRootNode && node.isReplica) {
367
+ attrControlsComments = {
368
+ ...attrControlsComments,
369
+ nodeId: 'This is a non-primary variant. To see children inside, call getNodeXml again on this nodeId.',
370
+ };
371
+ }
372
+ const nodeEntry = {
373
+ content: text,
374
+ nodeId,
375
+ name: 'name' in node ? node.name || '' : '',
376
+ attributes,
377
+ attrControlsComments,
378
+ children: [],
379
+ isReplica: node.isReplica,
380
+ disableSelfClosing: isTextNode(node) ? true : undefined,
381
+ // Add comment for replica nodes (variants) where children are skipped
382
+ comment: node.isReplica && !isRootNode
383
+ ? 'This is a non-primary variant. To see children inside, call getNodeXml again on this nodeId.'
384
+ : undefined,
385
+ };
386
+ currentLevel.push(nodeEntry);
387
+ // Sort the final level based on the last parent's children order
388
+ if (currentParent || parents.length === 0) {
389
+ const parentNode = currentParent ||
390
+ (parents.length === 0 && node.getParent
391
+ ? await node.getParent()
392
+ : null);
393
+ if (parentNode) {
394
+ const parentChildren = await parentNode.getChildren();
395
+ const childIds = parentChildren.map((child) => child.id);
396
+ sortArrayLike(currentLevel, (item) => item.nodeId || '', childIds);
397
+ }
398
+ }
399
+ return tree;
400
+ }
401
+ export async function getFramerTree({ rootNodes, recursive = true, }) {
402
+ const timeId = `getFramerTree-${Date.now()}-${Math.random().toString(36).slice(2)}`;
403
+ console.time(timeId);
404
+ let tree = [];
405
+ // Create semaphore with concurrency limit of 6
406
+ const semaphore = new Sema(6);
407
+ let componentInstanceChildrenSeen = new Set();
408
+ let rootNodeIds = new Set(rootNodes.map((n) => n.id));
409
+ let visitedComponents = new Set();
410
+ async function handleNode(node) {
411
+ if (isTextNode(node)) {
412
+ const isVisible = await isNodeVisible(node);
413
+ if (!isVisible) {
414
+ console.log('node not visible', node.id);
415
+ return;
416
+ }
417
+ // getText() can throw "Node is not a text node" for text nodes inside
418
+ // ComponentNode definitions (the Framer API server rejects getText on
419
+ // component-internal text nodes even though isTextNode returns true).
420
+ // In that case, push the node without text content so it still appears
421
+ // in the XML tree with its attributes.
422
+ let text = null;
423
+ try {
424
+ text = await node.getText();
425
+ }
426
+ catch {
427
+ console.log('getText() failed for node', node.id, node.name);
428
+ }
429
+ if (!text) {
430
+ console.log('no text found for node', node.id, node.name);
431
+ }
432
+ tree = await push({
433
+ node,
434
+ tree: tree,
435
+ text: text || undefined,
436
+ nodeId: node.id,
437
+ isReplica: node.isReplica,
438
+ isRootNode: rootNodeIds.has(node.id),
439
+ visitedComponents,
440
+ });
441
+ // Return early for text nodes to avoid duplicate push
442
+ return;
443
+ }
444
+ const isVisible = await isNodeVisible(node);
445
+ if (!isVisible) {
446
+ console.log('node not visible', node.id);
447
+ return;
448
+ }
449
+ tree = await push({
450
+ node,
451
+ tree: tree,
452
+ nodeId: node.id,
453
+ isReplica: node.isReplica,
454
+ isRootNode: rootNodeIds.has(node.id),
455
+ visitedComponents,
456
+ });
457
+ }
458
+ // Collect all nodes to process
459
+ const nodesToProcess = [];
460
+ for (let rootNode of rootNodes) {
461
+ if (!rootNode) {
462
+ continue;
463
+ }
464
+ const isRootReplica = rootNode.isReplica;
465
+ // Custom walk to handle replica children
466
+ async function* walkNode(node, isRoot = false) {
467
+ yield node;
468
+ // Skip children if this is a root replica node
469
+ if (isRoot && isRootReplica) {
470
+ console.log(`Skipping children of root replica node ${node.id} (${'name' in node ? node.name : 'unknown'})`);
471
+ return;
472
+ }
473
+ // Also skip children if this node itself is a replica (not just root replicas)
474
+ if (!isRoot && node.isReplica) {
475
+ console.log(`Skipping children of replica node ${node.id} (${'name' in node ? node.name : 'unknown'})`);
476
+ return;
477
+ }
478
+ const children = await node.getChildren();
479
+ for (const child of children) {
480
+ yield* walkNode(child, false);
481
+ }
482
+ }
483
+ for await (let node of walkNode(rootNode, true)) {
484
+ nodesToProcess.push({ node });
485
+ if (recursive) {
486
+ for await (let child of recurseIntoComponent(node, componentInstanceChildrenSeen)) {
487
+ nodesToProcess.push({ node: child, fromRecursion: true });
488
+ }
489
+ }
490
+ }
491
+ }
492
+ // Process nodes concurrently with semaphore limiting concurrency
493
+ const processingPromises = nodesToProcess.map(async ({ node }) => {
494
+ await semaphore.acquire();
495
+ try {
496
+ await handleNode(node);
497
+ }
498
+ finally {
499
+ semaphore.release();
500
+ }
501
+ });
502
+ // Wait for all nodes to be processed
503
+ await Promise.all(processingPromises);
504
+ console.timeEnd(timeId);
505
+ return tree;
506
+ }
507
+ export async function discardFramerChanges({ previousTree: previousTree, }) {
508
+ // Check permissions before proceeding
509
+ checkPermissions('Node.setAttributes', 'TextNode.setText');
510
+ const allNodes = bfsFramerLayersTree(previousTree).filter((x) => x?.nodeId);
511
+ const promises = allNodes.map(async (oldNodeObj) => {
512
+ const { nodeId, content: oldContent, attributes } = oldNodeObj;
513
+ if (!oldContent || !nodeId) {
514
+ console.log('no old content or node id found');
515
+ return Promise.resolve();
516
+ }
517
+ let node = await framer.getNode(nodeId);
518
+ if (isTextNode(node)) {
519
+ return await node.setText(oldContent);
520
+ }
521
+ await applyAttributes(node, attributes).catch((e) => {
522
+ console.error('applyAttributes', e);
523
+ framer.notify(e.message, { variant: 'error' });
524
+ });
525
+ });
526
+ return await Promise.all(promises);
527
+ }
528
+ // Note: inlineTextStyle attributes are now handled with dot notation
529
+ // e.g., inlineTextStyle.fontSize, inlineTextStyle.color, etc.
530
+ // Default values for common attributes to reduce XML verbosity
531
+ export const ATTRIBUTE_DEFAULTS = {
532
+ opacity: 1,
533
+ visible: true,
534
+ locked: false,
535
+ rotation: 0,
536
+ position: 'relative',
537
+ // Layout defaults
538
+ gap: '0px',
539
+ padding: '0px',
540
+ stackWrap: false,
541
+ // Grid item defaults
542
+ gridFillWidth: true,
543
+ gridFillHeight: true,
544
+ // width: '1fr',
545
+ // height: 'fit-content',
546
+ };
547
+ async function getNodeAttributesForXml(node, visitedComponents) {
548
+ let attributes = {};
549
+ // Helper to add attribute only if it differs from default
550
+ const addAttribute = (key, value) => {
551
+ if (value !== undefined && value !== null) {
552
+ // Check if this attribute has a default value
553
+ const defaultValue = ATTRIBUTE_DEFAULTS[key];
554
+ // Only add if value differs from default
555
+ if (defaultValue === undefined || value !== defaultValue) {
556
+ attributes[key] = value;
557
+ }
558
+ }
559
+ };
560
+ // Common attributes (DrawableNode)
561
+ if (supportsOpacity(node)) {
562
+ addAttribute('opacity', node.opacity);
563
+ }
564
+ if (supportsVisible(node)) {
565
+ addAttribute('visible', node.visible);
566
+ }
567
+ if (supportsLocked(node)) {
568
+ addAttribute('locked', node.locked);
569
+ }
570
+ // if (supportsName(node)) {
571
+ // addAttribute('name', node.name)
572
+ // }
573
+ // Position attributes
574
+ if (supportsPosition(node)) {
575
+ addAttribute('position', node.position);
576
+ }
577
+ // Size attributes
578
+ if (supportsSize(node)) {
579
+ addAttribute('width', node.width);
580
+ addAttribute('height', node.height);
581
+ }
582
+ // Rotation
583
+ if (supportsRotation(node)) {
584
+ addAttribute('rotation', node.rotation);
585
+ }
586
+ // Pins (positioning constraints)
587
+ if (supportsPins(node)) {
588
+ if (node.top !== undefined)
589
+ addAttribute('top', node.top);
590
+ if (node.right !== undefined)
591
+ addAttribute('right', node.right);
592
+ if (node.bottom !== undefined)
593
+ addAttribute('bottom', node.bottom);
594
+ if (node.left !== undefined)
595
+ addAttribute('left', node.left);
596
+ if (node.centerX !== undefined)
597
+ addAttribute('centerX', node.centerX);
598
+ if (node.centerY !== undefined)
599
+ addAttribute('centerY', node.centerY);
600
+ }
601
+ // Size constraints
602
+ if (supportsSizeConstraints(node)) {
603
+ if (node.minWidth !== undefined)
604
+ addAttribute('minWidth', node.minWidth);
605
+ if (node.maxWidth !== undefined)
606
+ addAttribute('maxWidth', node.maxWidth);
607
+ if (node.minHeight !== undefined)
608
+ addAttribute('minHeight', node.minHeight);
609
+ if (node.maxHeight !== undefined)
610
+ addAttribute('maxHeight', node.maxHeight);
611
+ }
612
+ // Aspect ratio
613
+ if (supportsAspectRatio(node)) {
614
+ addAttribute('aspectRatio', node.aspectRatio);
615
+ }
616
+ // Link attributes
617
+ if (supportsLink(node)) {
618
+ addAttribute('link', node.link);
619
+ addAttribute('linkOpenInNewTab', node.linkOpenInNewTab);
620
+ }
621
+ // Frame-specific attributes
622
+ if (supportsBackgroundColor(node) && node.backgroundColor) {
623
+ // Check if it's a ColorStyle or a plain color string
624
+ if (typeof node.backgroundColor === 'string') {
625
+ addAttribute('backgroundColor', node.backgroundColor);
626
+ }
627
+ else {
628
+ // It's a ColorStyle, store the path
629
+ addAttribute('backgroundColor', node.backgroundColor.path);
630
+ }
631
+ }
632
+ if (supportsBorderRadius(node)) {
633
+ addAttribute('borderRadius', node.borderRadius);
634
+ }
635
+ if (supportsImageRendering(node)) {
636
+ addAttribute('imageRendering', node.imageRendering);
637
+ }
638
+ if (supportsBackgroundImage(node) && node.backgroundImage) {
639
+ // Store the image URL
640
+ addAttribute('backgroundImage', node.backgroundImage.url);
641
+ }
642
+ // Font attributes (for TextNode)
643
+ if (supportsFont(node) && node.font) {
644
+ // Store only the font ID/selector
645
+ addAttribute('font', node.font.selector);
646
+ }
647
+ // Inline text style handling - always a reference to a TextStyle
648
+ if (supportsInlineTextStyle(node) && node.inlineTextStyle) {
649
+ // Store the path to the text style for easy lookup
650
+ addAttribute('inlineTextStyle', node.inlineTextStyle.path);
651
+ }
652
+ // SVG-specific attributes
653
+ if (supportsSVG(node) && node.svg) {
654
+ addAttribute('svg', node.svg);
655
+ }
656
+ // Border attributes
657
+ if (supportsBorder(node) && node.border) {
658
+ const borderColor = typeof node.border.color === 'string'
659
+ ? node.border.color
660
+ : node.border.color.path;
661
+ addAttribute('borderWidth', node.border.width);
662
+ addAttribute('borderStyle', node.border.style);
663
+ addAttribute('borderColor', borderColor);
664
+ }
665
+ // Z-Index attribute
666
+ if (supportsZIndex(node) && node.zIndex != null) {
667
+ addAttribute('zIndex', node.zIndex);
668
+ }
669
+ // Overflow attributes
670
+ if (supportsOverflow(node)) {
671
+ if (node.overflow != null)
672
+ addAttribute('overflow', node.overflow);
673
+ if (node.overflowX != null)
674
+ addAttribute('overflowX', node.overflowX);
675
+ if (node.overflowY != null)
676
+ addAttribute('overflowY', node.overflowY);
677
+ }
678
+ // Text truncation (line clamp)
679
+ if (supportsTextTruncation(node) && node.textTruncation != null) {
680
+ addAttribute('textTruncation', node.textTruncation);
681
+ }
682
+ // Layout attributes (Frame nodes only)
683
+ if (supportsLayout(node)) {
684
+ // Layout type
685
+ addAttribute('layout', node.layout);
686
+ // Common layout attributes
687
+ addAttribute('gap', node.gap);
688
+ addAttribute('padding', node.padding);
689
+ // Stack-specific attributes
690
+ if (node.layout === 'stack') {
691
+ addAttribute('stackDirection', node.stackDirection);
692
+ addAttribute('stackDistribution', node.stackDistribution);
693
+ addAttribute('stackAlignment', node.stackAlignment);
694
+ addAttribute('stackWrap', node.stackWrapEnabled);
695
+ }
696
+ // Grid-specific attributes
697
+ if (node.layout === 'grid') {
698
+ addAttribute('gridColumns', node.gridColumnCount);
699
+ addAttribute('gridRows', node.gridRowCount);
700
+ addAttribute('gridAlignment', node.gridAlignment);
701
+ addAttribute('gridColumnWidthType', node.gridColumnWidthType);
702
+ addAttribute('gridColumnWidth', node.gridColumnWidth);
703
+ addAttribute('gridColumnMinWidth', node.gridColumnMinWidth);
704
+ addAttribute('gridRowHeightType', node.gridRowHeightType);
705
+ addAttribute('gridRowHeight', node.gridRowHeight);
706
+ }
707
+ }
708
+ // Grid item attributes (for children of grid containers)
709
+ // Check if parent is a grid container
710
+ if ('gridItemFillCellWidth' in node) {
711
+ addAttribute('gridFillWidth', node.gridItemFillCellWidth);
712
+ addAttribute('gridFillHeight', node.gridItemFillCellHeight);
713
+ addAttribute('gridAlignX', node.gridItemHorizontalAlignment);
714
+ addAttribute('gridAlignY', node.gridItemVerticalAlignment);
715
+ addAttribute('gridColumnSpan', node.gridItemColumnSpan);
716
+ addAttribute('gridRowSpan', node.gridItemRowSpan);
717
+ }
718
+ // Create base comments object
719
+ let attrComments = {
720
+ inlineTextStyle: 'project text style path, always starts with /',
721
+ backgroundImage: 'background image URL, if you pass a new image url, the image will be uploaded to Framer',
722
+ backgroundColor: 'background color string or project color style path (if starts with /)',
723
+ };
724
+ // Component instance specific handling
725
+ if (isComponentInstanceNode(node)) {
726
+ // Add componentId attribute
727
+ const componentId = getInstanceComponentId(node);
728
+ if (componentId) {
729
+ addAttribute('componentId', componentId);
730
+ attrComments.componentId = 'the component id this instance uses';
731
+ }
732
+ if (!visitedComponents?.has(componentId || node.id)) {
733
+ if (!node.insertURL) {
734
+ console.log(`no node.insertURL for component instance ${node.name}`);
735
+ }
736
+ const { comments: controlComments } = await getComponentPropertyControls(node.insertURL || undefined);
737
+ if (controlComments) {
738
+ Object.assign(attrComments, controlComments);
739
+ if (visitedComponents && componentId) {
740
+ visitedComponents.add(componentId);
741
+ }
742
+ }
743
+ }
744
+ // Add all controls as top-level attributes
745
+ if (node.controls) {
746
+ for (const [key, value] of Object.entries(node.controls)) {
747
+ addAttribute(key, value);
748
+ }
749
+ }
750
+ }
751
+ attributes = serializeAttributesForXml(attributes);
752
+ return {
753
+ attributes,
754
+ attrControlsComments: attrComments,
755
+ };
756
+ }
757
+ Object.assign(globalThis, {
758
+ getNodeAttributesForXml,
759
+ supportsBackgroundColor,
760
+ getAttributesForSelectedNodes: async () => {
761
+ const selectedNodes = await framer.getSelection();
762
+ if (!selectedNodes || !selectedNodes.length) {
763
+ console.log('no nodes selected');
764
+ return;
765
+ }
766
+ const attributesList = [];
767
+ for (const node of selectedNodes) {
768
+ const { attributes: nodeAttrs, attrControlsComments } = await getNodeAttributesForXml(node);
769
+ console.log(node.id, node['name'], JSON.stringify(nodeAttrs, null, 2));
770
+ console.log(JSON.stringify(attrControlsComments, null, 2));
771
+ attributesList.push(nodeAttrs);
772
+ }
773
+ return attributesList;
774
+ },
775
+ });
776
+ function encodeAttributeValue(value) {
777
+ if (typeof value === 'string') {
778
+ return value;
779
+ }
780
+ return JSON.stringify(value);
781
+ }
782
+ function isRecord(value) {
783
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
784
+ }
785
+ function isAssetWithUrl(value) {
786
+ if (isImageAsset(value)) {
787
+ return true;
788
+ }
789
+ if (!isRecord(value)) {
790
+ return false;
791
+ }
792
+ return typeof value.url === 'string';
793
+ }
794
+ function normalizeAttributeValueForXml(value) {
795
+ if (isAssetWithUrl(value)) {
796
+ return value.url;
797
+ }
798
+ if (isRecord(value) && typeof value.path === 'string') {
799
+ return value.path;
800
+ }
801
+ if (isRecord(value) && typeof value.selector === 'string') {
802
+ return value.selector;
803
+ }
804
+ return value;
805
+ }
806
+ function isPlainObject(value) {
807
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
808
+ return false;
809
+ }
810
+ const prototype = Object.getPrototypeOf(value);
811
+ return prototype === Object.prototype || prototype === null;
812
+ }
813
+ export function serializeAttributesForXml(attributes) {
814
+ if (!attributes) {
815
+ return {};
816
+ }
817
+ const result = {};
818
+ for (const [key, value] of Object.entries(attributes)) {
819
+ if (value === undefined) {
820
+ continue;
821
+ }
822
+ const normalizedValue = normalizeAttributeValueForXml(value);
823
+ if (normalizedValue &&
824
+ typeof normalizedValue === 'object' &&
825
+ !Array.isArray(normalizedValue) &&
826
+ !isPlainObject(normalizedValue)) {
827
+ console.log('skipping non-plain object value for attribute', key, normalizedValue);
828
+ continue;
829
+ }
830
+ try {
831
+ result[key] = encodeAttributeValue(normalizedValue);
832
+ }
833
+ catch {
834
+ console.log('skipping non-serializable value for attribute', key);
835
+ }
836
+ }
837
+ return result;
838
+ }
839
+ function decodeAttributeValueAsJson(value) {
840
+ try {
841
+ return JSON.parse(value);
842
+ }
843
+ catch {
844
+ return value;
845
+ }
846
+ }
847
+ function onlyChangedKeys(oldObj, newObj) {
848
+ const changes = {};
849
+ for (const [key, newValue] of Object.entries(newObj)) {
850
+ const oldValue = oldObj[key];
851
+ if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) {
852
+ changes[key] = newValue;
853
+ }
854
+ }
855
+ return changes;
856
+ }
857
+ export async function applyAttributes(node, _attributes) {
858
+ if (!node || !_attributes || !Object.keys(_attributes).length) {
859
+ return;
860
+ }
861
+ // Check permissions before proceeding
862
+ checkPermissions('Node.setAttributes');
863
+ // Decode all attribute values
864
+ const decodedAttrs = {};
865
+ for (const [key, value] of Object.entries(_attributes)) {
866
+ decodedAttrs[key] = decodeAttributeValueAsJson(value);
867
+ }
868
+ // Treat undefined as "not provided" and do not forward it
869
+ for (const [key, value] of Object.entries(decodedAttrs)) {
870
+ if (value === undefined) {
871
+ delete decodedAttrs[key];
872
+ }
873
+ }
874
+ // Handle font selector if present
875
+ if (decodedAttrs.font && supportsFont(node)) {
876
+ const fontSelector = decodedAttrs.font;
877
+ const fonts = await framer.getFonts();
878
+ const font = fonts.find((f) => f.selector === fontSelector);
879
+ if (!font) {
880
+ throw new Error(`Font with selector "${fontSelector}" not found`);
881
+ }
882
+ decodedAttrs.font = font;
883
+ }
884
+ // Handle style references that start with /
885
+ // These could be TextStyle or ColorStyle paths
886
+ for (const [key, value] of Object.entries(decodedAttrs)) {
887
+ if (typeof value === 'string' && value.startsWith('/')) {
888
+ if (key === 'inlineTextStyle') {
889
+ // It's a TextStyle path
890
+ const textStyles = await framer.getTextStyles();
891
+ const textStyle = textStyles.find((ts) => ts.path === value);
892
+ if (!textStyle) {
893
+ throw new Error(`TextStyle with path "${value}" not found`);
894
+ }
895
+ decodedAttrs[key] = textStyle;
896
+ }
897
+ else if (key === 'backgroundColor') {
898
+ // It's a ColorStyle path
899
+ const colorStyles = await framer.getColorStyles();
900
+ const colorStyle = colorStyles.find((cs) => cs.path === value);
901
+ if (!colorStyle) {
902
+ throw new Error(`ColorStyle with path "${value}" not found`);
903
+ }
904
+ decodedAttrs[key] = colorStyle;
905
+ }
906
+ }
907
+ }
908
+ const hasBorderWidth = Object.prototype.hasOwnProperty.call(decodedAttrs, 'borderWidth');
909
+ const hasBorderStyle = Object.prototype.hasOwnProperty.call(decodedAttrs, 'borderStyle');
910
+ const hasBorderColor = Object.prototype.hasOwnProperty.call(decodedAttrs, 'borderColor');
911
+ if (supportsBorder(node) && (hasBorderWidth || hasBorderStyle || hasBorderColor)) {
912
+ if (!(hasBorderWidth && hasBorderStyle && hasBorderColor)) {
913
+ throw new Error('borderWidth, borderStyle, and borderColor must be provided together');
914
+ }
915
+ const borderWidth = decodedAttrs.borderWidth;
916
+ const borderStyle = decodedAttrs.borderStyle;
917
+ const borderColor = decodedAttrs.borderColor;
918
+ // Explicit clear semantics: all null means remove border
919
+ if (borderWidth === null && borderStyle === null && borderColor === null) {
920
+ decodedAttrs.border = null;
921
+ delete decodedAttrs.borderWidth;
922
+ delete decodedAttrs.borderStyle;
923
+ delete decodedAttrs.borderColor;
924
+ }
925
+ else {
926
+ if (typeof borderWidth !== 'string' ||
927
+ typeof borderStyle !== 'string' ||
928
+ typeof borderColor !== 'string') {
929
+ throw new Error('borderWidth, borderStyle, and borderColor must all be strings, or all null to clear border');
930
+ }
931
+ let resolvedBorderColor = borderColor;
932
+ if (borderColor.startsWith('/')) {
933
+ const colorStyles = await framer.getColorStyles();
934
+ const colorStyle = colorStyles.find((cs) => cs.path === borderColor);
935
+ if (!colorStyle) {
936
+ throw new Error(`ColorStyle with path "${borderColor}" not found`);
937
+ }
938
+ resolvedBorderColor = colorStyle;
939
+ }
940
+ decodedAttrs.border = {
941
+ width: borderWidth,
942
+ style: borderStyle,
943
+ color: resolvedBorderColor,
944
+ };
945
+ delete decodedAttrs.borderWidth;
946
+ delete decodedAttrs.borderStyle;
947
+ delete decodedAttrs.borderColor;
948
+ }
949
+ }
950
+ // Handle backgroundImage URL
951
+ if (decodedAttrs.backgroundImage &&
952
+ typeof decodedAttrs.backgroundImage === 'string' &&
953
+ supportsBackgroundImage(node)) {
954
+ const imageUrl = decodedAttrs.backgroundImage;
955
+ // Check if the image needs to be uploaded (not already on framerusercontent.com)
956
+ if (!imageUrl.includes('framerusercontent.com')) {
957
+ // Check cache first
958
+ let imageAsset = uploadedImagesCache.get(imageUrl);
959
+ if (!imageAsset) {
960
+ try {
961
+ // Upload the image and get the ImageAsset
962
+ imageAsset = await framer.uploadImage({
963
+ image: imageUrl,
964
+ name: 'background-image',
965
+ });
966
+ }
967
+ catch (error) {
968
+ throw new Error(`Failed to upload background image from URL "${imageUrl}": ${error}`);
969
+ }
970
+ }
971
+ uploadedImagesCache.set(imageUrl, imageAsset);
972
+ decodedAttrs.backgroundImage = imageAsset;
973
+ }
974
+ // If it's already on framerusercontent.com, leave it as-is for Framer to handle
975
+ }
976
+ // Map XML attribute names to Framer API names for layout attributes
977
+ const attributeMapping = {
978
+ 'stackWrap': 'stackWrapEnabled',
979
+ 'gridColumns': 'gridColumnCount',
980
+ 'gridRows': 'gridRowCount',
981
+ 'gridFillWidth': 'gridItemFillCellWidth',
982
+ 'gridFillHeight': 'gridItemFillCellHeight',
983
+ 'gridAlignX': 'gridItemHorizontalAlignment',
984
+ 'gridAlignY': 'gridItemVerticalAlignment',
985
+ };
986
+ // Apply the mapping
987
+ for (const [xmlName, framerName] of Object.entries(attributeMapping)) {
988
+ if (xmlName in decodedAttrs) {
989
+ decodedAttrs[framerName] = decodedAttrs[xmlName];
990
+ delete decodedAttrs[xmlName];
991
+ }
992
+ }
993
+ // For component instances, separate controls from other attributes
994
+ if (isComponentInstanceNode(node)) {
995
+ // Component instances only have these standard node attributes
996
+ const standardNodeAttrs = [
997
+ 'opacity',
998
+ 'visible',
999
+ 'locked',
1000
+ 'position',
1001
+ 'width',
1002
+ 'height',
1003
+ 'rotation',
1004
+ 'componentId', // componentId cannot be updated
1005
+ ];
1006
+ const nodeAttrs = {};
1007
+ const controlsAttrs = {};
1008
+ for (const [key, value] of Object.entries(decodedAttrs)) {
1009
+ if (standardNodeAttrs.includes(key)) {
1010
+ nodeAttrs[key] = value;
1011
+ }
1012
+ else {
1013
+ // It's a control property
1014
+ controlsAttrs[key] = value;
1015
+ }
1016
+ }
1017
+ // Apply node-level attributes
1018
+ const changedNodeAttrs = onlyChangedKeys(node, nodeAttrs);
1019
+ if (Object.keys(changedNodeAttrs).length > 0) {
1020
+ console.log(`Setting attributes on ${node.__class} (${node.id}):`, changedNodeAttrs);
1021
+ await node.setAttributes(changedNodeAttrs);
1022
+ }
1023
+ // Apply controls
1024
+ if (Object.keys(controlsAttrs).length > 0) {
1025
+ const changedControls = onlyChangedKeys(node.controls || {}, controlsAttrs);
1026
+ if (Object.keys(changedControls).length > 0) {
1027
+ const controlsUpdate = {
1028
+ controls: { ...node.controls, ...changedControls },
1029
+ };
1030
+ console.log(`Setting controls on ${node.__class} (${node.id}):`, controlsUpdate);
1031
+ await node.setAttributes(controlsUpdate);
1032
+ }
1033
+ }
1034
+ }
1035
+ else {
1036
+ // For non-component instance nodes, apply all attributes directly
1037
+ const changedNodeAttrs = onlyChangedKeys(node, decodedAttrs);
1038
+ if (Object.keys(changedNodeAttrs).length > 0) {
1039
+ await node.setAttributes(changedNodeAttrs);
1040
+ }
1041
+ }
1042
+ }
1043
+ // Helper functions that need to be in this package
1044
+ async function collectGenerator(gen) {
1045
+ const result = [];
1046
+ for await (const item of gen) {
1047
+ if (!item) {
1048
+ continue;
1049
+ }
1050
+ result.push(item);
1051
+ }
1052
+ return result;
1053
+ }
1054
+ export async function getParentNodesArray(node) {
1055
+ if (typeof node === 'string') {
1056
+ node = await framer.getNode(node);
1057
+ }
1058
+ if (!node) {
1059
+ return [];
1060
+ }
1061
+ let parent = await node.getParent();
1062
+ if (!parent) {
1063
+ console.log('no parent found', node.id);
1064
+ return [];
1065
+ }
1066
+ const parentsArray = [];
1067
+ const seen = new Set();
1068
+ while (parent) {
1069
+ if (seen.has(parent.id)) {
1070
+ break;
1071
+ }
1072
+ parentsArray.push(parent);
1073
+ seen.add(parent.id);
1074
+ const newParent = await parent.getParent();
1075
+ if (!newParent) {
1076
+ // console.log('no parent found, last one was', parent)
1077
+ break;
1078
+ }
1079
+ parent = newParent;
1080
+ }
1081
+ return parentsArray;
1082
+ }
1083
+ //# sourceMappingURL=framer.js.map
1084
+ //# sourceMappingURL=framer.js.map