@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.
- package/README.md +389 -0
- package/bin.js +5 -0
- package/dist/babel-jsx.d.ts +24 -0
- package/dist/babel-jsx.js +299 -0
- package/dist/babel-plugin-imports.d.ts +15 -0
- package/dist/babel-plugin-imports.js +211 -0
- package/dist/babel-typedoc.d.ts +39 -0
- package/dist/babel-typedoc.js +76 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +3 -0
- package/dist/cli.d.ts +59 -0
- package/dist/cli.js +821 -0
- package/dist/compat.d.ts +2 -0
- package/dist/compat.js +23 -0
- package/dist/config.d.ts +18 -0
- package/dist/config.js +27 -0
- package/dist/css-core.d.ts +50 -0
- package/dist/css-core.js +231 -0
- package/dist/css.d.ts +15 -0
- package/dist/css.js +36 -0
- package/dist/esbuild.d.ts +31 -0
- package/dist/esbuild.js +317 -0
- package/dist/exporter.d.ts +96 -0
- package/dist/exporter.js +1151 -0
- package/dist/flat-cache-interceptor.d.ts +24 -0
- package/dist/flat-cache-interceptor.js +87 -0
- package/dist/framer-chunks/chunk-IKQSD2QC.js +10 -0
- package/dist/framer-chunks/fontshare-PZLWRK4B-MHMZIGTX.js +7 -0
- package/dist/framer-chunks/fontshare-SXU5BGFE-OWTMMPGS.js +7 -0
- package/dist/framer-chunks/fontshare-TIA7QUPT-PUDLUTQ7.js +7 -0
- package/dist/framer-chunks/framer-font-RD2SUPQH-Q4MS7WS6.js +7 -0
- package/dist/framer-chunks/google-EGNT223R-P4DUHBW2.js +7 -0
- package/dist/framer-chunks/google-H6SFY4F5-5JSJCGDR.js +7 -0
- package/dist/framer-chunks/google-YSYBFRE6-L7YAHH7V.js +7 -0
- package/dist/framer-client.server.d.ts +4 -0
- package/dist/framer-client.server.js +45 -0
- package/dist/framer-package-versions.d.ts +781 -0
- package/dist/framer-package-versions.js +782 -0
- package/dist/framer-types.d.ts +30 -0
- package/dist/framer-types.js +35 -0
- package/dist/framer.d.ts +4990 -0
- package/dist/framer.js +64296 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +13 -0
- package/dist/package-manager.d.ts +10 -0
- package/dist/package-manager.js +138 -0
- package/dist/plugin-mcp-dist/lib/client-websocket.d.ts +5 -0
- package/dist/plugin-mcp-dist/lib/client-websocket.js +103 -0
- package/dist/plugin-mcp-dist/lib/cms.d.ts +10 -0
- package/dist/plugin-mcp-dist/lib/cms.js +58 -0
- package/dist/plugin-mcp-dist/lib/errors.d.ts +5 -0
- package/dist/plugin-mcp-dist/lib/errors.js +48 -0
- package/dist/plugin-mcp-dist/lib/framer-client.d.ts +2 -0
- package/dist/plugin-mcp-dist/lib/framer-client.js +4 -0
- package/dist/plugin-mcp-dist/lib/framer-client.server.d.ts +3 -0
- package/dist/plugin-mcp-dist/lib/framer-client.server.js +46 -0
- package/dist/plugin-mcp-dist/lib/framer.d.ts +30 -0
- package/dist/plugin-mcp-dist/lib/framer.js +1084 -0
- package/dist/plugin-mcp-dist/lib/hooks.d.ts +6 -0
- package/dist/plugin-mcp-dist/lib/hooks.js +46 -0
- package/dist/plugin-mcp-dist/lib/mcp-client.d.ts +142 -0
- package/dist/plugin-mcp-dist/lib/mcp-client.js +40 -0
- package/dist/plugin-mcp-dist/lib/mcp-handlers.d.ts +385 -0
- package/dist/plugin-mcp-dist/lib/mcp-handlers.js +1924 -0
- package/dist/plugin-mcp-dist/lib/mcp-tools.d.ts +5 -0
- package/dist/plugin-mcp-dist/lib/mcp-tools.js +5 -0
- package/dist/plugin-mcp-dist/lib/mcp-websocket.d.ts +10 -0
- package/dist/plugin-mcp-dist/lib/mcp-websocket.js +88 -0
- package/dist/plugin-mcp-dist/lib/plugin-websocket.d.ts +5 -0
- package/dist/plugin-mcp-dist/lib/plugin-websocket.js +170 -0
- package/dist/plugin-mcp-dist/lib/react-export.d.ts +51 -0
- package/dist/plugin-mcp-dist/lib/react-export.js +358 -0
- package/dist/plugin-mcp-dist/lib/schema.d.ts +276 -0
- package/dist/plugin-mcp-dist/lib/schema.js +907 -0
- package/dist/plugin-mcp-dist/lib/store.d.ts +2 -0
- package/dist/plugin-mcp-dist/lib/store.js +10 -0
- package/dist/plugin-mcp-dist/lib/tree-utils.d.ts +6 -0
- package/dist/plugin-mcp-dist/lib/tree-utils.js +141 -0
- package/dist/plugin-mcp-dist/lib/tunnel.d.ts +46 -0
- package/dist/plugin-mcp-dist/lib/tunnel.js +117 -0
- package/dist/plugin-mcp-dist/lib/types.d.ts +110 -0
- package/dist/plugin-mcp-dist/lib/types.js +188 -0
- package/dist/plugin-mcp-dist/lib/upstream-socket.d.ts +13 -0
- package/dist/plugin-mcp-dist/lib/upstream-socket.js +56 -0
- package/dist/plugin-mcp-dist/lib/utils.d.ts +13 -0
- package/dist/plugin-mcp-dist/lib/utils.js +57 -0
- package/dist/plugin-mcp-dist/lib/websocket-server.d.ts +10 -0
- package/dist/plugin-mcp-dist/lib/websocket-server.js +88 -0
- package/dist/plugin-mcp-dist/lib/xml.d.ts +10 -0
- package/dist/plugin-mcp-dist/lib/xml.js +395 -0
- package/dist/react.d.ts +76 -0
- package/dist/react.js +280 -0
- package/dist/renamer.d.ts +11 -0
- package/dist/renamer.js +139 -0
- package/dist/sentry.d.ts +2 -0
- package/dist/sentry.js +5 -0
- package/dist/stackblitz.d.ts +9 -0
- package/dist/stackblitz.js +179 -0
- package/dist/styles/framer.css +920 -0
- package/dist/styles/reset.css +37 -0
- package/dist/typescript.d.ts +24 -0
- package/dist/typescript.js +197 -0
- package/dist/undici-dispatcher.d.ts +2 -0
- package/dist/undici-dispatcher.js +10 -0
- package/dist/unframer-loader.d.ts +2 -0
- package/dist/unframer-loader.js +12 -0
- package/dist/utils.d.ts +20 -0
- package/dist/utils.js +81 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- 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
|