@jay-framework/jay-stack-cli 0.12.0 → 0.13.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/agent-kit-template/INSTRUCTIONS.md +9 -5
- package/agent-kit-template/cli-commands.md +1 -1
- package/agent-kit-template/contracts-and-plugins.md +68 -4
- package/dist/index.d.ts +74 -2
- package/dist/index.js +1973 -530
- package/lib/vendors/README.md +510 -0
- package/lib/vendors/figma/README.md +396 -0
- package/package.json +10 -10
- package/test/vendors/figma/fixtures/README.md +164 -0
package/dist/index.js
CHANGED
|
@@ -7,10 +7,8 @@ import path from "path";
|
|
|
7
7
|
import fs, { promises } from "fs";
|
|
8
8
|
import YAML from "yaml";
|
|
9
9
|
import { getLogger, createDevLogger, setDevLogger } from "@jay-framework/logger";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { parseJayFile, JAY_IMPORT_RESOLVER, generateElementDefinitionFile, parseContract, ContractTagType, generateElementFile } from "@jay-framework/compiler-jay-html";
|
|
13
|
-
import { JAY_CONTRACT_EXTENSION, JAY_EXTENSION, JayAtomicType, JayEnumType, loadPluginManifest, RuntimeMode, GenerateTarget } from "@jay-framework/compiler-shared";
|
|
10
|
+
import { parseJayFile, JAY_IMPORT_RESOLVER, generateElementDefinitionFile, ContractTagType, parseContract, generateElementFile } from "@jay-framework/compiler-jay-html";
|
|
11
|
+
import { JAY_CONTRACT_EXTENSION, JAY_EXTENSION, resolvePluginManifest, LOCAL_PLUGIN_PATH, JayAtomicType, JayEnumType, loadPluginManifest, RuntimeMode, GenerateTarget } from "@jay-framework/compiler-shared";
|
|
14
12
|
import { listContracts, materializeContracts } from "@jay-framework/stack-server-runtime";
|
|
15
13
|
import { listContracts as listContracts2, materializeContracts as materializeContracts2 } from "@jay-framework/stack-server-runtime";
|
|
16
14
|
import { Command } from "commander";
|
|
@@ -88,9 +86,1470 @@ function updateConfig(updates) {
|
|
|
88
86
|
getLogger().warn(`Failed to update .jay config file: ${error}`);
|
|
89
87
|
}
|
|
90
88
|
}
|
|
89
|
+
function rgbToHex(color, opacity) {
|
|
90
|
+
const r = Math.round(color.r * 255);
|
|
91
|
+
const g = Math.round(color.g * 255);
|
|
92
|
+
const b = Math.round(color.b * 255);
|
|
93
|
+
if (opacity !== void 0 && opacity < 1) {
|
|
94
|
+
const alphaHex = Math.round(opacity * 255).toString(16).padStart(2, "0");
|
|
95
|
+
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}${alphaHex}`;
|
|
96
|
+
} else {
|
|
97
|
+
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function getPositionType(node) {
|
|
101
|
+
if (node.layoutPositioning === "ABSOLUTE") {
|
|
102
|
+
return "absolute";
|
|
103
|
+
}
|
|
104
|
+
if (node.parentOverflowDirection && node.parentOverflowDirection !== "NONE") {
|
|
105
|
+
if (node.parentNumberOfFixedChildren && node.parentChildIndex !== void 0) {
|
|
106
|
+
if (node.parentChildIndex >= 0 && node.parentChildIndex < node.parentNumberOfFixedChildren) {
|
|
107
|
+
return "sticky";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (node.scrollBehavior === "FIXED" && node.parentLayoutMode === "NONE") {
|
|
112
|
+
return "fixed";
|
|
113
|
+
}
|
|
114
|
+
if (node.parentType === "SECTION") {
|
|
115
|
+
return "absolute";
|
|
116
|
+
}
|
|
117
|
+
if (node.parentLayoutMode === "NONE") {
|
|
118
|
+
return "absolute";
|
|
119
|
+
}
|
|
120
|
+
if (node.parentLayoutMode && (node.parentLayoutMode === "HORIZONTAL" || node.parentLayoutMode === "VERTICAL")) {
|
|
121
|
+
return "relative";
|
|
122
|
+
}
|
|
123
|
+
return "static";
|
|
124
|
+
}
|
|
125
|
+
function getPositionStyle(node) {
|
|
126
|
+
if (node.type === "COMPONENT") {
|
|
127
|
+
return "";
|
|
128
|
+
}
|
|
129
|
+
const positionType = getPositionType(node);
|
|
130
|
+
if (positionType === "static") {
|
|
131
|
+
return "";
|
|
132
|
+
}
|
|
133
|
+
if (positionType === "absolute" || positionType === "fixed") {
|
|
134
|
+
const top = node.y !== void 0 ? node.y : 0;
|
|
135
|
+
const left = node.x !== void 0 ? node.x : 0;
|
|
136
|
+
return `position: ${positionType};top: ${top}px;left: ${left}px;`;
|
|
137
|
+
}
|
|
138
|
+
if (positionType === "sticky") {
|
|
139
|
+
return `position: ${positionType};top: 0;z-index: 10;`;
|
|
140
|
+
}
|
|
141
|
+
return `position: ${positionType};`;
|
|
142
|
+
}
|
|
143
|
+
function getAutoLayoutChildSizeStyles(node) {
|
|
144
|
+
if (!node.parentLayoutMode || node.parentLayoutMode === "NONE") {
|
|
145
|
+
const width2 = node.width !== void 0 ? node.width : 0;
|
|
146
|
+
const height2 = node.height !== void 0 ? node.height : 0;
|
|
147
|
+
return `width: ${width2}px;height: ${height2}px;`;
|
|
148
|
+
}
|
|
149
|
+
let styles = "";
|
|
150
|
+
if (!node.layoutGrow && !node.layoutAlign) {
|
|
151
|
+
const width2 = node.width !== void 0 ? node.width : 0;
|
|
152
|
+
const height2 = node.height !== void 0 ? node.height : 0;
|
|
153
|
+
return `width: ${width2}px;height: ${height2}px;`;
|
|
154
|
+
}
|
|
155
|
+
const isHorizontalLayout = node.parentLayoutMode === "HORIZONTAL";
|
|
156
|
+
const width = node.width !== void 0 ? node.width : 0;
|
|
157
|
+
const height = node.height !== void 0 ? node.height : 0;
|
|
158
|
+
if (node.layoutSizingHorizontal) {
|
|
159
|
+
switch (node.layoutSizingHorizontal) {
|
|
160
|
+
case "FIXED":
|
|
161
|
+
styles += `width: ${width}px;`;
|
|
162
|
+
break;
|
|
163
|
+
case "HUG":
|
|
164
|
+
styles += "width: fit-content;";
|
|
165
|
+
break;
|
|
166
|
+
case "FILL":
|
|
167
|
+
if (node.type === "TEXT") {
|
|
168
|
+
styles += "width: auto;";
|
|
169
|
+
} else if (isHorizontalLayout) {
|
|
170
|
+
styles += "flex-grow: 1;";
|
|
171
|
+
} else {
|
|
172
|
+
styles += "width: 100%;";
|
|
173
|
+
}
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
if (isHorizontalLayout && node.layoutGrow && node.layoutGrow > 0) {
|
|
178
|
+
styles += `flex-grow: ${node.layoutGrow};width: 0;`;
|
|
179
|
+
} else if (!isHorizontalLayout && node.layoutAlign === "STRETCH") {
|
|
180
|
+
styles += "width: 100%;";
|
|
181
|
+
} else {
|
|
182
|
+
styles += `width: ${width}px;`;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (node.layoutSizingVertical) {
|
|
186
|
+
switch (node.layoutSizingVertical) {
|
|
187
|
+
case "FIXED":
|
|
188
|
+
styles += `height: ${height}px;`;
|
|
189
|
+
break;
|
|
190
|
+
case "HUG":
|
|
191
|
+
styles += "height: fit-content;";
|
|
192
|
+
break;
|
|
193
|
+
case "FILL":
|
|
194
|
+
if (!isHorizontalLayout) {
|
|
195
|
+
styles += "flex-grow: 1;";
|
|
196
|
+
} else {
|
|
197
|
+
styles += "height: 100%;";
|
|
198
|
+
}
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
if (!isHorizontalLayout && node.layoutGrow && node.layoutGrow > 0) {
|
|
203
|
+
styles += `flex-grow: ${node.layoutGrow};height: 0;`;
|
|
204
|
+
} else if (isHorizontalLayout && node.layoutAlign === "STRETCH") {
|
|
205
|
+
styles += "height: 100%;";
|
|
206
|
+
} else {
|
|
207
|
+
styles += `height: ${height}px;`;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (node.layoutAlign) {
|
|
211
|
+
switch (node.layoutAlign) {
|
|
212
|
+
case "MIN":
|
|
213
|
+
styles += "align-self: flex-start;";
|
|
214
|
+
break;
|
|
215
|
+
case "CENTER":
|
|
216
|
+
styles += "align-self: center;";
|
|
217
|
+
break;
|
|
218
|
+
case "MAX":
|
|
219
|
+
styles += "align-self: flex-end;";
|
|
220
|
+
break;
|
|
221
|
+
case "STRETCH":
|
|
222
|
+
styles += "align-self: stretch;";
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return styles;
|
|
227
|
+
}
|
|
228
|
+
function getNodeSizeStyles(node) {
|
|
229
|
+
if (node.parentType === "SECTION") {
|
|
230
|
+
const height = node.height !== void 0 ? node.height : 0;
|
|
231
|
+
return `width: 100%;height: ${height}px;`;
|
|
232
|
+
}
|
|
233
|
+
return getAutoLayoutChildSizeStyles(node);
|
|
234
|
+
}
|
|
235
|
+
function getCommonStyles(node) {
|
|
236
|
+
let styles = "";
|
|
237
|
+
const transformStyles = [];
|
|
238
|
+
if (node.opacity !== void 0 && node.opacity < 1) {
|
|
239
|
+
styles += `opacity: ${node.opacity};`;
|
|
240
|
+
}
|
|
241
|
+
if (node.rotation !== void 0 && node.rotation !== 0) {
|
|
242
|
+
transformStyles.push(`rotate(${node.rotation}deg)`);
|
|
243
|
+
}
|
|
244
|
+
if (node.effects && Array.isArray(node.effects) && node.effects.length > 0) {
|
|
245
|
+
const visibleEffects = node.effects.filter((e) => e.visible !== false).reverse();
|
|
246
|
+
const filterFunctions = [];
|
|
247
|
+
const boxShadows = [];
|
|
248
|
+
for (const effect of visibleEffects) {
|
|
249
|
+
switch (effect.type) {
|
|
250
|
+
case "DROP_SHADOW":
|
|
251
|
+
case "INNER_SHADOW": {
|
|
252
|
+
if (effect.color && effect.offset && effect.radius !== void 0) {
|
|
253
|
+
const { offset, radius, color, spread } = effect;
|
|
254
|
+
const shadowColor = `rgba(${Math.round(color.r * 255)}, ${Math.round(color.g * 255)}, ${Math.round(color.b * 255)}, ${color.a ?? 1})`;
|
|
255
|
+
const inset = effect.type === "INNER_SHADOW" ? "inset " : "";
|
|
256
|
+
boxShadows.push(
|
|
257
|
+
`${inset}${offset.x}px ${offset.y}px ${radius}px ${spread ?? 0}px ${shadowColor}`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
case "LAYER_BLUR":
|
|
263
|
+
if (effect.radius !== void 0) {
|
|
264
|
+
filterFunctions.push(`blur(${effect.radius}px)`);
|
|
265
|
+
}
|
|
266
|
+
break;
|
|
267
|
+
case "BACKGROUND_BLUR":
|
|
268
|
+
if (effect.radius !== void 0) {
|
|
269
|
+
styles += `backdrop-filter: blur(${effect.radius}px);`;
|
|
270
|
+
styles += `-webkit-backdrop-filter: blur(${effect.radius}px);`;
|
|
271
|
+
}
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (boxShadows.length > 0) {
|
|
276
|
+
styles += `box-shadow: ${boxShadows.join(", ")};`;
|
|
277
|
+
}
|
|
278
|
+
if (filterFunctions.length > 0) {
|
|
279
|
+
styles += `filter: ${filterFunctions.join(" ")};`;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (transformStyles.length > 0) {
|
|
283
|
+
styles += `transform: ${transformStyles.join(" ")};`;
|
|
284
|
+
const width = node.width !== void 0 ? node.width : 0;
|
|
285
|
+
const height = node.height !== void 0 ? node.height : 0;
|
|
286
|
+
styles += `transform-origin: ${width / 2}px ${height / 2}px;`;
|
|
287
|
+
}
|
|
288
|
+
return styles;
|
|
289
|
+
}
|
|
290
|
+
function getBorderRadius(node) {
|
|
291
|
+
if (typeof node.cornerRadius === "number") {
|
|
292
|
+
return `border-radius: ${node.cornerRadius}px;`;
|
|
293
|
+
} else if (node.cornerRadius === "MIXED" && node.topLeftRadius !== void 0) {
|
|
294
|
+
return `border-radius: ${node.topLeftRadius}px ${node.topRightRadius}px ${node.bottomRightRadius}px ${node.bottomLeftRadius}px;`;
|
|
295
|
+
}
|
|
296
|
+
return "border-radius: 0px;";
|
|
297
|
+
}
|
|
298
|
+
function getAutoLayoutStyles(node) {
|
|
299
|
+
if (node.layoutMode === "NONE" || !node.layoutMode) {
|
|
300
|
+
return "";
|
|
301
|
+
}
|
|
302
|
+
let flexStyles = "display: flex;";
|
|
303
|
+
if (node.layoutMode === "HORIZONTAL") {
|
|
304
|
+
flexStyles += "flex-direction: row;";
|
|
305
|
+
} else if (node.layoutMode === "VERTICAL") {
|
|
306
|
+
flexStyles += "flex-direction: column;";
|
|
307
|
+
}
|
|
308
|
+
if (node.primaryAxisAlignItems) {
|
|
309
|
+
switch (node.primaryAxisAlignItems) {
|
|
310
|
+
case "MIN":
|
|
311
|
+
flexStyles += "justify-content: flex-start;";
|
|
312
|
+
break;
|
|
313
|
+
case "CENTER":
|
|
314
|
+
flexStyles += "justify-content: center;";
|
|
315
|
+
break;
|
|
316
|
+
case "MAX":
|
|
317
|
+
flexStyles += "justify-content: flex-end;";
|
|
318
|
+
break;
|
|
319
|
+
case "SPACE_BETWEEN":
|
|
320
|
+
flexStyles += "justify-content: space-between;";
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (node.counterAxisAlignItems) {
|
|
325
|
+
switch (node.counterAxisAlignItems) {
|
|
326
|
+
case "MIN":
|
|
327
|
+
flexStyles += "align-items: flex-start;";
|
|
328
|
+
break;
|
|
329
|
+
case "CENTER":
|
|
330
|
+
flexStyles += "align-items: center;";
|
|
331
|
+
break;
|
|
332
|
+
case "MAX":
|
|
333
|
+
flexStyles += "align-items: flex-end;";
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (typeof node.itemSpacing === "number") {
|
|
338
|
+
flexStyles += `gap: ${node.itemSpacing}px;`;
|
|
339
|
+
}
|
|
340
|
+
if (typeof node.paddingLeft === "number")
|
|
341
|
+
flexStyles += `padding-left: ${node.paddingLeft}px;`;
|
|
342
|
+
if (typeof node.paddingRight === "number")
|
|
343
|
+
flexStyles += `padding-right: ${node.paddingRight}px;`;
|
|
344
|
+
if (typeof node.paddingTop === "number")
|
|
345
|
+
flexStyles += `padding-top: ${node.paddingTop}px;`;
|
|
346
|
+
if (typeof node.paddingBottom === "number")
|
|
347
|
+
flexStyles += `padding-bottom: ${node.paddingBottom}px;`;
|
|
348
|
+
return flexStyles;
|
|
349
|
+
}
|
|
350
|
+
function getOverflowStyles(node) {
|
|
351
|
+
let overflowStyles = "";
|
|
352
|
+
const shouldClip = node.clipsContent;
|
|
353
|
+
const overflowDirection = node.overflowDirection || "NONE";
|
|
354
|
+
switch (overflowDirection) {
|
|
355
|
+
case "HORIZONTAL":
|
|
356
|
+
overflowStyles += shouldClip ? "overflow-x: auto; overflow-y: hidden;" : "overflow-x: auto; overflow-y: visible;";
|
|
357
|
+
break;
|
|
358
|
+
case "VERTICAL":
|
|
359
|
+
overflowStyles += shouldClip ? "overflow-x: hidden; overflow-y: auto;" : "overflow-x: visible; overflow-y: auto;";
|
|
360
|
+
break;
|
|
361
|
+
case "BOTH":
|
|
362
|
+
overflowStyles += "overflow: auto;";
|
|
363
|
+
break;
|
|
364
|
+
case "NONE":
|
|
365
|
+
default:
|
|
366
|
+
overflowStyles += shouldClip ? "overflow: hidden;" : "overflow: visible;";
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
if (overflowDirection !== "NONE") {
|
|
370
|
+
overflowStyles += "scrollbar-width: thin; scrollbar-color: rgba(0, 0, 0, 0.3) transparent;";
|
|
371
|
+
}
|
|
372
|
+
return overflowStyles;
|
|
373
|
+
}
|
|
374
|
+
function getBackgroundFillsStyle(node) {
|
|
375
|
+
if (!node.fills || !Array.isArray(node.fills) || node.fills.length === 0) {
|
|
376
|
+
return "background: transparent;";
|
|
377
|
+
}
|
|
378
|
+
const backgrounds = [];
|
|
379
|
+
const backgroundSizes = [];
|
|
380
|
+
const backgroundPositions = [];
|
|
381
|
+
const backgroundRepeats = [];
|
|
382
|
+
for (const fill of [...node.fills].reverse()) {
|
|
383
|
+
if (fill.visible === false)
|
|
384
|
+
continue;
|
|
385
|
+
if (fill.type === "SOLID" && fill.color) {
|
|
386
|
+
const { r, g, b } = fill.color;
|
|
387
|
+
const opacity = fill.opacity !== void 0 ? fill.opacity : 1;
|
|
388
|
+
backgrounds.push(
|
|
389
|
+
`linear-gradient(rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, ${opacity}), rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, ${opacity}))`
|
|
390
|
+
);
|
|
391
|
+
backgroundSizes.push("100% 100%");
|
|
392
|
+
backgroundPositions.push("center");
|
|
393
|
+
backgroundRepeats.push("no-repeat");
|
|
394
|
+
} else if (fill.type === "IMAGE") {
|
|
395
|
+
console.warn("Image fills are not yet supported in vendor conversion");
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (backgrounds.length === 0) {
|
|
399
|
+
return "background: transparent;";
|
|
400
|
+
}
|
|
401
|
+
let style = `background-image: ${backgrounds.join(", ")};`;
|
|
402
|
+
style += `background-size: ${backgroundSizes.join(", ")};`;
|
|
403
|
+
style += `background-position: ${backgroundPositions.join(", ")};`;
|
|
404
|
+
style += `background-repeat: ${backgroundRepeats.join(", ")};`;
|
|
405
|
+
return style;
|
|
406
|
+
}
|
|
407
|
+
function getStrokeStyles(node) {
|
|
408
|
+
if (!node.strokes || node.strokes.length === 0) {
|
|
409
|
+
return "";
|
|
410
|
+
}
|
|
411
|
+
const visibleStrokes = node.strokes.filter((s) => s.visible !== false);
|
|
412
|
+
if (visibleStrokes.length === 0) {
|
|
413
|
+
return "";
|
|
414
|
+
}
|
|
415
|
+
const stroke = visibleStrokes[0];
|
|
416
|
+
let cssProps = [];
|
|
417
|
+
if (stroke.type === "SOLID" && stroke.color) {
|
|
418
|
+
const r = Math.round(stroke.color.r * 255);
|
|
419
|
+
const g = Math.round(stroke.color.g * 255);
|
|
420
|
+
const b = Math.round(stroke.color.b * 255);
|
|
421
|
+
const a = stroke.opacity !== void 0 ? stroke.opacity : 1;
|
|
422
|
+
if (a < 1) {
|
|
423
|
+
cssProps.push(`border-color: rgba(${r}, ${g}, ${b}, ${a.toFixed(2)});`);
|
|
424
|
+
} else {
|
|
425
|
+
cssProps.push(`border-color: rgb(${r}, ${g}, ${b});`);
|
|
426
|
+
}
|
|
427
|
+
} else {
|
|
428
|
+
return "";
|
|
429
|
+
}
|
|
430
|
+
const top = typeof node.strokeTopWeight === "number" ? node.strokeTopWeight : typeof node.strokeWeight === "number" ? node.strokeWeight : 0;
|
|
431
|
+
const right = typeof node.strokeRightWeight === "number" ? node.strokeRightWeight : typeof node.strokeWeight === "number" ? node.strokeWeight : 0;
|
|
432
|
+
const bottom = typeof node.strokeBottomWeight === "number" ? node.strokeBottomWeight : typeof node.strokeWeight === "number" ? node.strokeWeight : 0;
|
|
433
|
+
const left = typeof node.strokeLeftWeight === "number" ? node.strokeLeftWeight : typeof node.strokeWeight === "number" ? node.strokeWeight : 0;
|
|
434
|
+
if (top !== 0 || right !== 0 || bottom !== 0 || left !== 0) {
|
|
435
|
+
if (top === right && right === bottom && bottom === left) {
|
|
436
|
+
cssProps.push(`border-width: ${top}px;`);
|
|
437
|
+
} else {
|
|
438
|
+
cssProps.push(`border-width: ${top}px ${right}px ${bottom}px ${left}px;`);
|
|
439
|
+
}
|
|
440
|
+
if (node.dashPattern && node.dashPattern.length > 0) {
|
|
441
|
+
cssProps.push("border-style: dashed;");
|
|
442
|
+
} else {
|
|
443
|
+
cssProps.push("border-style: solid;");
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return cssProps.join(" ");
|
|
447
|
+
}
|
|
448
|
+
function getFrameSizeStyles(node) {
|
|
449
|
+
const isTopLevel = node.parentType === "SECTION";
|
|
450
|
+
if (isTopLevel) {
|
|
451
|
+
const height2 = node.height !== void 0 ? node.height : 0;
|
|
452
|
+
return `width: 100%;height: ${height2}px;`;
|
|
453
|
+
}
|
|
454
|
+
if (node.layoutMode === "NONE" || !node.layoutMode) {
|
|
455
|
+
const width2 = node.width !== void 0 ? node.width : 0;
|
|
456
|
+
const height2 = node.height !== void 0 ? node.height : 0;
|
|
457
|
+
return `width: ${width2}px;height: ${height2}px;`;
|
|
458
|
+
}
|
|
459
|
+
let sizeStyles = "";
|
|
460
|
+
const width = node.width !== void 0 ? node.width : 0;
|
|
461
|
+
const height = node.height !== void 0 ? node.height : 0;
|
|
462
|
+
if (node.layoutSizingHorizontal) {
|
|
463
|
+
switch (node.layoutSizingHorizontal) {
|
|
464
|
+
case "FIXED":
|
|
465
|
+
sizeStyles += `width: ${width}px;`;
|
|
466
|
+
break;
|
|
467
|
+
case "HUG":
|
|
468
|
+
sizeStyles += "width: fit-content;";
|
|
469
|
+
break;
|
|
470
|
+
case "FILL":
|
|
471
|
+
sizeStyles += "width: 100%;";
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
} else {
|
|
475
|
+
sizeStyles += `width: ${width}px;`;
|
|
476
|
+
}
|
|
477
|
+
if (node.layoutSizingVertical) {
|
|
478
|
+
switch (node.layoutSizingVertical) {
|
|
479
|
+
case "FIXED":
|
|
480
|
+
sizeStyles += `height: ${height}px;`;
|
|
481
|
+
break;
|
|
482
|
+
case "HUG":
|
|
483
|
+
sizeStyles += "height: fit-content;";
|
|
484
|
+
break;
|
|
485
|
+
case "FILL":
|
|
486
|
+
sizeStyles += "height: 100%;";
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
} else {
|
|
490
|
+
sizeStyles += `height: ${height}px;`;
|
|
491
|
+
}
|
|
492
|
+
if (node.layoutWrap === "WRAP") {
|
|
493
|
+
sizeStyles += `max-width: ${width}px;`;
|
|
494
|
+
}
|
|
495
|
+
return sizeStyles;
|
|
496
|
+
}
|
|
497
|
+
function escapeHtmlContent(text) {
|
|
498
|
+
const map = {
|
|
499
|
+
"&": "&",
|
|
500
|
+
"<": "<",
|
|
501
|
+
">": ">",
|
|
502
|
+
'"': """,
|
|
503
|
+
"'": "'"
|
|
504
|
+
};
|
|
505
|
+
return text.replace(/[&<>"']/g, (char) => map[char]);
|
|
506
|
+
}
|
|
507
|
+
function convertTextNodeToHtml(node, indent, dynamicContent, refAttr, attributesHtml) {
|
|
508
|
+
const {
|
|
509
|
+
name,
|
|
510
|
+
id,
|
|
511
|
+
characters,
|
|
512
|
+
fontName,
|
|
513
|
+
fontSize,
|
|
514
|
+
fontWeight,
|
|
515
|
+
fills,
|
|
516
|
+
textAlignHorizontal,
|
|
517
|
+
textAlignVertical,
|
|
518
|
+
letterSpacing,
|
|
519
|
+
lineHeight,
|
|
520
|
+
textDecoration,
|
|
521
|
+
textCase,
|
|
522
|
+
textTruncation,
|
|
523
|
+
maxLines,
|
|
524
|
+
maxWidth,
|
|
525
|
+
textAutoResize,
|
|
526
|
+
hasMissingFont,
|
|
527
|
+
hyperlinks
|
|
528
|
+
} = node;
|
|
529
|
+
if (hasMissingFont || !characters) {
|
|
530
|
+
if (hasMissingFont) {
|
|
531
|
+
return `${indent}<!-- Text node "${name}" has missing fonts -->
|
|
532
|
+
`;
|
|
533
|
+
}
|
|
534
|
+
return "";
|
|
535
|
+
}
|
|
536
|
+
let fontFamilyStyle = "font-family: sans-serif;";
|
|
537
|
+
if (fontName && typeof fontName === "object" && fontName.family) {
|
|
538
|
+
fontFamilyStyle = `font-family: '${fontName.family}', sans-serif;`;
|
|
539
|
+
}
|
|
540
|
+
const fontSizeValue = typeof fontSize === "number" ? fontSize : 16;
|
|
541
|
+
const fontSizeStyle = `font-size: ${fontSizeValue}px;`;
|
|
542
|
+
const fontWeightValue = typeof fontWeight === "number" ? fontWeight : 400;
|
|
543
|
+
const fontWeightStyle = `font-weight: ${fontWeightValue};`;
|
|
544
|
+
let textColor = "#000000";
|
|
545
|
+
if (fills && Array.isArray(fills) && fills.length > 0 && fills[0].type === "SOLID" && fills[0].color) {
|
|
546
|
+
textColor = rgbToHex(fills[0].color);
|
|
547
|
+
}
|
|
548
|
+
const colorStyle = `color: ${textColor};`;
|
|
549
|
+
const textAlign = textAlignHorizontal ? textAlignHorizontal.toLowerCase() : "left";
|
|
550
|
+
const textAlignStyle = `text-align: ${textAlign};`;
|
|
551
|
+
let verticalAlignWrapperStyle = "";
|
|
552
|
+
if (textAlignVertical) {
|
|
553
|
+
verticalAlignWrapperStyle = "display: flex; flex-direction: column;";
|
|
554
|
+
switch (textAlignVertical) {
|
|
555
|
+
case "TOP":
|
|
556
|
+
verticalAlignWrapperStyle += "justify-content: flex-start;";
|
|
557
|
+
break;
|
|
558
|
+
case "CENTER":
|
|
559
|
+
verticalAlignWrapperStyle += "justify-content: center;";
|
|
560
|
+
break;
|
|
561
|
+
case "BOTTOM":
|
|
562
|
+
verticalAlignWrapperStyle += "justify-content: flex-end;";
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
let letterSpacingStyle = "";
|
|
567
|
+
if (letterSpacing && letterSpacing.value !== 0) {
|
|
568
|
+
const unit = letterSpacing.unit === "PIXELS" ? "px" : "%";
|
|
569
|
+
letterSpacingStyle = `letter-spacing: ${letterSpacing.value}${unit};`;
|
|
570
|
+
}
|
|
571
|
+
let lineHeightStyle = "";
|
|
572
|
+
if (lineHeight) {
|
|
573
|
+
if (lineHeight.unit === "AUTO") {
|
|
574
|
+
lineHeightStyle = "line-height: normal;";
|
|
575
|
+
} else {
|
|
576
|
+
const unit = lineHeight.unit === "PIXELS" ? "px" : "%";
|
|
577
|
+
lineHeightStyle = `line-height: ${lineHeight.value}${unit};`;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
let textDecorationStyle = "";
|
|
581
|
+
if (textDecoration === "UNDERLINE") {
|
|
582
|
+
textDecorationStyle = "text-decoration: underline;";
|
|
583
|
+
} else if (textDecoration === "STRIKETHROUGH") {
|
|
584
|
+
textDecorationStyle = "text-decoration: line-through;";
|
|
585
|
+
}
|
|
586
|
+
let textTransformStyle = "";
|
|
587
|
+
if (textCase && textCase !== "ORIGINAL") {
|
|
588
|
+
switch (textCase) {
|
|
589
|
+
case "UPPER":
|
|
590
|
+
textTransformStyle = "text-transform: uppercase;";
|
|
591
|
+
break;
|
|
592
|
+
case "LOWER":
|
|
593
|
+
textTransformStyle = "text-transform: lowercase;";
|
|
594
|
+
break;
|
|
595
|
+
case "TITLE":
|
|
596
|
+
textTransformStyle = "text-transform: capitalize;";
|
|
597
|
+
break;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
let truncationStyle = "";
|
|
601
|
+
if (textTruncation === "ENDING") {
|
|
602
|
+
if (maxLines && maxLines > 1) {
|
|
603
|
+
truncationStyle = `display: -webkit-box; -webkit-line-clamp: ${maxLines}; -webkit-box-orient: vertical; overflow: hidden;`;
|
|
604
|
+
} else {
|
|
605
|
+
truncationStyle = "white-space: nowrap; overflow: hidden; text-overflow: ellipsis;";
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
if (maxWidth && maxWidth > 0) {
|
|
609
|
+
truncationStyle += `max-width: ${maxWidth}px;`;
|
|
610
|
+
}
|
|
611
|
+
const positionStyle = getPositionStyle(node);
|
|
612
|
+
let sizeStyles = getNodeSizeStyles(node);
|
|
613
|
+
const commonStyles = getCommonStyles(node);
|
|
614
|
+
if (textAutoResize === "HEIGHT") {
|
|
615
|
+
sizeStyles = sizeStyles.replace(/height: [^;]+;/, "height: auto;");
|
|
616
|
+
}
|
|
617
|
+
const textStyles = `${fontFamilyStyle}${fontSizeStyle}${fontWeightStyle}${colorStyle}${textAlignStyle}${letterSpacingStyle}${lineHeightStyle}${textDecorationStyle}${textTransformStyle}${truncationStyle}`;
|
|
618
|
+
let htmlContent = "";
|
|
619
|
+
if (dynamicContent) {
|
|
620
|
+
htmlContent = dynamicContent;
|
|
621
|
+
} else if (hyperlinks && hyperlinks.length > 0) {
|
|
622
|
+
let lastEnd = 0;
|
|
623
|
+
for (const link of hyperlinks) {
|
|
624
|
+
if (link.start > lastEnd) {
|
|
625
|
+
const beforeText = characters.substring(lastEnd, link.start);
|
|
626
|
+
htmlContent += escapeHtmlContent(beforeText).replace(/\n/g, "<br>");
|
|
627
|
+
}
|
|
628
|
+
const linkText = characters.substring(link.start, link.end + 1);
|
|
629
|
+
htmlContent += `<a href="${escapeHtmlContent(link.url)}" style="color: inherit;">${escapeHtmlContent(linkText).replace(/\n/g, "<br>")}</a>`;
|
|
630
|
+
lastEnd = link.end + 1;
|
|
631
|
+
}
|
|
632
|
+
if (lastEnd < characters.length) {
|
|
633
|
+
const afterText = characters.substring(lastEnd);
|
|
634
|
+
htmlContent += escapeHtmlContent(afterText).replace(/\n/g, "<br>");
|
|
635
|
+
}
|
|
636
|
+
} else {
|
|
637
|
+
htmlContent = escapeHtmlContent(characters).replace(/\n/g, "<br>");
|
|
638
|
+
}
|
|
639
|
+
const childIndent = indent + " ";
|
|
640
|
+
const innerIndent = indent + " ";
|
|
641
|
+
const styleAttr = `${positionStyle}${sizeStyles}${commonStyles}${textStyles}`;
|
|
642
|
+
const refString = refAttr || "";
|
|
643
|
+
const attrsString = attributesHtml || "";
|
|
644
|
+
if (verticalAlignWrapperStyle) {
|
|
645
|
+
return `${indent}<div data-figma-id="${id}"${refString}${attrsString} style="${styleAttr}${verticalAlignWrapperStyle}">
|
|
646
|
+
${childIndent}<div style="${textStyles}">
|
|
647
|
+
${innerIndent}${htmlContent}
|
|
648
|
+
${childIndent}</div>
|
|
649
|
+
${indent}</div>
|
|
650
|
+
`;
|
|
651
|
+
} else {
|
|
652
|
+
return `${indent}<div data-figma-id="${id}"${refString}${attrsString} style="${styleAttr}">${htmlContent}</div>
|
|
653
|
+
`;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
function convertImageNodeToHtml(node, indent, srcBinding, altBinding, refAttr, staticImageUrl) {
|
|
657
|
+
const { name, id } = node;
|
|
658
|
+
const positionStyle = getPositionStyle(node);
|
|
659
|
+
const commonStyles = getCommonStyles(node);
|
|
660
|
+
const borderRadius = getBorderRadius(node);
|
|
661
|
+
const sizeStyles = getNodeSizeStyles(node);
|
|
662
|
+
const styles = `${positionStyle}${sizeStyles}${borderRadius}${commonStyles}`.trim();
|
|
663
|
+
let src = "";
|
|
664
|
+
if (srcBinding) {
|
|
665
|
+
src = srcBinding;
|
|
666
|
+
} else if (staticImageUrl) {
|
|
667
|
+
src = staticImageUrl;
|
|
668
|
+
} else {
|
|
669
|
+
src = "/placeholder-image.png";
|
|
670
|
+
console.warn(`Image node "${name}" (${id}) has no src binding or static image`);
|
|
671
|
+
}
|
|
672
|
+
const alt = altBinding || name;
|
|
673
|
+
const refAttribute = refAttr || "";
|
|
674
|
+
const styleAttribute = styles ? ` style="${styles}"` : "";
|
|
675
|
+
const dataAttribute = ` data-figma-id="${id}"`;
|
|
676
|
+
return `${indent}<img${dataAttribute}${refAttribute} src="${src}" alt="${alt}"${styleAttribute} />
|
|
677
|
+
`;
|
|
678
|
+
}
|
|
679
|
+
function extractStaticImageUrl(node) {
|
|
680
|
+
if (!node.fills || !Array.isArray(node.fills)) {
|
|
681
|
+
return void 0;
|
|
682
|
+
}
|
|
683
|
+
for (const fill of node.fills) {
|
|
684
|
+
if (fill.visible !== false && fill.type === "IMAGE") {
|
|
685
|
+
if (fill.imageUrl) {
|
|
686
|
+
return fill.imageUrl;
|
|
687
|
+
}
|
|
688
|
+
if (fill.imageHash) {
|
|
689
|
+
console.warn(
|
|
690
|
+
`Image fill with hash "${fill.imageHash}" found on node "${node.name}" (${node.id}) but no imageUrl in serialized data. Update plugin serialization to export and save images.`
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
return void 0;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return void 0;
|
|
697
|
+
}
|
|
698
|
+
function convertRectangleToHtml(node, indent) {
|
|
699
|
+
const { id } = node;
|
|
700
|
+
const positionStyle = getPositionStyle(node);
|
|
701
|
+
const sizeStyles = getNodeSizeStyles(node);
|
|
702
|
+
const commonStyles = getCommonStyles(node);
|
|
703
|
+
const backgroundStyle = getBackgroundFillsStyle(node);
|
|
704
|
+
const borderRadius = getBorderRadius(node);
|
|
705
|
+
const strokeStyles = getStrokeStyles(node);
|
|
706
|
+
const allStyles = `${positionStyle}${sizeStyles}${backgroundStyle}${strokeStyles}${borderRadius}${commonStyles}box-sizing: border-box;`;
|
|
707
|
+
return `${indent}<div data-figma-id="${id}" style="${allStyles}"></div>
|
|
708
|
+
`;
|
|
709
|
+
}
|
|
710
|
+
function convertEllipseToHtml(node, indent) {
|
|
711
|
+
const { id } = node;
|
|
712
|
+
const positionStyle = getPositionStyle(node);
|
|
713
|
+
const sizeStyles = getNodeSizeStyles(node);
|
|
714
|
+
const commonStyles = getCommonStyles(node);
|
|
715
|
+
const backgroundStyle = getBackgroundFillsStyle(node);
|
|
716
|
+
const strokeStyles = getStrokeStyles(node);
|
|
717
|
+
const borderRadius = "border-radius: 50%;";
|
|
718
|
+
const allStyles = `${positionStyle}${sizeStyles}${backgroundStyle}${strokeStyles}${borderRadius}${commonStyles}box-sizing: border-box;`;
|
|
719
|
+
return `${indent}<div data-figma-id="${id}" style="${allStyles}"></div>
|
|
720
|
+
`;
|
|
721
|
+
}
|
|
722
|
+
function convertVectorToHtml(node, indent) {
|
|
723
|
+
const { id, name, svgContent, svgExportFailed, width, height } = node;
|
|
724
|
+
const positionStyle = getPositionStyle(node);
|
|
725
|
+
const sizeStyles = getNodeSizeStyles(node);
|
|
726
|
+
const commonStyles = getCommonStyles(node);
|
|
727
|
+
let finalSvgContent;
|
|
728
|
+
if (svgExportFailed || !svgContent) {
|
|
729
|
+
finalSvgContent = `<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg"><rect width="${width}" height="${height}" fill="none" stroke="#ccc" stroke-width="1" stroke-dasharray="5,5"/><text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-size="10" fill="#999">Vector: ${name}</text></svg>`;
|
|
730
|
+
} else {
|
|
731
|
+
finalSvgContent = svgContent;
|
|
732
|
+
}
|
|
733
|
+
const allStyles = `${positionStyle}${sizeStyles}${commonStyles}box-sizing: border-box;`;
|
|
734
|
+
const childIndent = indent + " ";
|
|
735
|
+
return `${indent}<div data-figma-id="${id}" data-figma-type="vector" style="${allStyles}">
|
|
736
|
+
${childIndent}${finalSvgContent}
|
|
737
|
+
${indent}</div>
|
|
738
|
+
`;
|
|
739
|
+
}
|
|
740
|
+
function getComponentVariantValues(node, propertyBindings) {
|
|
741
|
+
const values = /* @__PURE__ */ new Map();
|
|
742
|
+
const filterPseudoVariants = (variantValues) => {
|
|
743
|
+
return variantValues.filter((value) => !value.includes(":"));
|
|
744
|
+
};
|
|
745
|
+
if (node.componentPropertyDefinitions) {
|
|
746
|
+
for (const binding of propertyBindings) {
|
|
747
|
+
const propDef = node.componentPropertyDefinitions[binding.property];
|
|
748
|
+
if (propDef && propDef.type === "VARIANT" && propDef.variantOptions) {
|
|
749
|
+
const filtered = filterPseudoVariants(propDef.variantOptions);
|
|
750
|
+
if (filtered.length > 0) {
|
|
751
|
+
values.set(binding.property, filtered);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
if (values.size === 0 && node.variants && node.variants.length > 0) {
|
|
757
|
+
const propertyValuesMap = /* @__PURE__ */ new Map();
|
|
758
|
+
for (const variant of node.variants) {
|
|
759
|
+
if (variant.variantProperties) {
|
|
760
|
+
for (const binding of propertyBindings) {
|
|
761
|
+
const propValue = variant.variantProperties[binding.property];
|
|
762
|
+
if (propValue && !propValue.includes(":")) {
|
|
763
|
+
if (!propertyValuesMap.has(binding.property)) {
|
|
764
|
+
propertyValuesMap.set(binding.property, /* @__PURE__ */ new Set());
|
|
765
|
+
}
|
|
766
|
+
propertyValuesMap.get(binding.property).add(propValue);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
for (const [prop, valueSet] of propertyValuesMap) {
|
|
772
|
+
if (valueSet.size > 0) {
|
|
773
|
+
values.set(prop, Array.from(valueSet));
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return values;
|
|
778
|
+
}
|
|
779
|
+
function isBooleanVariant(values, contractTag) {
|
|
780
|
+
if (contractTag.dataType !== "boolean") {
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
783
|
+
if (values.length !== 2) {
|
|
784
|
+
return false;
|
|
785
|
+
}
|
|
786
|
+
const sortedValues = [...values].sort();
|
|
787
|
+
return sortedValues[0] === "false" && sortedValues[1] === "true";
|
|
788
|
+
}
|
|
789
|
+
function generatePermutations(propertyValues, bindings) {
|
|
790
|
+
const properties = Array.from(propertyValues.entries());
|
|
791
|
+
if (properties.length === 0) {
|
|
792
|
+
return [];
|
|
793
|
+
}
|
|
794
|
+
const permutations = [];
|
|
795
|
+
function generate(index, current) {
|
|
796
|
+
if (index === properties.length) {
|
|
797
|
+
permutations.push([...current]);
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
const [propName, propValues] = properties[index];
|
|
801
|
+
const binding = bindings.find((b) => b.property === propName);
|
|
802
|
+
if (!binding)
|
|
803
|
+
return;
|
|
804
|
+
const isBoolean = isBooleanVariant(propValues, binding.contractTag);
|
|
805
|
+
for (const value of propValues) {
|
|
806
|
+
current.push({ property: propName, tagPath: binding.tagPath, value, isBoolean });
|
|
807
|
+
generate(index + 1, current);
|
|
808
|
+
current.pop();
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
generate(0, []);
|
|
812
|
+
return permutations;
|
|
813
|
+
}
|
|
814
|
+
function findComponentVariant(node, permutation) {
|
|
815
|
+
if (!node.variants || node.variants.length === 0) {
|
|
816
|
+
throw new Error(
|
|
817
|
+
`Node "${node.name}" has no variants array - cannot find variant component`
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
const targetProps = /* @__PURE__ */ new Map();
|
|
821
|
+
for (const { property, value } of permutation) {
|
|
822
|
+
targetProps.set(property, value);
|
|
823
|
+
}
|
|
824
|
+
const matchingVariant = node.variants.find((variant) => {
|
|
825
|
+
if (!variant.variantProperties) {
|
|
826
|
+
return false;
|
|
827
|
+
}
|
|
828
|
+
for (const [prop, value] of targetProps) {
|
|
829
|
+
if (variant.variantProperties[prop] !== value) {
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
for (const [prop, value] of Object.entries(variant.variantProperties)) {
|
|
834
|
+
if (targetProps.has(prop) && targetProps.get(prop) !== value) {
|
|
835
|
+
return false;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return true;
|
|
839
|
+
});
|
|
840
|
+
if (!matchingVariant) {
|
|
841
|
+
console.log(
|
|
842
|
+
`No matching variant found for "${node.name}" with properties:`,
|
|
843
|
+
Object.fromEntries(targetProps),
|
|
844
|
+
"\nAvailable variants:",
|
|
845
|
+
node.variants.map((v) => v.variantProperties),
|
|
846
|
+
"\nUsing first variant as fallback"
|
|
847
|
+
);
|
|
848
|
+
return node.variants[0] || node;
|
|
849
|
+
}
|
|
850
|
+
return matchingVariant;
|
|
851
|
+
}
|
|
852
|
+
function buildVariantCondition(permutation) {
|
|
853
|
+
const conditions = permutation.map(({ tagPath, value, isBoolean }) => {
|
|
854
|
+
if (isBoolean) {
|
|
855
|
+
if (value === "true") {
|
|
856
|
+
return tagPath;
|
|
857
|
+
} else {
|
|
858
|
+
return `!${tagPath}`;
|
|
859
|
+
}
|
|
860
|
+
} else {
|
|
861
|
+
return `${tagPath} == ${value}`;
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
return conditions.join(" && ");
|
|
865
|
+
}
|
|
866
|
+
function convertVariantNode(node, analysis, context, convertNodeToJayHtml2) {
|
|
867
|
+
const indent = " ".repeat(context.indentLevel);
|
|
868
|
+
const innerIndent = " ".repeat(context.indentLevel + 1);
|
|
869
|
+
const propertyValues = getComponentVariantValues(node, analysis.propertyBindings);
|
|
870
|
+
const permutations = generatePermutations(propertyValues, analysis.propertyBindings);
|
|
871
|
+
if (permutations.length === 0) {
|
|
872
|
+
throw new Error(
|
|
873
|
+
`No permutations generated for variant node "${node.name}" - check property definitions`
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
let variantHtml = "";
|
|
877
|
+
for (const permutation of permutations) {
|
|
878
|
+
const conditions = buildVariantCondition(permutation);
|
|
879
|
+
const variantNode = findComponentVariant(node, permutation);
|
|
880
|
+
variantHtml += `${innerIndent}<div if="${conditions}">
|
|
881
|
+
`;
|
|
882
|
+
const variantContext = {
|
|
883
|
+
...context,
|
|
884
|
+
indentLevel: context.indentLevel + 2
|
|
885
|
+
// +2 because we're inside wrapper and if div
|
|
886
|
+
};
|
|
887
|
+
if (variantNode.children && variantNode.children.length > 0) {
|
|
888
|
+
for (const child of variantNode.children) {
|
|
889
|
+
variantHtml += convertNodeToJayHtml2(child, variantContext);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
variantHtml += `${innerIndent}</div>
|
|
893
|
+
`;
|
|
894
|
+
}
|
|
895
|
+
const positionStyle = getPositionStyle(node);
|
|
896
|
+
const frameSizeStyles = getFrameSizeStyles(node);
|
|
897
|
+
const backgroundStyle = getBackgroundFillsStyle(node);
|
|
898
|
+
const borderRadius = getBorderRadius(node);
|
|
899
|
+
const strokeStyles = getStrokeStyles(node);
|
|
900
|
+
const flexStyles = getAutoLayoutStyles(node);
|
|
901
|
+
const overflowStyles = getOverflowStyles(node);
|
|
902
|
+
const commonStyles = getCommonStyles(node);
|
|
903
|
+
const wrapperStyleAttr = `${positionStyle}${frameSizeStyles}${backgroundStyle}${strokeStyles}${borderRadius}${overflowStyles}${commonStyles}${flexStyles}box-sizing: border-box;`;
|
|
904
|
+
let refAttr = "";
|
|
905
|
+
if (analysis.refPath) {
|
|
906
|
+
refAttr = ` ref="${analysis.refPath}"`;
|
|
907
|
+
} else if (analysis.dualPath) {
|
|
908
|
+
refAttr = ` ref="${analysis.dualPath}"`;
|
|
909
|
+
} else if (analysis.interactiveVariantPath) {
|
|
910
|
+
refAttr = ` ref="${analysis.interactiveVariantPath}"`;
|
|
911
|
+
}
|
|
912
|
+
return `${indent}<div id="${node.id}" data-figma-id="${node.id}" data-figma-type="variant-container"${refAttr} style="${wrapperStyleAttr}">
|
|
913
|
+
` + variantHtml + `${indent}</div>
|
|
914
|
+
`;
|
|
915
|
+
}
|
|
916
|
+
function convertRepeaterNode(node, analysis, context, convertNodeToJayHtml2) {
|
|
917
|
+
const { repeaterPath, trackByKey } = analysis;
|
|
918
|
+
const indent = " ".repeat(context.indentLevel);
|
|
919
|
+
const innerIndent = " ".repeat(context.indentLevel + 1);
|
|
920
|
+
if (node.type !== "FRAME") {
|
|
921
|
+
throw new Error(`Repeater node "${node.name}" must be a FRAME (got: ${node.type})`);
|
|
922
|
+
}
|
|
923
|
+
if (!node.layoutMode || node.layoutMode === "NONE") {
|
|
924
|
+
throw new Error(
|
|
925
|
+
`Repeater node "${node.name}" must have auto-layout (HORIZONTAL or VERTICAL)`
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
const positionStyle = getPositionStyle(node);
|
|
929
|
+
const frameSizeStyles = getFrameSizeStyles(node);
|
|
930
|
+
const backgroundStyle = getBackgroundFillsStyle(node);
|
|
931
|
+
const borderRadius = getBorderRadius(node);
|
|
932
|
+
const strokeStyles = getStrokeStyles(node);
|
|
933
|
+
const flexStyles = getAutoLayoutStyles(node);
|
|
934
|
+
const overflowStyles = getOverflowStyles(node);
|
|
935
|
+
const commonStyles = getCommonStyles(node);
|
|
936
|
+
const outerStyleAttr = `${positionStyle}${frameSizeStyles}${backgroundStyle}${strokeStyles}${borderRadius}${overflowStyles}${commonStyles}${flexStyles}box-sizing: border-box;`;
|
|
937
|
+
let innerDivSizeStyles = "";
|
|
938
|
+
if (node.layoutWrap === "WRAP") {
|
|
939
|
+
innerDivSizeStyles = "width: fit-content; height: fit-content;";
|
|
940
|
+
} else if (node.layoutMode === "HORIZONTAL") {
|
|
941
|
+
innerDivSizeStyles = "height: 100%;";
|
|
942
|
+
} else if (node.layoutMode === "VERTICAL") {
|
|
943
|
+
innerDivSizeStyles = "width: 100%;";
|
|
944
|
+
}
|
|
945
|
+
let html = `${indent}<div id="${node.id}" data-figma-id="${node.id}" data-figma-type="frame-repeater" style="${outerStyleAttr}">
|
|
946
|
+
`;
|
|
947
|
+
html += `${innerIndent}<div style="position: relative; ${innerDivSizeStyles}" forEach="${repeaterPath}" trackBy="${trackByKey}">
|
|
948
|
+
`;
|
|
949
|
+
const newContext = {
|
|
950
|
+
...context,
|
|
951
|
+
repeaterPathStack: [...context.repeaterPathStack, repeaterPath.split(".")],
|
|
952
|
+
indentLevel: context.indentLevel + 2
|
|
953
|
+
// +2 because we're inside both divs
|
|
954
|
+
};
|
|
955
|
+
if (node.children && node.children.length > 0) {
|
|
956
|
+
html += convertNodeToJayHtml2(node.children[0], newContext);
|
|
957
|
+
} else {
|
|
958
|
+
throw new Error(
|
|
959
|
+
`Repeater node "${node.name}" has no children - repeater template is required`
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
html += `${innerIndent}</div>
|
|
963
|
+
`;
|
|
964
|
+
html += `${indent}</div>
|
|
965
|
+
`;
|
|
966
|
+
return html;
|
|
967
|
+
}
|
|
968
|
+
function convertGroupNode(node, analysis, context, convertNodeToJayHtml2) {
|
|
969
|
+
const indent = " ".repeat(context.indentLevel);
|
|
970
|
+
const positionStyle = getPositionStyle(node);
|
|
971
|
+
const sizeStyles = getNodeSizeStyles(node);
|
|
972
|
+
const commonStyles = getCommonStyles(node);
|
|
973
|
+
const styleAttr = `${positionStyle}${sizeStyles}${commonStyles}box-sizing: border-box;`;
|
|
974
|
+
let refAttr = "";
|
|
975
|
+
if (analysis.refPath) {
|
|
976
|
+
refAttr = ` ref="${analysis.refPath}"`;
|
|
977
|
+
} else if (analysis.dualPath) {
|
|
978
|
+
refAttr = ` ref="${analysis.dualPath}"`;
|
|
979
|
+
}
|
|
980
|
+
let htmlAttrs = `id="${node.id}" data-figma-id="${node.id}" data-figma-type="group"${refAttr} style="${styleAttr}"`;
|
|
981
|
+
for (const [attr, tagPath] of analysis.attributes) {
|
|
982
|
+
htmlAttrs += ` ${attr}="{${tagPath}}"`;
|
|
983
|
+
}
|
|
984
|
+
let html = `${indent}<div ${htmlAttrs}>
|
|
985
|
+
`;
|
|
986
|
+
const childContext = {
|
|
987
|
+
...context,
|
|
988
|
+
indentLevel: context.indentLevel + 1
|
|
989
|
+
};
|
|
990
|
+
if (node.children && node.children.length > 0) {
|
|
991
|
+
for (const child of node.children) {
|
|
992
|
+
html += convertNodeToJayHtml2(child, childContext);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
html += `${indent}</div>
|
|
996
|
+
`;
|
|
997
|
+
return html;
|
|
998
|
+
}
|
|
999
|
+
function findContractTag(tags, tagPath) {
|
|
1000
|
+
if (tagPath.length === 0) {
|
|
1001
|
+
return void 0;
|
|
1002
|
+
}
|
|
1003
|
+
const tag = tags.find((t) => t.tag === tagPath[0]);
|
|
1004
|
+
if (!tag) {
|
|
1005
|
+
return void 0;
|
|
1006
|
+
}
|
|
1007
|
+
if (tagPath.length === 1) {
|
|
1008
|
+
return tag;
|
|
1009
|
+
}
|
|
1010
|
+
if (!tag.tags || tag.tags.length === 0) {
|
|
1011
|
+
return void 0;
|
|
1012
|
+
}
|
|
1013
|
+
return findContractTag(tag.tags, tagPath.slice(1));
|
|
1014
|
+
}
|
|
1015
|
+
function findPlugin(plugins, pluginName) {
|
|
1016
|
+
return plugins.find((p) => p.name === pluginName);
|
|
1017
|
+
}
|
|
1018
|
+
function findPluginContract(plugin, componentName) {
|
|
1019
|
+
const contract = plugin.contracts.find((c) => c.name === componentName);
|
|
1020
|
+
return contract ? { tags: contract.tags } : void 0;
|
|
1021
|
+
}
|
|
1022
|
+
function findPageContract(projectPage) {
|
|
1023
|
+
return projectPage.contract ? { tags: projectPage.contract.tags } : void 0;
|
|
1024
|
+
}
|
|
1025
|
+
function isDataTag(contractTag) {
|
|
1026
|
+
if (Array.isArray(contractTag.type)) {
|
|
1027
|
+
return contractTag.type.includes("data");
|
|
1028
|
+
}
|
|
1029
|
+
return contractTag.type === "data";
|
|
1030
|
+
}
|
|
1031
|
+
function isInteractiveTag(contractTag) {
|
|
1032
|
+
if (Array.isArray(contractTag.type)) {
|
|
1033
|
+
return contractTag.type.includes("interactive");
|
|
1034
|
+
}
|
|
1035
|
+
return contractTag.type === "interactive";
|
|
1036
|
+
}
|
|
1037
|
+
function isDualTag(contractTag) {
|
|
1038
|
+
if (Array.isArray(contractTag.type)) {
|
|
1039
|
+
return contractTag.type.includes("data") && contractTag.type.includes("interactive");
|
|
1040
|
+
}
|
|
1041
|
+
return false;
|
|
1042
|
+
}
|
|
1043
|
+
function isRepeaterTag(contractTag) {
|
|
1044
|
+
return contractTag.type === "subContract" && contractTag.repeated === true;
|
|
1045
|
+
}
|
|
1046
|
+
function applyRepeaterContext(path2, repeaterStack) {
|
|
1047
|
+
for (const repeaterPath of repeaterStack) {
|
|
1048
|
+
const prefix = repeaterPath.join(".") + ".";
|
|
1049
|
+
if (path2.startsWith(prefix)) {
|
|
1050
|
+
path2 = path2.substring(prefix.length);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
return path2;
|
|
1054
|
+
}
|
|
1055
|
+
function resolveBinding(binding, context) {
|
|
1056
|
+
let contract;
|
|
1057
|
+
let key;
|
|
1058
|
+
let tagPathWithoutKey;
|
|
1059
|
+
if (binding.pageContractPath.pluginName && binding.pageContractPath.componentName) {
|
|
1060
|
+
const plugin = findPlugin(context.plugins, binding.pageContractPath.pluginName);
|
|
1061
|
+
if (!plugin) {
|
|
1062
|
+
throw new Error(`Plugin not found: ${binding.pageContractPath.pluginName}`);
|
|
1063
|
+
}
|
|
1064
|
+
contract = findPluginContract(plugin, binding.pageContractPath.componentName);
|
|
1065
|
+
if (!contract) {
|
|
1066
|
+
throw new Error(
|
|
1067
|
+
`Contract not found in plugin ${binding.pageContractPath.pluginName}: ${binding.pageContractPath.componentName}`
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
const usedComponent = context.projectPage.usedComponents?.find(
|
|
1071
|
+
(c) => c.componentName === binding.pageContractPath.componentName
|
|
1072
|
+
);
|
|
1073
|
+
if (!usedComponent) {
|
|
1074
|
+
throw new Error(
|
|
1075
|
+
`Used component not found in page: ${binding.pageContractPath.componentName}`
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
key = usedComponent.key;
|
|
1079
|
+
tagPathWithoutKey = binding.tagPath.slice(1);
|
|
1080
|
+
} else {
|
|
1081
|
+
contract = findPageContract(context.projectPage);
|
|
1082
|
+
if (!contract) {
|
|
1083
|
+
throw new Error(`Page contract not found for page ${context.projectPage.url}`);
|
|
1084
|
+
}
|
|
1085
|
+
tagPathWithoutKey = binding.tagPath;
|
|
1086
|
+
}
|
|
1087
|
+
const contractTag = findContractTag(contract.tags, tagPathWithoutKey);
|
|
1088
|
+
if (!contractTag) {
|
|
1089
|
+
throw new Error(`Contract tag not found: ${tagPathWithoutKey.join(".")} in contract`);
|
|
1090
|
+
}
|
|
1091
|
+
let fullPath;
|
|
1092
|
+
if (key) {
|
|
1093
|
+
fullPath = [key, ...tagPathWithoutKey].join(".");
|
|
1094
|
+
} else {
|
|
1095
|
+
fullPath = binding.tagPath.join(".");
|
|
1096
|
+
}
|
|
1097
|
+
fullPath = applyRepeaterContext(fullPath, context.repeaterPathStack);
|
|
1098
|
+
return { fullPath, contractTag };
|
|
1099
|
+
}
|
|
1100
|
+
function getBindingsData(node) {
|
|
1101
|
+
const bindingsDataRaw = node.pluginData?.["jay-layer-bindings"];
|
|
1102
|
+
if (bindingsDataRaw) {
|
|
1103
|
+
try {
|
|
1104
|
+
return JSON.parse(bindingsDataRaw);
|
|
1105
|
+
} catch (error) {
|
|
1106
|
+
console.warn(`Failed to parse bindings data for node ${node.name}:`, error);
|
|
1107
|
+
return [];
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
return [];
|
|
1111
|
+
}
|
|
1112
|
+
function analyzeBindings(bindings, context) {
|
|
1113
|
+
const analysis = {
|
|
1114
|
+
type: "none",
|
|
1115
|
+
attributes: /* @__PURE__ */ new Map(),
|
|
1116
|
+
propertyBindings: [],
|
|
1117
|
+
isRepeater: false
|
|
1118
|
+
};
|
|
1119
|
+
if (bindings.length === 0) {
|
|
1120
|
+
return analysis;
|
|
1121
|
+
}
|
|
1122
|
+
const resolved = bindings.map((b) => ({
|
|
1123
|
+
binding: b,
|
|
1124
|
+
...resolveBinding(b, context)
|
|
1125
|
+
})).filter((r) => r.fullPath && r.contractTag);
|
|
1126
|
+
if (resolved.length === 0) {
|
|
1127
|
+
return analysis;
|
|
1128
|
+
}
|
|
1129
|
+
const repeaterBinding = resolved.find((r) => isRepeaterTag(r.contractTag));
|
|
1130
|
+
if (repeaterBinding) {
|
|
1131
|
+
analysis.type = "repeater";
|
|
1132
|
+
analysis.isRepeater = true;
|
|
1133
|
+
analysis.repeaterPath = repeaterBinding.fullPath;
|
|
1134
|
+
analysis.repeaterTag = repeaterBinding.contractTag;
|
|
1135
|
+
analysis.trackByKey = repeaterBinding.contractTag.trackBy || "id";
|
|
1136
|
+
return analysis;
|
|
1137
|
+
}
|
|
1138
|
+
const propertyBindings = resolved.filter((r) => r.binding.property);
|
|
1139
|
+
if (propertyBindings.length > 0) {
|
|
1140
|
+
if (propertyBindings.length !== resolved.length) {
|
|
1141
|
+
throw new Error(`Node has mixed property and non-property bindings - this is invalid`);
|
|
1142
|
+
}
|
|
1143
|
+
analysis.type = "property-variant";
|
|
1144
|
+
analysis.propertyBindings = propertyBindings.map((r) => ({
|
|
1145
|
+
property: r.binding.property,
|
|
1146
|
+
tagPath: r.fullPath,
|
|
1147
|
+
contractTag: r.contractTag
|
|
1148
|
+
}));
|
|
1149
|
+
for (const r of propertyBindings) {
|
|
1150
|
+
if (isInteractiveTag(r.contractTag)) {
|
|
1151
|
+
analysis.interactiveVariantPath = r.fullPath;
|
|
1152
|
+
break;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
return analysis;
|
|
1156
|
+
}
|
|
1157
|
+
const attributeBindings = resolved.filter((r) => r.binding.attribute);
|
|
1158
|
+
if (attributeBindings.length > 0) {
|
|
1159
|
+
analysis.type = "attribute";
|
|
1160
|
+
for (const r of attributeBindings) {
|
|
1161
|
+
analysis.attributes.set(r.binding.attribute, r.fullPath);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
const contentBindings = resolved.filter((r) => !r.binding.attribute && !r.binding.property);
|
|
1165
|
+
if (contentBindings.length > 0) {
|
|
1166
|
+
const binding = contentBindings[0];
|
|
1167
|
+
if (isDualTag(binding.contractTag)) {
|
|
1168
|
+
analysis.type = "dual";
|
|
1169
|
+
analysis.dualPath = binding.fullPath;
|
|
1170
|
+
} else if (isInteractiveTag(binding.contractTag)) {
|
|
1171
|
+
analysis.type = "interactive";
|
|
1172
|
+
analysis.refPath = binding.fullPath;
|
|
1173
|
+
} else if (isDataTag(binding.contractTag)) {
|
|
1174
|
+
if (analysis.type === "attribute") {
|
|
1175
|
+
analysis.dynamicContentPath = binding.fullPath;
|
|
1176
|
+
analysis.dynamicContentTag = binding.contractTag;
|
|
1177
|
+
} else {
|
|
1178
|
+
analysis.type = "dynamic-content";
|
|
1179
|
+
analysis.dynamicContentPath = binding.fullPath;
|
|
1180
|
+
analysis.dynamicContentTag = binding.contractTag;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
return analysis;
|
|
1185
|
+
}
|
|
1186
|
+
function validateBindings(analysis, node) {
|
|
1187
|
+
if (analysis.type === "property-variant" && analysis.attributes.size > 0) {
|
|
1188
|
+
throw new Error(
|
|
1189
|
+
`Node "${node.name}" has both property and attribute bindings - this is invalid`
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
if (analysis.type === "interactive" && analysis.attributes.size > 0) {
|
|
1193
|
+
throw new Error(
|
|
1194
|
+
`Node "${node.name}" has interactive binding with attributes - this is invalid`
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
function convertRegularNode(node, analysis, context) {
|
|
1199
|
+
const indent = " ".repeat(context.indentLevel);
|
|
1200
|
+
const { type, children, pluginData } = node;
|
|
1201
|
+
const semanticHtml = pluginData?.["semanticHtml"];
|
|
1202
|
+
if (type === "TEXT") {
|
|
1203
|
+
const dynamicContent = analysis.dynamicContentPath ? `{${analysis.dynamicContentPath}}` : "";
|
|
1204
|
+
const refAttr = analysis.refPath ? ` ref="${analysis.refPath}"` : "";
|
|
1205
|
+
const dualContent = analysis.dualPath ? `{${analysis.dualPath}}` : "";
|
|
1206
|
+
const dualRef = analysis.dualPath ? ` ref="${analysis.dualPath}"` : "";
|
|
1207
|
+
let attributesHtml = "";
|
|
1208
|
+
for (const [attr, tagPath] of analysis.attributes) {
|
|
1209
|
+
attributesHtml += ` ${attr}="{${tagPath}}"`;
|
|
1210
|
+
}
|
|
1211
|
+
return convertTextNodeToHtml(
|
|
1212
|
+
node,
|
|
1213
|
+
indent,
|
|
1214
|
+
dynamicContent || dualContent,
|
|
1215
|
+
refAttr || dualRef,
|
|
1216
|
+
attributesHtml
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
if (semanticHtml === "img") {
|
|
1220
|
+
let srcBinding;
|
|
1221
|
+
let altBinding;
|
|
1222
|
+
for (const [attr, tagPath] of analysis.attributes) {
|
|
1223
|
+
if (attr === "src") {
|
|
1224
|
+
srcBinding = `{${tagPath}}`;
|
|
1225
|
+
} else if (attr === "alt") {
|
|
1226
|
+
altBinding = `{${tagPath}}`;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
let staticImageUrl;
|
|
1230
|
+
if (!srcBinding) {
|
|
1231
|
+
staticImageUrl = extractStaticImageUrl(node);
|
|
1232
|
+
}
|
|
1233
|
+
const refAttr = analysis.refPath ? ` ref="${analysis.refPath}"` : analysis.dualPath ? ` ref="${analysis.dualPath}"` : "";
|
|
1234
|
+
return convertImageNodeToHtml(
|
|
1235
|
+
node,
|
|
1236
|
+
indent,
|
|
1237
|
+
srcBinding,
|
|
1238
|
+
altBinding,
|
|
1239
|
+
refAttr,
|
|
1240
|
+
staticImageUrl
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
const positionStyle = getPositionStyle(node);
|
|
1244
|
+
const sizeStyles = getNodeSizeStyles(node);
|
|
1245
|
+
const commonStyles = getCommonStyles(node);
|
|
1246
|
+
let styleAttr = "";
|
|
1247
|
+
if (type === "FRAME") {
|
|
1248
|
+
const backgroundStyle = getBackgroundFillsStyle(node);
|
|
1249
|
+
const borderRadius = getBorderRadius(node);
|
|
1250
|
+
const strokeStyles = getStrokeStyles(node);
|
|
1251
|
+
const flexStyles = getAutoLayoutStyles(node);
|
|
1252
|
+
const overflowStyles = getOverflowStyles(node);
|
|
1253
|
+
const frameSizeStyles = getFrameSizeStyles(node);
|
|
1254
|
+
styleAttr = `${positionStyle}${frameSizeStyles}${backgroundStyle}${strokeStyles}${borderRadius}${overflowStyles}${commonStyles}${flexStyles}box-sizing: border-box;`;
|
|
1255
|
+
} else {
|
|
1256
|
+
styleAttr = `${positionStyle}${sizeStyles}${commonStyles}`;
|
|
1257
|
+
}
|
|
1258
|
+
const tag = semanticHtml || "div";
|
|
1259
|
+
let htmlAttrs = `data-figma-id="${node.id}" data-figma-type="${type.toLowerCase()}" style="${styleAttr}"`;
|
|
1260
|
+
if (analysis.refPath) {
|
|
1261
|
+
htmlAttrs += ` ref="${analysis.refPath}"`;
|
|
1262
|
+
} else if (analysis.dualPath) {
|
|
1263
|
+
htmlAttrs += ` ref="${analysis.dualPath}"`;
|
|
1264
|
+
}
|
|
1265
|
+
for (const [attr, tagPath] of analysis.attributes) {
|
|
1266
|
+
htmlAttrs += ` ${attr}="{${tagPath}}"`;
|
|
1267
|
+
}
|
|
1268
|
+
if (type === "RECTANGLE") {
|
|
1269
|
+
return convertRectangleToHtml(node, indent);
|
|
1270
|
+
} else if (type === "ELLIPSE") {
|
|
1271
|
+
return convertEllipseToHtml(node, indent);
|
|
1272
|
+
} else if (type === "GROUP") {
|
|
1273
|
+
return convertGroupNode(node, analysis, context, convertNodeToJayHtml);
|
|
1274
|
+
} else if (type === "VECTOR" || type === "STAR" || type === "POLYGON" || type === "LINE" || type === "BOOLEAN_OPERATION") {
|
|
1275
|
+
return convertVectorToHtml(node, indent);
|
|
1276
|
+
} else if (children && children.length > 0) {
|
|
1277
|
+
let html = `${indent}<${tag} ${htmlAttrs}>
|
|
1278
|
+
`;
|
|
1279
|
+
html += `${indent} <!-- ${node.name} -->
|
|
1280
|
+
`;
|
|
1281
|
+
const childContext = {
|
|
1282
|
+
...context,
|
|
1283
|
+
indentLevel: context.indentLevel + 1
|
|
1284
|
+
};
|
|
1285
|
+
for (const child of children) {
|
|
1286
|
+
html += convertNodeToJayHtml(child, childContext);
|
|
1287
|
+
}
|
|
1288
|
+
html += `${indent}</${tag}>
|
|
1289
|
+
`;
|
|
1290
|
+
return html;
|
|
1291
|
+
} else {
|
|
1292
|
+
return `${indent}<!-- ${node.name} (${type}) -->
|
|
1293
|
+
`;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
function convertNodeToJayHtml(node, context) {
|
|
1297
|
+
const { name, type, children, pluginData } = node;
|
|
1298
|
+
const isJPage = pluginData?.["jpage"] === "true";
|
|
1299
|
+
const urlRoute = pluginData?.["urlRoute"];
|
|
1300
|
+
if (type === "TEXT" && node.fontName) {
|
|
1301
|
+
if (typeof node.fontName === "object" && node.fontName.family) {
|
|
1302
|
+
context.fontFamilies.add(node.fontName.family);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
const indent = " ".repeat(context.indentLevel);
|
|
1306
|
+
if (type === "SECTION" && isJPage) {
|
|
1307
|
+
let html = `${indent}<section data-figma-id="${node.id}" data-page-url="${urlRoute || ""}">
|
|
1308
|
+
`;
|
|
1309
|
+
html += `${indent} <!-- Jay Page: ${name} -->
|
|
1310
|
+
`;
|
|
1311
|
+
if (children && children.length > 0) {
|
|
1312
|
+
const childContext = {
|
|
1313
|
+
...context,
|
|
1314
|
+
indentLevel: context.indentLevel + 1
|
|
1315
|
+
};
|
|
1316
|
+
for (const child of children) {
|
|
1317
|
+
html += convertNodeToJayHtml(child, childContext);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
html += `${indent}</section>
|
|
1321
|
+
`;
|
|
1322
|
+
return html;
|
|
1323
|
+
}
|
|
1324
|
+
const bindings = getBindingsData(node);
|
|
1325
|
+
const analysis = analyzeBindings(bindings, context);
|
|
1326
|
+
validateBindings(analysis, node);
|
|
1327
|
+
if (analysis.isRepeater) {
|
|
1328
|
+
return convertRepeaterNode(node, analysis, context, convertNodeToJayHtml);
|
|
1329
|
+
}
|
|
1330
|
+
if (analysis.type === "property-variant") {
|
|
1331
|
+
return convertVariantNode(node, analysis, context, convertNodeToJayHtml);
|
|
1332
|
+
}
|
|
1333
|
+
return convertRegularNode(node, analysis, context);
|
|
1334
|
+
}
|
|
1335
|
+
function findContentFrame(section) {
|
|
1336
|
+
if (!section.children || section.children.length === 0) {
|
|
1337
|
+
return {
|
|
1338
|
+
frame: null,
|
|
1339
|
+
error: `Jay Page section "${section.name}" has no children`
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
const frameNodes = section.children.filter((child) => child.type === "FRAME");
|
|
1343
|
+
if (frameNodes.length === 0) {
|
|
1344
|
+
return {
|
|
1345
|
+
frame: null,
|
|
1346
|
+
error: `Jay Page section "${section.name}" has no FrameNode children. Found: ${section.children.map((c) => c.type).join(", ")}`
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
if (frameNodes.length > 1) {
|
|
1350
|
+
return {
|
|
1351
|
+
frame: frameNodes[0],
|
|
1352
|
+
warning: `Jay Page section "${section.name}" has ${frameNodes.length} FrameNodes, using the first one`
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
return { frame: frameNodes[0] };
|
|
1356
|
+
}
|
|
1357
|
+
const figmaVendor = {
|
|
1358
|
+
vendorId: "figma",
|
|
1359
|
+
async convertToBodyHtml(vendorDoc, pageUrl, projectPage, plugins) {
|
|
1360
|
+
console.log(`🎨 Converting Figma document for page: ${pageUrl}`);
|
|
1361
|
+
console.log(` Document type: ${vendorDoc.type}, name: ${vendorDoc.name}`);
|
|
1362
|
+
const isJPage = vendorDoc.pluginData?.["jpage"] === "true";
|
|
1363
|
+
if (!isJPage) {
|
|
1364
|
+
throw new Error(
|
|
1365
|
+
`Document "${vendorDoc.name}" is not marked as a Jay Page (missing jpage='true' in pluginData)`
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
const { frame, error, warning } = findContentFrame(vendorDoc);
|
|
1369
|
+
if (error) {
|
|
1370
|
+
throw new Error(`Cannot convert to Jay HTML: ${error}`);
|
|
1371
|
+
}
|
|
1372
|
+
if (warning) {
|
|
1373
|
+
console.warn(`⚠️ ${warning}`);
|
|
1374
|
+
}
|
|
1375
|
+
if (!frame) {
|
|
1376
|
+
throw new Error(`Cannot convert to Jay HTML: No content frame found`);
|
|
1377
|
+
}
|
|
1378
|
+
console.log(` Converting content frame: ${frame.name} (${frame.type})`);
|
|
1379
|
+
const fontFamilies = /* @__PURE__ */ new Set();
|
|
1380
|
+
const context = {
|
|
1381
|
+
repeaterPathStack: [],
|
|
1382
|
+
indentLevel: 1,
|
|
1383
|
+
// Start at 1 for body content
|
|
1384
|
+
fontFamilies,
|
|
1385
|
+
projectPage,
|
|
1386
|
+
plugins
|
|
1387
|
+
};
|
|
1388
|
+
const bodyHtml = convertNodeToJayHtml(frame, context);
|
|
1389
|
+
if (fontFamilies.size > 0) {
|
|
1390
|
+
console.log(
|
|
1391
|
+
` Found ${fontFamilies.size} font families: ${Array.from(fontFamilies).join(", ")}`
|
|
1392
|
+
);
|
|
1393
|
+
}
|
|
1394
|
+
return {
|
|
1395
|
+
bodyHtml,
|
|
1396
|
+
fontFamilies,
|
|
1397
|
+
// No contract data for now - Figma vendor doesn't generate contracts yet
|
|
1398
|
+
contractData: void 0
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
};
|
|
1402
|
+
const vendorRegistry = /* @__PURE__ */ new Map([
|
|
1403
|
+
[figmaVendor.vendorId, figmaVendor]
|
|
1404
|
+
// Add more vendors here as they are contributed
|
|
1405
|
+
]);
|
|
1406
|
+
function getVendor(vendorId) {
|
|
1407
|
+
return vendorRegistry.get(vendorId);
|
|
1408
|
+
}
|
|
1409
|
+
function hasVendor(vendorId) {
|
|
1410
|
+
return vendorRegistry.has(vendorId);
|
|
1411
|
+
}
|
|
1412
|
+
function getRegisteredVendors() {
|
|
1413
|
+
return Array.from(vendorRegistry.keys());
|
|
1414
|
+
}
|
|
1415
|
+
function escapeHtml(text) {
|
|
1416
|
+
if (!text)
|
|
1417
|
+
return "";
|
|
1418
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1419
|
+
}
|
|
1420
|
+
function generateGoogleFontsLinks(fontFamilies) {
|
|
1421
|
+
if (fontFamilies.size === 0) {
|
|
1422
|
+
return "";
|
|
1423
|
+
}
|
|
1424
|
+
const families = Array.from(fontFamilies);
|
|
1425
|
+
const googleFontsUrl = `https://fonts.googleapis.com/css2?${families.map((family) => {
|
|
1426
|
+
const encodedFamily = encodeURIComponent(family).replace(/%20/g, "+");
|
|
1427
|
+
return `family=${encodedFamily}:wght@100;200;300;400;500;600;700;800;900`;
|
|
1428
|
+
}).join("&")}&display=swap`;
|
|
1429
|
+
return ` <link rel="preconnect" href="https://fonts.googleapis.com">
|
|
1430
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
1431
|
+
<link href="${googleFontsUrl}" rel="stylesheet">`;
|
|
1432
|
+
}
|
|
1433
|
+
function generateHeadlessComponentScripts(components) {
|
|
1434
|
+
if (components.length === 0) {
|
|
1435
|
+
return "";
|
|
1436
|
+
}
|
|
1437
|
+
const scriptTags = components.map(
|
|
1438
|
+
(comp) => ` <script
|
|
1439
|
+
type="application/jay-headless"
|
|
1440
|
+
plugin="${comp.plugin}"
|
|
1441
|
+
contract="${comp.contract}"
|
|
1442
|
+
key="${comp.key}"
|
|
1443
|
+
><\/script>`
|
|
1444
|
+
);
|
|
1445
|
+
return "\n" + scriptTags.join("\n");
|
|
1446
|
+
}
|
|
1447
|
+
function generateJayDataScript(contractData) {
|
|
1448
|
+
if (contractData) {
|
|
1449
|
+
return ` <script type="application/jay-data">
|
|
1450
|
+
data:
|
|
1451
|
+
${contractData.tagsYaml}
|
|
1452
|
+
<\/script>`;
|
|
1453
|
+
}
|
|
1454
|
+
return ` <script type="application/jay-data">
|
|
1455
|
+
data:
|
|
1456
|
+
<\/script>`;
|
|
1457
|
+
}
|
|
1458
|
+
function buildJayHtml(options) {
|
|
1459
|
+
const {
|
|
1460
|
+
bodyHtml,
|
|
1461
|
+
fontFamilies,
|
|
1462
|
+
contractData,
|
|
1463
|
+
headlessComponents = [],
|
|
1464
|
+
title = "Page"
|
|
1465
|
+
} = options;
|
|
1466
|
+
const fontLinks = generateGoogleFontsLinks(fontFamilies);
|
|
1467
|
+
const headlessScripts = generateHeadlessComponentScripts(headlessComponents);
|
|
1468
|
+
const jayDataScript = generateJayDataScript(contractData);
|
|
1469
|
+
return `<!DOCTYPE html>
|
|
1470
|
+
<html lang="en">
|
|
1471
|
+
<head>
|
|
1472
|
+
<meta charset="UTF-8">
|
|
1473
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1474
|
+
${fontLinks}${headlessScripts}
|
|
1475
|
+
${jayDataScript}
|
|
1476
|
+
<title>${escapeHtml(title)}</title>
|
|
1477
|
+
<style>
|
|
1478
|
+
/* Basic reset */
|
|
1479
|
+
body { margin: 0; font-family: sans-serif; }
|
|
1480
|
+
a { color: inherit; text-decoration: none; }
|
|
1481
|
+
a:hover { text-decoration: underline; }
|
|
1482
|
+
div { box-sizing: border-box; }
|
|
1483
|
+
|
|
1484
|
+
/* Scrollbar styling for Webkit browsers */
|
|
1485
|
+
::-webkit-scrollbar {
|
|
1486
|
+
width: 8px;
|
|
1487
|
+
height: 8px;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
::-webkit-scrollbar-track {
|
|
1491
|
+
background: transparent;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
::-webkit-scrollbar-thumb {
|
|
1495
|
+
background: rgba(0, 0, 0, 0.3);
|
|
1496
|
+
border-radius: 4px;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
::-webkit-scrollbar-thumb:hover {
|
|
1500
|
+
background: rgba(0, 0, 0, 0.5);
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
/* Smooth scrolling */
|
|
1504
|
+
* {
|
|
1505
|
+
scroll-behavior: smooth;
|
|
1506
|
+
}
|
|
1507
|
+
</style>
|
|
1508
|
+
</head>
|
|
1509
|
+
<body>
|
|
1510
|
+
${bodyHtml}
|
|
1511
|
+
</body>
|
|
1512
|
+
</html>`;
|
|
1513
|
+
}
|
|
1514
|
+
async function buildJayHtmlFromVendorResult(conversionResult, pageDirectory, pageTitle) {
|
|
1515
|
+
const pageConfigPath = path.join(pageDirectory, "page.conf.yaml");
|
|
1516
|
+
const headlessComponents = [];
|
|
1517
|
+
if (fs.existsSync(pageConfigPath)) {
|
|
1518
|
+
try {
|
|
1519
|
+
const configContent = await fs.promises.readFile(pageConfigPath, "utf-8");
|
|
1520
|
+
const pageConfig = YAML.parse(configContent);
|
|
1521
|
+
if (pageConfig.used_components && Array.isArray(pageConfig.used_components)) {
|
|
1522
|
+
for (const comp of pageConfig.used_components) {
|
|
1523
|
+
if (comp.plugin && comp.contract && comp.key) {
|
|
1524
|
+
headlessComponents.push({
|
|
1525
|
+
plugin: comp.plugin,
|
|
1526
|
+
contract: comp.contract,
|
|
1527
|
+
key: comp.key
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
} catch (configError) {
|
|
1533
|
+
console.warn(`Failed to read page config ${pageConfigPath}:`, configError);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
const title = pageTitle || path.basename(pageDirectory);
|
|
1537
|
+
return buildJayHtml({
|
|
1538
|
+
bodyHtml: conversionResult.bodyHtml,
|
|
1539
|
+
fontFamilies: conversionResult.fontFamilies,
|
|
1540
|
+
contractData: conversionResult.contractData,
|
|
1541
|
+
headlessComponents,
|
|
1542
|
+
title
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
91
1545
|
const PAGE_FILENAME = `page${JAY_EXTENSION}`;
|
|
92
1546
|
const PAGE_CONTRACT_FILENAME = `page${JAY_CONTRACT_EXTENSION}`;
|
|
93
1547
|
const PAGE_CONFIG_FILENAME = "page.conf.yaml";
|
|
1548
|
+
function pageUrlToDirectoryPath(pageUrl, pagesBasePath) {
|
|
1549
|
+
const routePath = pageUrl === "/" ? "" : pageUrl;
|
|
1550
|
+
const fsPath = routePath.replace(/:([^/]+)/g, "[$1]");
|
|
1551
|
+
return path.join(pagesBasePath, fsPath);
|
|
1552
|
+
}
|
|
94
1553
|
function jayTypeToString(jayType) {
|
|
95
1554
|
if (!jayType)
|
|
96
1555
|
return void 0;
|
|
@@ -103,31 +1562,30 @@ function jayTypeToString(jayType) {
|
|
|
103
1562
|
}
|
|
104
1563
|
}
|
|
105
1564
|
function convertContractTagToProtocol(tag) {
|
|
106
|
-
const
|
|
1565
|
+
const typeArray = Array.isArray(tag.type) ? tag.type : [tag.type];
|
|
1566
|
+
const typeStrings = typeArray.map((t) => ContractTagType[t]);
|
|
1567
|
+
return {
|
|
107
1568
|
tag: tag.tag,
|
|
108
|
-
type:
|
|
1569
|
+
type: typeStrings.length === 1 ? typeStrings[0] : typeStrings,
|
|
1570
|
+
dataType: tag.dataType ? jayTypeToString(tag.dataType) : void 0,
|
|
1571
|
+
elementType: tag.elementType ? tag.elementType.join(" | ") : void 0,
|
|
1572
|
+
required: tag.required,
|
|
1573
|
+
repeated: tag.repeated,
|
|
1574
|
+
trackBy: tag.trackBy,
|
|
1575
|
+
async: tag.async,
|
|
1576
|
+
phase: tag.phase,
|
|
1577
|
+
link: tag.link,
|
|
1578
|
+
tags: tag.tags ? tag.tags.map(convertContractTagToProtocol) : void 0
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
function convertContractToProtocol(contract) {
|
|
1582
|
+
return {
|
|
1583
|
+
name: contract.name,
|
|
1584
|
+
tags: contract.tags.map(convertContractTagToProtocol)
|
|
109
1585
|
};
|
|
110
|
-
if (tag.dataType) {
|
|
111
|
-
protocolTag.dataType = jayTypeToString(tag.dataType);
|
|
112
|
-
}
|
|
113
|
-
if (tag.elementType) {
|
|
114
|
-
protocolTag.elementType = tag.elementType.join(" | ");
|
|
115
|
-
}
|
|
116
|
-
if (tag.required !== void 0) {
|
|
117
|
-
protocolTag.required = tag.required;
|
|
118
|
-
}
|
|
119
|
-
if (tag.repeated !== void 0) {
|
|
120
|
-
protocolTag.repeated = tag.repeated;
|
|
121
|
-
}
|
|
122
|
-
if (tag.link) {
|
|
123
|
-
protocolTag.link = tag.link;
|
|
124
|
-
}
|
|
125
|
-
if (tag.tags) {
|
|
126
|
-
protocolTag.tags = tag.tags.map(convertContractTagToProtocol);
|
|
127
|
-
}
|
|
128
|
-
return protocolTag;
|
|
129
1586
|
}
|
|
130
|
-
function isPageDirectory(
|
|
1587
|
+
async function isPageDirectory(dirPath) {
|
|
1588
|
+
const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
|
|
131
1589
|
const hasPageHtml = entries.some((e) => e.name === PAGE_FILENAME);
|
|
132
1590
|
const hasPageContract = entries.some((e) => e.name === PAGE_CONTRACT_FILENAME);
|
|
133
1591
|
const hasPageConfig = entries.some((e) => e.name === PAGE_CONFIG_FILENAME);
|
|
@@ -137,8 +1595,7 @@ function isPageDirectory(entries) {
|
|
|
137
1595
|
async function scanPageDirectories(pagesBasePath, onPageFound) {
|
|
138
1596
|
async function scanDirectory(dirPath, urlPath = "") {
|
|
139
1597
|
try {
|
|
140
|
-
const
|
|
141
|
-
const { isPage, hasPageHtml, hasPageContract, hasPageConfig } = isPageDirectory(entries);
|
|
1598
|
+
const { isPage, hasPageHtml, hasPageContract, hasPageConfig } = await isPageDirectory(dirPath);
|
|
142
1599
|
if (isPage) {
|
|
143
1600
|
const pageUrl = urlPath || "/";
|
|
144
1601
|
const pageName = dirPath === pagesBasePath ? "Home" : path.basename(dirPath);
|
|
@@ -151,6 +1608,7 @@ async function scanPageDirectories(pagesBasePath, onPageFound) {
|
|
|
151
1608
|
hasPageConfig
|
|
152
1609
|
});
|
|
153
1610
|
}
|
|
1611
|
+
const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
|
|
154
1612
|
for (const entry of entries) {
|
|
155
1613
|
const fullPath = path.join(dirPath, entry.name);
|
|
156
1614
|
if (entry.isDirectory()) {
|
|
@@ -161,244 +1619,124 @@ async function scanPageDirectories(pagesBasePath, onPageFound) {
|
|
|
161
1619
|
}
|
|
162
1620
|
}
|
|
163
1621
|
} catch (error) {
|
|
164
|
-
getLogger().warn(`Failed to scan directory ${dirPath}
|
|
1622
|
+
getLogger().warn(`Failed to scan directory ${dirPath}:`, error);
|
|
165
1623
|
}
|
|
166
1624
|
}
|
|
167
1625
|
await scanDirectory(pagesBasePath);
|
|
168
1626
|
}
|
|
169
|
-
|
|
170
|
-
try {
|
|
171
|
-
const contractYaml = await fs.promises.readFile(contractFilePath, "utf-8");
|
|
172
|
-
const parsedContract = parseContract(contractYaml, contractFilePath);
|
|
173
|
-
if (parsedContract.validations.length > 0) {
|
|
174
|
-
getLogger().warn(
|
|
175
|
-
`Contract validation errors in ${contractFilePath}: ${parsedContract.validations.join(", ")}`
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
if (parsedContract.val) {
|
|
179
|
-
const resolvedTags = await resolveLinkedTags(
|
|
180
|
-
parsedContract.val.tags,
|
|
181
|
-
path.dirname(contractFilePath)
|
|
182
|
-
);
|
|
183
|
-
return {
|
|
184
|
-
name: parsedContract.val.name,
|
|
185
|
-
tags: resolvedTags
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
} catch (error) {
|
|
189
|
-
getLogger().warn(`Failed to parse contract file ${contractFilePath}: ${error}`);
|
|
190
|
-
}
|
|
191
|
-
return null;
|
|
192
|
-
}
|
|
193
|
-
async function resolveLinkedTags(tags, baseDir) {
|
|
1627
|
+
function expandContractTags(tags, baseDir) {
|
|
194
1628
|
const resolvedTags = [];
|
|
195
1629
|
for (const tag of tags) {
|
|
196
1630
|
if (tag.link) {
|
|
197
1631
|
try {
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
|
|
1632
|
+
const linkWithExtension = tag.link.endsWith(JAY_CONTRACT_EXTENSION) ? tag.link : tag.link + JAY_CONTRACT_EXTENSION;
|
|
1633
|
+
const linkedPath = JAY_IMPORT_RESOLVER.resolveLink(baseDir, linkWithExtension);
|
|
1634
|
+
const loadResult = JAY_IMPORT_RESOLVER.loadContract(linkedPath);
|
|
1635
|
+
if (loadResult.val) {
|
|
1636
|
+
const expandedSubTags = expandContractTags(
|
|
1637
|
+
loadResult.val.tags,
|
|
1638
|
+
path.dirname(linkedPath)
|
|
1639
|
+
);
|
|
201
1640
|
const resolvedTag = {
|
|
202
1641
|
tag: tag.tag,
|
|
203
|
-
type: tag.type
|
|
204
|
-
|
|
205
|
-
|
|
1642
|
+
type: tag.type,
|
|
1643
|
+
// Keep the original enum type
|
|
1644
|
+
tags: expandedSubTags,
|
|
1645
|
+
required: tag.required,
|
|
1646
|
+
repeated: tag.repeated,
|
|
1647
|
+
trackBy: tag.trackBy,
|
|
1648
|
+
async: tag.async,
|
|
1649
|
+
phase: tag.phase,
|
|
1650
|
+
link: tag.link
|
|
206
1651
|
};
|
|
207
|
-
if (tag.required !== void 0) {
|
|
208
|
-
resolvedTag.required = tag.required;
|
|
209
|
-
}
|
|
210
|
-
if (tag.repeated !== void 0) {
|
|
211
|
-
resolvedTag.repeated = tag.repeated;
|
|
212
|
-
}
|
|
213
1652
|
resolvedTags.push(resolvedTag);
|
|
214
1653
|
} else {
|
|
215
1654
|
getLogger().warn(`Failed to load linked contract: ${tag.link} from ${baseDir}`);
|
|
216
|
-
resolvedTags.push(
|
|
1655
|
+
resolvedTags.push(tag);
|
|
217
1656
|
}
|
|
218
1657
|
} catch (error) {
|
|
219
|
-
getLogger().warn(`Error resolving linked contract ${tag.link}
|
|
220
|
-
resolvedTags.push(
|
|
1658
|
+
getLogger().warn(`Error resolving linked contract ${tag.link}:`, error);
|
|
1659
|
+
resolvedTags.push(tag);
|
|
221
1660
|
}
|
|
222
1661
|
} else if (tag.tags) {
|
|
223
|
-
const resolvedSubTags =
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
1662
|
+
const resolvedSubTags = expandContractTags(tag.tags, baseDir);
|
|
1663
|
+
const resolvedTag = {
|
|
1664
|
+
...tag,
|
|
1665
|
+
tags: resolvedSubTags
|
|
1666
|
+
};
|
|
1667
|
+
resolvedTags.push(resolvedTag);
|
|
227
1668
|
} else {
|
|
228
|
-
resolvedTags.push(
|
|
1669
|
+
resolvedTags.push(tag);
|
|
229
1670
|
}
|
|
230
1671
|
}
|
|
231
1672
|
return resolvedTags;
|
|
232
1673
|
}
|
|
233
|
-
function
|
|
1674
|
+
function loadAndExpandContract(contractFilePath) {
|
|
234
1675
|
try {
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
1676
|
+
const loadResult = JAY_IMPORT_RESOLVER.loadContract(contractFilePath);
|
|
1677
|
+
if (loadResult.validations.length > 0) {
|
|
1678
|
+
getLogger().warn(
|
|
1679
|
+
`Contract validation errors in ${contractFilePath}:`,
|
|
1680
|
+
loadResult.validations
|
|
1681
|
+
);
|
|
1682
|
+
}
|
|
1683
|
+
if (loadResult.val) {
|
|
1684
|
+
const resolvedTags = expandContractTags(
|
|
1685
|
+
loadResult.val.tags,
|
|
1686
|
+
path.dirname(contractFilePath)
|
|
1687
|
+
);
|
|
1688
|
+
return convertContractToProtocol({
|
|
1689
|
+
name: loadResult.val.name,
|
|
1690
|
+
tags: resolvedTags
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
239
1693
|
} catch (error) {
|
|
240
|
-
getLogger().warn(
|
|
241
|
-
`Failed to resolve contract: ${appModule}/${contractFileName} - ${error instanceof Error ? error.message : error}`
|
|
242
|
-
);
|
|
243
|
-
return null;
|
|
1694
|
+
getLogger().warn(`Failed to parse contract file ${contractFilePath}:`, error);
|
|
244
1695
|
}
|
|
1696
|
+
return null;
|
|
245
1697
|
}
|
|
246
|
-
async function
|
|
247
|
-
const installedAppContracts = {};
|
|
248
|
-
const installedAppsPath = path.join(configBasePath, "installedApps");
|
|
1698
|
+
async function extractHeadlessComponentsFromJayHtml(jayHtmlContent, pageFilePath, projectRootPath) {
|
|
249
1699
|
try {
|
|
250
|
-
|
|
251
|
-
|
|
1700
|
+
const parsedJayHtml = await parseJayFile(
|
|
1701
|
+
jayHtmlContent,
|
|
1702
|
+
path.basename(pageFilePath),
|
|
1703
|
+
path.dirname(pageFilePath),
|
|
1704
|
+
{ relativePath: "" },
|
|
1705
|
+
// We don't need TypeScript config for headless extraction
|
|
1706
|
+
JAY_IMPORT_RESOLVER,
|
|
1707
|
+
projectRootPath
|
|
1708
|
+
);
|
|
1709
|
+
if (parsedJayHtml.validations.length > 0) {
|
|
1710
|
+
getLogger().warn(
|
|
1711
|
+
`Jay-HTML parsing warnings for ${pageFilePath}:`,
|
|
1712
|
+
parsedJayHtml.validations
|
|
1713
|
+
);
|
|
252
1714
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
appName,
|
|
265
|
-
module: appModule,
|
|
266
|
-
pages: [],
|
|
267
|
-
components: []
|
|
268
|
-
};
|
|
269
|
-
if (appConfig.pages && Array.isArray(appConfig.pages)) {
|
|
270
|
-
for (const page of appConfig.pages) {
|
|
271
|
-
if (page.headless_components && Array.isArray(page.headless_components)) {
|
|
272
|
-
for (const component of page.headless_components) {
|
|
273
|
-
if (component.contract) {
|
|
274
|
-
const contractPath = resolveAppContractPath(
|
|
275
|
-
appModule,
|
|
276
|
-
component.contract,
|
|
277
|
-
projectRootPath
|
|
278
|
-
);
|
|
279
|
-
if (contractPath) {
|
|
280
|
-
const contractSchema = await parseContractFile(contractPath);
|
|
281
|
-
if (contractSchema) {
|
|
282
|
-
appContracts.pages.push({
|
|
283
|
-
pageName: page.name,
|
|
284
|
-
contractSchema
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
if (appConfig.components && Array.isArray(appConfig.components)) {
|
|
294
|
-
for (const component of appConfig.components) {
|
|
295
|
-
if (component.headless_components && Array.isArray(component.headless_components)) {
|
|
296
|
-
for (const headlessComp of component.headless_components) {
|
|
297
|
-
if (headlessComp.contract) {
|
|
298
|
-
const contractPath = resolveAppContractPath(
|
|
299
|
-
appModule,
|
|
300
|
-
headlessComp.contract,
|
|
301
|
-
projectRootPath
|
|
302
|
-
);
|
|
303
|
-
if (contractPath) {
|
|
304
|
-
const contractSchema = await parseContractFile(contractPath);
|
|
305
|
-
if (contractSchema) {
|
|
306
|
-
appContracts.components.push({
|
|
307
|
-
componentName: component.name,
|
|
308
|
-
contractSchema
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
installedAppContracts[appName] = appContracts;
|
|
318
|
-
}
|
|
319
|
-
} catch (error) {
|
|
320
|
-
getLogger().warn(`Failed to parse app config ${appConfigPath}: ${error}`);
|
|
1715
|
+
if (!parsedJayHtml.val) {
|
|
1716
|
+
getLogger().warn(`Failed to parse jay-html file: ${pageFilePath}`);
|
|
1717
|
+
return [];
|
|
1718
|
+
}
|
|
1719
|
+
const resolvedComponents = [];
|
|
1720
|
+
for (const headlessImport of parsedJayHtml.val.headlessImports) {
|
|
1721
|
+
if (headlessImport.codeLink) {
|
|
1722
|
+
let pluginName = headlessImport.codeLink.module;
|
|
1723
|
+
const nodeModulesMatch = pluginName.match(/node_modules\/([^/]+)/);
|
|
1724
|
+
if (nodeModulesMatch) {
|
|
1725
|
+
pluginName = nodeModulesMatch[1];
|
|
321
1726
|
}
|
|
1727
|
+
const componentName = headlessImport.contract?.name || "unknown";
|
|
1728
|
+
resolvedComponents.push({
|
|
1729
|
+
appName: pluginName,
|
|
1730
|
+
componentName,
|
|
1731
|
+
key: headlessImport.key
|
|
1732
|
+
});
|
|
322
1733
|
}
|
|
323
1734
|
}
|
|
1735
|
+
return resolvedComponents;
|
|
324
1736
|
} catch (error) {
|
|
325
|
-
getLogger().warn(`Failed to
|
|
326
|
-
|
|
327
|
-
return installedAppContracts;
|
|
328
|
-
}
|
|
329
|
-
function extractHeadlessComponents(jayHtmlContent, installedApps, installedAppContracts) {
|
|
330
|
-
const root = parse(jayHtmlContent);
|
|
331
|
-
const headlessScripts = root.querySelectorAll('script[type="application/jay-headless"]');
|
|
332
|
-
const resolvedComponents = [];
|
|
333
|
-
for (const script of headlessScripts) {
|
|
334
|
-
const src = script.getAttribute("src") || "";
|
|
335
|
-
const name = script.getAttribute("name") || "";
|
|
336
|
-
const key = script.getAttribute("key") || "";
|
|
337
|
-
let resolved = false;
|
|
338
|
-
for (const app of installedApps) {
|
|
339
|
-
if (app.module !== src && app.name !== src) {
|
|
340
|
-
continue;
|
|
341
|
-
}
|
|
342
|
-
for (const appPage of app.pages) {
|
|
343
|
-
for (const headlessComp of appPage.headless_components) {
|
|
344
|
-
if (headlessComp.name === name && headlessComp.key === key) {
|
|
345
|
-
const appContracts = installedAppContracts[app.name];
|
|
346
|
-
if (appContracts) {
|
|
347
|
-
const matchingPageContract = appContracts.pages.find(
|
|
348
|
-
(pc) => pc.pageName === appPage.name
|
|
349
|
-
);
|
|
350
|
-
if (matchingPageContract) {
|
|
351
|
-
resolvedComponents.push({
|
|
352
|
-
appName: app.name,
|
|
353
|
-
componentName: appPage.name,
|
|
354
|
-
key
|
|
355
|
-
});
|
|
356
|
-
resolved = true;
|
|
357
|
-
break;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
if (resolved)
|
|
363
|
-
break;
|
|
364
|
-
}
|
|
365
|
-
if (resolved)
|
|
366
|
-
break;
|
|
367
|
-
for (const appComponent of app.components) {
|
|
368
|
-
for (const headlessComp of appComponent.headless_components) {
|
|
369
|
-
if (headlessComp.name === name && headlessComp.key === key) {
|
|
370
|
-
const appContracts = installedAppContracts[app.name];
|
|
371
|
-
if (appContracts) {
|
|
372
|
-
const matchingComponentContract = appContracts.components.find(
|
|
373
|
-
(cc) => cc.componentName === appComponent.name
|
|
374
|
-
);
|
|
375
|
-
if (matchingComponentContract) {
|
|
376
|
-
resolvedComponents.push({
|
|
377
|
-
appName: app.name,
|
|
378
|
-
componentName: appComponent.name,
|
|
379
|
-
key
|
|
380
|
-
});
|
|
381
|
-
resolved = true;
|
|
382
|
-
break;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
if (resolved)
|
|
388
|
-
break;
|
|
389
|
-
}
|
|
390
|
-
if (resolved)
|
|
391
|
-
break;
|
|
392
|
-
}
|
|
393
|
-
if (!resolved) {
|
|
394
|
-
resolvedComponents.push({
|
|
395
|
-
appName: src,
|
|
396
|
-
componentName: name,
|
|
397
|
-
key
|
|
398
|
-
});
|
|
399
|
-
}
|
|
1737
|
+
getLogger().warn(`Failed to parse jay-html content for ${pageFilePath}:`, error);
|
|
1738
|
+
return [];
|
|
400
1739
|
}
|
|
401
|
-
return resolvedComponents;
|
|
402
1740
|
}
|
|
403
1741
|
async function scanProjectComponents(componentsBasePath) {
|
|
404
1742
|
const components = [];
|
|
@@ -421,43 +1759,10 @@ async function scanProjectComponents(componentsBasePath) {
|
|
|
421
1759
|
}
|
|
422
1760
|
}
|
|
423
1761
|
} catch (error) {
|
|
424
|
-
getLogger().warn(`Failed to scan components directory ${componentsBasePath}
|
|
1762
|
+
getLogger().warn(`Failed to scan components directory ${componentsBasePath}:`, error);
|
|
425
1763
|
}
|
|
426
1764
|
return components;
|
|
427
1765
|
}
|
|
428
|
-
async function scanInstalledApps(configBasePath) {
|
|
429
|
-
const installedApps = [];
|
|
430
|
-
const installedAppsPath = path.join(configBasePath, "installedApps");
|
|
431
|
-
try {
|
|
432
|
-
if (!fs.existsSync(installedAppsPath)) {
|
|
433
|
-
return installedApps;
|
|
434
|
-
}
|
|
435
|
-
const appDirs = await fs.promises.readdir(installedAppsPath, { withFileTypes: true });
|
|
436
|
-
for (const appDir of appDirs) {
|
|
437
|
-
if (appDir.isDirectory()) {
|
|
438
|
-
const appConfigPath = path.join(installedAppsPath, appDir.name, "app.conf.yaml");
|
|
439
|
-
try {
|
|
440
|
-
if (fs.existsSync(appConfigPath)) {
|
|
441
|
-
const configContent = await fs.promises.readFile(appConfigPath, "utf-8");
|
|
442
|
-
const appConfig = YAML.parse(configContent);
|
|
443
|
-
installedApps.push({
|
|
444
|
-
name: appConfig.name || appDir.name,
|
|
445
|
-
module: appConfig.module || appDir.name,
|
|
446
|
-
pages: appConfig.pages || [],
|
|
447
|
-
components: appConfig.components || [],
|
|
448
|
-
config_map: appConfig.config_map || []
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
} catch (error) {
|
|
452
|
-
getLogger().warn(`Failed to parse app config ${appConfigPath}: ${error}`);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
} catch (error) {
|
|
457
|
-
getLogger().warn(`Failed to scan installed apps directory ${installedAppsPath}: ${error}`);
|
|
458
|
-
}
|
|
459
|
-
return installedApps;
|
|
460
|
-
}
|
|
461
1766
|
async function getProjectName(configBasePath) {
|
|
462
1767
|
const projectConfigPath = path.join(configBasePath, "project.conf.yaml");
|
|
463
1768
|
try {
|
|
@@ -467,266 +1772,256 @@ async function getProjectName(configBasePath) {
|
|
|
467
1772
|
return projectConfig.name || "Unnamed Project";
|
|
468
1773
|
}
|
|
469
1774
|
} catch (error) {
|
|
470
|
-
getLogger().warn(`Failed to read project config ${projectConfigPath}
|
|
1775
|
+
getLogger().warn(`Failed to read project config ${projectConfigPath}:`, error);
|
|
471
1776
|
}
|
|
472
1777
|
return "Unnamed Project";
|
|
473
1778
|
}
|
|
474
|
-
async function
|
|
1779
|
+
async function scanLocalPluginNames(projectRoot) {
|
|
475
1780
|
const plugins = [];
|
|
476
|
-
const
|
|
477
|
-
if (fs.existsSync(
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
const
|
|
1781
|
+
const localPluginsDir = path.join(projectRoot, LOCAL_PLUGIN_PATH);
|
|
1782
|
+
if (!fs.existsSync(localPluginsDir)) {
|
|
1783
|
+
return plugins;
|
|
1784
|
+
}
|
|
1785
|
+
try {
|
|
1786
|
+
const entries = await fs.promises.readdir(localPluginsDir, { withFileTypes: true });
|
|
1787
|
+
for (const entry of entries) {
|
|
1788
|
+
if (entry.isDirectory()) {
|
|
1789
|
+
const pluginDir = path.join(localPluginsDir, entry.name);
|
|
1790
|
+
const pluginYamlPath = path.join(pluginDir, "plugin.yaml");
|
|
485
1791
|
if (fs.existsSync(pluginYamlPath)) {
|
|
486
|
-
|
|
487
|
-
const yamlContent = await fs.promises.readFile(pluginYamlPath, "utf-8");
|
|
488
|
-
const manifest = YAML.parse(yamlContent);
|
|
489
|
-
plugins.push({
|
|
490
|
-
manifest,
|
|
491
|
-
location: {
|
|
492
|
-
type: "local",
|
|
493
|
-
path: pluginPath
|
|
494
|
-
}
|
|
495
|
-
});
|
|
496
|
-
} catch (error) {
|
|
497
|
-
getLogger().warn(`Failed to parse plugin.yaml for ${dir.name}: ${error}`);
|
|
498
|
-
}
|
|
1792
|
+
plugins.push(entry.name);
|
|
499
1793
|
}
|
|
500
1794
|
}
|
|
501
|
-
} catch (error) {
|
|
502
|
-
getLogger().warn(
|
|
503
|
-
`Failed to scan local plugins directory ${localPluginsPath}: ${error}`
|
|
504
|
-
);
|
|
505
1795
|
}
|
|
1796
|
+
} catch (error) {
|
|
1797
|
+
getLogger().warn(`Failed to scan local plugins directory ${localPluginsDir}:`, error);
|
|
506
1798
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
manifest: {
|
|
546
|
-
...manifest,
|
|
547
|
-
module: moduleName
|
|
548
|
-
},
|
|
549
|
-
location: {
|
|
550
|
-
type: "npm",
|
|
551
|
-
module: moduleName || manifest.name
|
|
552
|
-
}
|
|
553
|
-
});
|
|
554
|
-
} catch (error) {
|
|
555
|
-
getLogger().warn(
|
|
556
|
-
`Failed to parse plugin.yaml for package ${pkgPath}: ${error}`
|
|
557
|
-
);
|
|
558
|
-
}
|
|
1799
|
+
return plugins;
|
|
1800
|
+
}
|
|
1801
|
+
async function findPluginNamesFromPackageJson(projectRootPath) {
|
|
1802
|
+
const pluginNames = [];
|
|
1803
|
+
try {
|
|
1804
|
+
const packageJsonPath = path.join(projectRootPath, "package.json");
|
|
1805
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
1806
|
+
getLogger().warn("package.json not found");
|
|
1807
|
+
return pluginNames;
|
|
1808
|
+
}
|
|
1809
|
+
const packageJsonContent = await fs.promises.readFile(packageJsonPath, "utf-8");
|
|
1810
|
+
const packageJson = JSON.parse(packageJsonContent);
|
|
1811
|
+
const workspaceDependencies = /* @__PURE__ */ new Set();
|
|
1812
|
+
const regularDependencies = /* @__PURE__ */ new Set();
|
|
1813
|
+
for (const [depName, version] of Object.entries({
|
|
1814
|
+
...packageJson.dependencies
|
|
1815
|
+
})) {
|
|
1816
|
+
if (typeof version === "string" && version.startsWith("workspace:")) {
|
|
1817
|
+
workspaceDependencies.add(depName);
|
|
1818
|
+
} else {
|
|
1819
|
+
regularDependencies.add(depName);
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
const nodeModulesPath = path.join(projectRootPath, "node_modules");
|
|
1823
|
+
for (const depName of regularDependencies) {
|
|
1824
|
+
if (await checkPackageForPlugin(nodeModulesPath, depName)) {
|
|
1825
|
+
pluginNames.push(depName);
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
if (workspaceDependencies.size > 0) {
|
|
1829
|
+
const workspaceNodeModules = await findWorkspaceNodeModulesPath(
|
|
1830
|
+
projectRootPath,
|
|
1831
|
+
Array.from(workspaceDependencies)
|
|
1832
|
+
);
|
|
1833
|
+
if (workspaceNodeModules) {
|
|
1834
|
+
for (const depName of workspaceDependencies) {
|
|
1835
|
+
if (await checkPackageForPlugin(workspaceNodeModules, depName)) {
|
|
1836
|
+
pluginNames.push(depName);
|
|
559
1837
|
}
|
|
560
1838
|
}
|
|
561
1839
|
}
|
|
562
|
-
} catch (error) {
|
|
563
|
-
getLogger().warn(`Failed to scan node_modules for plugins: ${error}`);
|
|
564
1840
|
}
|
|
1841
|
+
} catch (error) {
|
|
1842
|
+
getLogger().error("Error finding plugins from package.json:", error);
|
|
565
1843
|
}
|
|
566
|
-
return
|
|
1844
|
+
return pluginNames;
|
|
567
1845
|
}
|
|
568
|
-
async function
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
const
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
const parsedContract = await parseContractFile(contractPath);
|
|
587
|
-
if (parsedContract) {
|
|
588
|
-
contractSchema = parsedContract;
|
|
1846
|
+
async function checkPackageForPlugin(nodeModulesDir, packageName) {
|
|
1847
|
+
try {
|
|
1848
|
+
const packageDir = path.join(nodeModulesDir, packageName);
|
|
1849
|
+
const pluginYamlPath = path.join(packageDir, "plugin.yaml");
|
|
1850
|
+
return fs.existsSync(pluginYamlPath);
|
|
1851
|
+
} catch (error) {
|
|
1852
|
+
return false;
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
async function findWorkspaceNodeModulesPath(startPath, workspaceDeps) {
|
|
1856
|
+
let currentPath = startPath;
|
|
1857
|
+
while (currentPath !== path.dirname(currentPath)) {
|
|
1858
|
+
const nodeModulesPath = path.join(currentPath, "node_modules");
|
|
1859
|
+
if (fs.existsSync(nodeModulesPath)) {
|
|
1860
|
+
for (const depName of workspaceDeps) {
|
|
1861
|
+
const depPath = path.join(nodeModulesPath, depName);
|
|
1862
|
+
if (fs.existsSync(depPath)) {
|
|
1863
|
+
return nodeModulesPath;
|
|
589
1864
|
}
|
|
590
|
-
} catch (error) {
|
|
591
|
-
getLogger().warn(`Failed to parse contract file ${contractPath}: ${error}`);
|
|
592
1865
|
}
|
|
593
1866
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
1867
|
+
currentPath = path.dirname(currentPath);
|
|
1868
|
+
}
|
|
1869
|
+
return null;
|
|
1870
|
+
}
|
|
1871
|
+
async function scanPlugins(projectRootPath) {
|
|
1872
|
+
const plugins = [];
|
|
1873
|
+
try {
|
|
1874
|
+
const [localPluginNames, dependencyPluginNames] = await Promise.all([
|
|
1875
|
+
scanLocalPluginNames(projectRootPath),
|
|
1876
|
+
findPluginNamesFromPackageJson(projectRootPath)
|
|
1877
|
+
]);
|
|
1878
|
+
const allPluginNames = [.../* @__PURE__ */ new Set([...localPluginNames, ...dependencyPluginNames])];
|
|
1879
|
+
getLogger().info(`Found ${allPluginNames.length} plugins: ${allPluginNames.join(", ")}`);
|
|
1880
|
+
for (const pluginName of allPluginNames) {
|
|
1881
|
+
const manifest = resolvePluginManifest(projectRootPath, pluginName);
|
|
1882
|
+
if (manifest.validations.length > 0) {
|
|
1883
|
+
getLogger().warn(
|
|
1884
|
+
`Failed to resolve plugin manifest for ${pluginName}:`,
|
|
1885
|
+
manifest.validations
|
|
601
1886
|
);
|
|
602
|
-
|
|
603
|
-
getLogger().warn(`Failed to read page file ${pageFilePath}: ${error}`);
|
|
1887
|
+
continue;
|
|
604
1888
|
}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
1889
|
+
if (!manifest.val) {
|
|
1890
|
+
getLogger().warn(
|
|
1891
|
+
`Failed to resolve plugin manifest for ${pluginName}:`,
|
|
1892
|
+
manifest.validations
|
|
1893
|
+
);
|
|
1894
|
+
continue;
|
|
1895
|
+
}
|
|
1896
|
+
const contracts = manifest.val.contracts;
|
|
1897
|
+
plugins.push({
|
|
1898
|
+
name: pluginName,
|
|
1899
|
+
contracts: contracts.map((contract) => {
|
|
1900
|
+
const resolveResult = JAY_IMPORT_RESOLVER.resolvePluginComponent(
|
|
1901
|
+
pluginName,
|
|
1902
|
+
contract.name,
|
|
1903
|
+
projectRootPath
|
|
1904
|
+
);
|
|
1905
|
+
if (resolveResult.validations.length > 0) {
|
|
1906
|
+
getLogger().warn(
|
|
1907
|
+
`Failed to resolve plugin component for ${pluginName}:${contract.name}:`,
|
|
1908
|
+
resolveResult.validations
|
|
1909
|
+
);
|
|
1910
|
+
return null;
|
|
1911
|
+
}
|
|
1912
|
+
if (!resolveResult.val) {
|
|
1913
|
+
getLogger().warn(
|
|
1914
|
+
`Failed to resolve plugin component for ${pluginName}:${contract.name}:`,
|
|
1915
|
+
resolveResult.validations
|
|
1916
|
+
);
|
|
1917
|
+
return null;
|
|
1918
|
+
}
|
|
1919
|
+
const expandedContract = loadAndExpandContract(resolveResult.val.contractPath);
|
|
1920
|
+
if (!expandedContract) {
|
|
1921
|
+
return null;
|
|
1922
|
+
}
|
|
1923
|
+
return expandedContract;
|
|
1924
|
+
})
|
|
1925
|
+
});
|
|
1926
|
+
}
|
|
1927
|
+
} catch (error) {
|
|
1928
|
+
getLogger().error("Error scanning plugins:", error);
|
|
1929
|
+
}
|
|
1930
|
+
return plugins;
|
|
1931
|
+
}
|
|
1932
|
+
async function loadProjectPage(pageContext, plugins) {
|
|
1933
|
+
const { dirPath, pageUrl, pageName, hasPageHtml, hasPageContract, hasPageConfig } = pageContext;
|
|
1934
|
+
const pageFilePath = path.join(dirPath, PAGE_FILENAME);
|
|
1935
|
+
const pageConfigPath = path.join(dirPath, PAGE_CONFIG_FILENAME);
|
|
1936
|
+
const contractPath = path.join(dirPath, PAGE_CONTRACT_FILENAME);
|
|
1937
|
+
const projectRootPath = process.cwd();
|
|
1938
|
+
let usedComponents = [];
|
|
1939
|
+
let contract;
|
|
1940
|
+
if (hasPageContract) {
|
|
1941
|
+
const parsedContract = loadAndExpandContract(contractPath);
|
|
1942
|
+
if (parsedContract) {
|
|
1943
|
+
contract = parsedContract;
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
if (hasPageHtml) {
|
|
1947
|
+
try {
|
|
1948
|
+
const jayHtmlContent = await fs.promises.readFile(pageFilePath, "utf-8");
|
|
1949
|
+
usedComponents = await extractHeadlessComponentsFromJayHtml(
|
|
1950
|
+
jayHtmlContent,
|
|
1951
|
+
pageFilePath,
|
|
1952
|
+
projectRootPath
|
|
1953
|
+
);
|
|
1954
|
+
} catch (error) {
|
|
1955
|
+
getLogger().warn(`Failed to read page file ${pageFilePath}:`, error);
|
|
1956
|
+
}
|
|
1957
|
+
} else if (hasPageConfig) {
|
|
1958
|
+
try {
|
|
1959
|
+
const configContent = await fs.promises.readFile(pageConfigPath, "utf-8");
|
|
1960
|
+
const pageConfig = YAML.parse(configContent);
|
|
1961
|
+
if (pageConfig.used_components && Array.isArray(pageConfig.used_components)) {
|
|
1962
|
+
for (const comp of pageConfig.used_components) {
|
|
1963
|
+
const key = comp.key || "";
|
|
1964
|
+
if (comp.plugin && comp.contract) {
|
|
1965
|
+
const plugin = plugins.find((p) => p.name === comp.plugin);
|
|
1966
|
+
if (plugin && plugin.contracts) {
|
|
1967
|
+
const contract2 = plugin.contracts.find((c) => c.name === comp.contract);
|
|
1968
|
+
if (contract2) {
|
|
1969
|
+
usedComponents.push({
|
|
1970
|
+
appName: comp.plugin,
|
|
1971
|
+
componentName: comp.contract,
|
|
1972
|
+
key
|
|
1973
|
+
});
|
|
641
1974
|
continue;
|
|
642
1975
|
}
|
|
643
|
-
for (const appPage of app.pages) {
|
|
644
|
-
for (const headlessComp of appPage.headless_components) {
|
|
645
|
-
if (headlessComp.name === name && headlessComp.key === key) {
|
|
646
|
-
const appContracts = installedAppContracts[app.name];
|
|
647
|
-
if (appContracts) {
|
|
648
|
-
const matchingPageContract = appContracts.pages.find(
|
|
649
|
-
(pc) => pc.pageName === appPage.name
|
|
650
|
-
);
|
|
651
|
-
if (matchingPageContract) {
|
|
652
|
-
usedComponents.push({
|
|
653
|
-
appName: app.name,
|
|
654
|
-
componentName: appPage.name,
|
|
655
|
-
key
|
|
656
|
-
});
|
|
657
|
-
resolved = true;
|
|
658
|
-
break;
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
if (resolved)
|
|
664
|
-
break;
|
|
665
|
-
}
|
|
666
|
-
if (resolved)
|
|
667
|
-
break;
|
|
668
|
-
for (const appComponent of app.components) {
|
|
669
|
-
for (const headlessComp of appComponent.headless_components) {
|
|
670
|
-
if (headlessComp.name === name && headlessComp.key === key) {
|
|
671
|
-
const appContracts = installedAppContracts[app.name];
|
|
672
|
-
if (appContracts) {
|
|
673
|
-
const matchingComponentContract = appContracts.components.find(
|
|
674
|
-
(cc) => cc.componentName === appComponent.name
|
|
675
|
-
);
|
|
676
|
-
if (matchingComponentContract) {
|
|
677
|
-
usedComponents.push({
|
|
678
|
-
appName: app.name,
|
|
679
|
-
componentName: appComponent.name,
|
|
680
|
-
key
|
|
681
|
-
});
|
|
682
|
-
resolved = true;
|
|
683
|
-
break;
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
if (resolved)
|
|
689
|
-
break;
|
|
690
|
-
}
|
|
691
|
-
if (resolved)
|
|
692
|
-
break;
|
|
693
|
-
}
|
|
694
|
-
if (!resolved) {
|
|
695
|
-
usedComponents.push({
|
|
696
|
-
appName: src,
|
|
697
|
-
componentName: name,
|
|
698
|
-
key
|
|
699
|
-
});
|
|
700
1976
|
}
|
|
1977
|
+
usedComponents.push({
|
|
1978
|
+
appName: comp.plugin,
|
|
1979
|
+
componentName: comp.contract,
|
|
1980
|
+
key
|
|
1981
|
+
});
|
|
1982
|
+
} else {
|
|
1983
|
+
getLogger().warn(
|
|
1984
|
+
`Invalid component definition in ${pageConfigPath}: Only plugin/contract syntax is supported for headless components. Found:`,
|
|
1985
|
+
comp
|
|
1986
|
+
);
|
|
701
1987
|
}
|
|
702
1988
|
}
|
|
703
|
-
} catch (error) {
|
|
704
|
-
getLogger().warn(`Failed to parse page config ${pageConfigPath}: ${error}`);
|
|
705
1989
|
}
|
|
1990
|
+
} catch (error) {
|
|
1991
|
+
getLogger().warn(`Failed to parse page config ${pageConfigPath}:`, error);
|
|
706
1992
|
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
1993
|
+
}
|
|
1994
|
+
return {
|
|
1995
|
+
name: pageName,
|
|
1996
|
+
url: pageUrl,
|
|
1997
|
+
filePath: pageFilePath,
|
|
1998
|
+
contract,
|
|
1999
|
+
usedComponents
|
|
2000
|
+
};
|
|
2001
|
+
}
|
|
2002
|
+
async function scanProjectInfo(pagesBasePath, componentsBasePath, configBasePath, projectRootPath) {
|
|
2003
|
+
const [projectName, components, plugins] = await Promise.all([
|
|
2004
|
+
getProjectName(configBasePath),
|
|
2005
|
+
scanProjectComponents(componentsBasePath),
|
|
2006
|
+
scanPlugins(projectRootPath)
|
|
2007
|
+
]);
|
|
2008
|
+
const pages = [];
|
|
2009
|
+
await scanPageDirectories(pagesBasePath, async (context) => {
|
|
2010
|
+
const page = await loadProjectPage(context, plugins);
|
|
2011
|
+
pages.push(page);
|
|
714
2012
|
});
|
|
715
2013
|
return {
|
|
716
2014
|
name: projectName,
|
|
717
2015
|
localPath: projectRootPath,
|
|
718
2016
|
pages,
|
|
719
2017
|
components,
|
|
720
|
-
installedApps,
|
|
721
|
-
installedAppContracts,
|
|
722
2018
|
plugins
|
|
723
2019
|
};
|
|
724
2020
|
}
|
|
725
2021
|
async function handlePagePublish(resolvedConfig, page) {
|
|
726
2022
|
try {
|
|
727
2023
|
const pagesBasePath = path.resolve(resolvedConfig.devServer.pagesBase);
|
|
728
|
-
const
|
|
729
|
-
const dirname = path.join(pagesBasePath, routePath);
|
|
2024
|
+
const dirname = pageUrlToDirectoryPath(page.route, pagesBasePath);
|
|
730
2025
|
const fullPath = path.join(dirname, PAGE_FILENAME);
|
|
731
2026
|
await fs.promises.mkdir(dirname, { recursive: true });
|
|
732
2027
|
await fs.promises.writeFile(fullPath, page.jayHtml, "utf-8");
|
|
@@ -752,7 +2047,7 @@ async function handlePagePublish(resolvedConfig, page) {
|
|
|
752
2047
|
createdJayHtml
|
|
753
2048
|
];
|
|
754
2049
|
} catch (error) {
|
|
755
|
-
getLogger().error(`Failed to publish page ${page.route}
|
|
2050
|
+
getLogger().error(`Failed to publish page ${page.route}:`, error);
|
|
756
2051
|
return [
|
|
757
2052
|
{
|
|
758
2053
|
success: false,
|
|
@@ -790,7 +2085,7 @@ async function handleComponentPublish(resolvedConfig, component) {
|
|
|
790
2085
|
createdJayHtml
|
|
791
2086
|
];
|
|
792
2087
|
} catch (error) {
|
|
793
|
-
getLogger().error(`Failed to publish component ${component.name}
|
|
2088
|
+
getLogger().error(`Failed to publish component ${component.name}:`, error);
|
|
794
2089
|
return [
|
|
795
2090
|
{
|
|
796
2091
|
success: false,
|
|
@@ -800,6 +2095,22 @@ async function handleComponentPublish(resolvedConfig, component) {
|
|
|
800
2095
|
];
|
|
801
2096
|
}
|
|
802
2097
|
}
|
|
2098
|
+
async function loadPageContracts(dirPath, pageUrl, projectRootPath) {
|
|
2099
|
+
const { hasPageHtml, hasPageContract, hasPageConfig } = await isPageDirectory(dirPath);
|
|
2100
|
+
const plugins = await scanPlugins(projectRootPath);
|
|
2101
|
+
const pageInfo = await loadProjectPage(
|
|
2102
|
+
{
|
|
2103
|
+
dirPath,
|
|
2104
|
+
pageUrl,
|
|
2105
|
+
pageName: path.basename(dirPath),
|
|
2106
|
+
hasPageHtml,
|
|
2107
|
+
hasPageContract,
|
|
2108
|
+
hasPageConfig
|
|
2109
|
+
},
|
|
2110
|
+
plugins
|
|
2111
|
+
);
|
|
2112
|
+
return { projectPage: pageInfo, plugins };
|
|
2113
|
+
}
|
|
803
2114
|
function createEditorHandlers(config, tsConfigPath, projectRoot) {
|
|
804
2115
|
const onPublish = async (params) => {
|
|
805
2116
|
const status = [];
|
|
@@ -834,7 +2145,7 @@ function createEditorHandlers(config, tsConfigPath, projectRoot) {
|
|
|
834
2145
|
);
|
|
835
2146
|
const definitionFile = generateElementDefinitionFile(parsedJayHtml);
|
|
836
2147
|
if (definitionFile.validations.length > 0)
|
|
837
|
-
getLogger().
|
|
2148
|
+
getLogger().info(
|
|
838
2149
|
`failed to generate .d.ts for ${fullPath} with validation errors: ${definitionFile.validations.join("\n")}`
|
|
839
2150
|
);
|
|
840
2151
|
else
|
|
@@ -860,7 +2171,7 @@ function createEditorHandlers(config, tsConfigPath, projectRoot) {
|
|
|
860
2171
|
imageUrl: `/images/${filename}`
|
|
861
2172
|
};
|
|
862
2173
|
} catch (error) {
|
|
863
|
-
getLogger().error(
|
|
2174
|
+
getLogger().error("Failed to save image:", error);
|
|
864
2175
|
return {
|
|
865
2176
|
type: "saveImage",
|
|
866
2177
|
success: false,
|
|
@@ -884,7 +2195,7 @@ function createEditorHandlers(config, tsConfigPath, projectRoot) {
|
|
|
884
2195
|
imageUrl: exists ? `/images/${filename}` : void 0
|
|
885
2196
|
};
|
|
886
2197
|
} catch (error) {
|
|
887
|
-
getLogger().error(
|
|
2198
|
+
getLogger().error("Failed to check image:", error);
|
|
888
2199
|
return {
|
|
889
2200
|
type: "hasImage",
|
|
890
2201
|
success: false,
|
|
@@ -898,25 +2209,23 @@ function createEditorHandlers(config, tsConfigPath, projectRoot) {
|
|
|
898
2209
|
const pagesBasePath = path.resolve(config.devServer.pagesBase);
|
|
899
2210
|
const componentsBasePath = path.resolve(config.devServer.componentsBase);
|
|
900
2211
|
const configBasePath = path.resolve(config.devServer.configBase);
|
|
901
|
-
const projectRootPath = process.cwd();
|
|
902
2212
|
const info = await scanProjectInfo(
|
|
903
2213
|
pagesBasePath,
|
|
904
2214
|
componentsBasePath,
|
|
905
2215
|
configBasePath,
|
|
906
|
-
|
|
2216
|
+
projectRoot
|
|
907
2217
|
);
|
|
908
2218
|
getLogger().info(`📋 Retrieved project info: ${info.name}`);
|
|
909
2219
|
getLogger().info(` Pages: ${info.pages.length}`);
|
|
910
2220
|
getLogger().info(` Components: ${info.components.length}`);
|
|
911
|
-
getLogger().info(`
|
|
912
|
-
getLogger().info(` App Contracts: ${Object.keys(info.installedAppContracts).length}`);
|
|
2221
|
+
getLogger().info(` plugins: ${info.plugins.length}`);
|
|
913
2222
|
return {
|
|
914
2223
|
type: "getProjectInfo",
|
|
915
2224
|
success: true,
|
|
916
2225
|
info
|
|
917
2226
|
};
|
|
918
2227
|
} catch (error) {
|
|
919
|
-
getLogger().error(
|
|
2228
|
+
getLogger().error("Failed to get project info:", error);
|
|
920
2229
|
return {
|
|
921
2230
|
type: "getProjectInfo",
|
|
922
2231
|
success: false,
|
|
@@ -926,18 +2235,118 @@ function createEditorHandlers(config, tsConfigPath, projectRoot) {
|
|
|
926
2235
|
localPath: process.cwd(),
|
|
927
2236
|
pages: [],
|
|
928
2237
|
components: [],
|
|
929
|
-
installedApps: [],
|
|
930
|
-
installedAppContracts: {},
|
|
931
2238
|
plugins: []
|
|
932
2239
|
}
|
|
933
2240
|
};
|
|
934
2241
|
}
|
|
935
2242
|
};
|
|
2243
|
+
const onExport = async (params) => {
|
|
2244
|
+
try {
|
|
2245
|
+
const pagesBasePath = path.resolve(config.devServer.pagesBase);
|
|
2246
|
+
const { vendorId, pageUrl, vendorDoc } = params;
|
|
2247
|
+
const dirname = pageUrlToDirectoryPath(pageUrl, pagesBasePath);
|
|
2248
|
+
const vendorFilename = `page.${vendorId}.json`;
|
|
2249
|
+
const vendorFilePath = path.join(dirname, vendorFilename);
|
|
2250
|
+
await fs.promises.mkdir(dirname, { recursive: true });
|
|
2251
|
+
await fs.promises.writeFile(
|
|
2252
|
+
vendorFilePath,
|
|
2253
|
+
JSON.stringify(vendorDoc, null, 2),
|
|
2254
|
+
"utf-8"
|
|
2255
|
+
);
|
|
2256
|
+
getLogger().info(`📦 Exported ${vendorId} document to: ${vendorFilePath}`);
|
|
2257
|
+
if (hasVendor(vendorId)) {
|
|
2258
|
+
getLogger().info(`🔄 Converting ${vendorId} document to Jay HTML...`);
|
|
2259
|
+
const vendor = getVendor(vendorId);
|
|
2260
|
+
try {
|
|
2261
|
+
const { projectPage, plugins } = await loadPageContracts(
|
|
2262
|
+
dirname,
|
|
2263
|
+
pageUrl,
|
|
2264
|
+
projectRoot
|
|
2265
|
+
);
|
|
2266
|
+
const conversionResult = await vendor.convertToBodyHtml(
|
|
2267
|
+
vendorDoc,
|
|
2268
|
+
pageUrl,
|
|
2269
|
+
projectPage,
|
|
2270
|
+
plugins
|
|
2271
|
+
);
|
|
2272
|
+
const fullJayHtml = await buildJayHtmlFromVendorResult(
|
|
2273
|
+
conversionResult,
|
|
2274
|
+
dirname,
|
|
2275
|
+
path.basename(dirname)
|
|
2276
|
+
);
|
|
2277
|
+
const jayHtmlPath = path.join(dirname, "page.jay-html");
|
|
2278
|
+
await fs.promises.writeFile(jayHtmlPath, fullJayHtml, "utf-8");
|
|
2279
|
+
getLogger().info(`✅ Successfully converted to Jay HTML: ${jayHtmlPath}`);
|
|
2280
|
+
return {
|
|
2281
|
+
type: "export",
|
|
2282
|
+
success: true,
|
|
2283
|
+
vendorSourcePath: vendorFilePath,
|
|
2284
|
+
jayHtmlPath
|
|
2285
|
+
};
|
|
2286
|
+
} catch (conversionError) {
|
|
2287
|
+
getLogger().error(`❌ Vendor conversion threw an error:`, conversionError);
|
|
2288
|
+
return {
|
|
2289
|
+
type: "export",
|
|
2290
|
+
success: false,
|
|
2291
|
+
vendorSourcePath: vendorFilePath,
|
|
2292
|
+
error: conversionError instanceof Error ? conversionError.message : "Unknown conversion error"
|
|
2293
|
+
};
|
|
2294
|
+
}
|
|
2295
|
+
} else {
|
|
2296
|
+
getLogger().info(`ℹ️ No vendor found for '${vendorId}'. Skipping conversion.`);
|
|
2297
|
+
}
|
|
2298
|
+
return {
|
|
2299
|
+
type: "export",
|
|
2300
|
+
success: true,
|
|
2301
|
+
vendorSourcePath: vendorFilePath
|
|
2302
|
+
};
|
|
2303
|
+
} catch (error) {
|
|
2304
|
+
getLogger().error("Failed to export vendor document:", error);
|
|
2305
|
+
return {
|
|
2306
|
+
type: "export",
|
|
2307
|
+
success: false,
|
|
2308
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
2309
|
+
};
|
|
2310
|
+
}
|
|
2311
|
+
};
|
|
2312
|
+
const onImport = async (params) => {
|
|
2313
|
+
try {
|
|
2314
|
+
const pagesBasePath = path.resolve(config.devServer.pagesBase);
|
|
2315
|
+
const { vendorId, pageUrl } = params;
|
|
2316
|
+
const dirname = pageUrlToDirectoryPath(pageUrl, pagesBasePath);
|
|
2317
|
+
const vendorFilename = `page.${vendorId}.json`;
|
|
2318
|
+
const vendorFilePath = path.join(dirname, vendorFilename);
|
|
2319
|
+
if (!fs.existsSync(vendorFilePath)) {
|
|
2320
|
+
return {
|
|
2321
|
+
type: "import",
|
|
2322
|
+
success: false,
|
|
2323
|
+
error: `No ${vendorId} document found at ${pageUrl}. File not found: ${vendorFilePath}`
|
|
2324
|
+
};
|
|
2325
|
+
}
|
|
2326
|
+
const fileContent = await fs.promises.readFile(vendorFilePath, "utf-8");
|
|
2327
|
+
const vendorDoc = JSON.parse(fileContent);
|
|
2328
|
+
getLogger().info(`📥 Imported ${vendorId} document from: ${vendorFilePath}`);
|
|
2329
|
+
return {
|
|
2330
|
+
type: "import",
|
|
2331
|
+
success: true,
|
|
2332
|
+
vendorDoc
|
|
2333
|
+
};
|
|
2334
|
+
} catch (error) {
|
|
2335
|
+
getLogger().error("Failed to import vendor document:", error);
|
|
2336
|
+
return {
|
|
2337
|
+
type: "import",
|
|
2338
|
+
success: false,
|
|
2339
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
2340
|
+
};
|
|
2341
|
+
}
|
|
2342
|
+
};
|
|
936
2343
|
return {
|
|
937
2344
|
onPublish,
|
|
938
2345
|
onSaveImage,
|
|
939
2346
|
onHasImage,
|
|
940
|
-
onGetProjectInfo
|
|
2347
|
+
onGetProjectInfo,
|
|
2348
|
+
onExport,
|
|
2349
|
+
onImport
|
|
941
2350
|
};
|
|
942
2351
|
}
|
|
943
2352
|
async function generatePageDefinitionFiles(routes, tsConfigPath, projectRoot) {
|
|
@@ -1011,6 +2420,12 @@ async function startDevServer(options = {}) {
|
|
|
1011
2420
|
}
|
|
1012
2421
|
});
|
|
1013
2422
|
const { port: editorPort, editorId } = await editorServer.start();
|
|
2423
|
+
const registeredVendors = getRegisteredVendors();
|
|
2424
|
+
if (registeredVendors.length > 0) {
|
|
2425
|
+
log.info(
|
|
2426
|
+
`📦 Registered ${registeredVendors.length} vendor(s): ${registeredVendors.join(", ")}`
|
|
2427
|
+
);
|
|
2428
|
+
}
|
|
1014
2429
|
const handlers = createEditorHandlers(
|
|
1015
2430
|
resolvedConfig,
|
|
1016
2431
|
jayOptions.tsConfigFilePath,
|
|
@@ -1020,6 +2435,8 @@ async function startDevServer(options = {}) {
|
|
|
1020
2435
|
editorServer.onSaveImage(handlers.onSaveImage);
|
|
1021
2436
|
editorServer.onHasImage(handlers.onHasImage);
|
|
1022
2437
|
editorServer.onGetProjectInfo(handlers.onGetProjectInfo);
|
|
2438
|
+
editorServer.onExport(handlers.onExport);
|
|
2439
|
+
editorServer.onImport(handlers.onImport);
|
|
1023
2440
|
const { server, viteServer, routes } = await mkDevServer({
|
|
1024
2441
|
pagesRootFolder: path.resolve(resolvedConfig.devServer.pagesBase),
|
|
1025
2442
|
projectRootFolder: process.cwd(),
|
|
@@ -1664,6 +3081,7 @@ async function initializeServicesForCli(projectRoot, viteServer) {
|
|
|
1664
3081
|
sortPluginsByDependencies,
|
|
1665
3082
|
executePluginServerInits
|
|
1666
3083
|
} = await import("@jay-framework/stack-server-runtime");
|
|
3084
|
+
let initErrors = /* @__PURE__ */ new Map();
|
|
1667
3085
|
try {
|
|
1668
3086
|
const discoveredPlugins = await discoverPluginsWithInit({
|
|
1669
3087
|
projectRoot,
|
|
@@ -1671,7 +3089,7 @@ async function initializeServicesForCli(projectRoot, viteServer) {
|
|
|
1671
3089
|
});
|
|
1672
3090
|
const pluginsWithInit = sortPluginsByDependencies(discoveredPlugins);
|
|
1673
3091
|
try {
|
|
1674
|
-
await executePluginServerInits(pluginsWithInit, viteServer, false);
|
|
3092
|
+
initErrors = await executePluginServerInits(pluginsWithInit, viteServer, false);
|
|
1675
3093
|
} catch (error) {
|
|
1676
3094
|
getLogger().warn(chalk.yellow(`⚠️ Plugin initialization skipped: ${error.message}`));
|
|
1677
3095
|
}
|
|
@@ -1691,7 +3109,7 @@ async function initializeServicesForCli(projectRoot, viteServer) {
|
|
|
1691
3109
|
getLogger().warn(chalk.yellow(`⚠️ Service initialization failed: ${error.message}`));
|
|
1692
3110
|
getLogger().warn(chalk.gray(" Static contracts will still be listed."));
|
|
1693
3111
|
}
|
|
1694
|
-
return getServiceRegistry();
|
|
3112
|
+
return { services: getServiceRegistry(), initErrors };
|
|
1695
3113
|
}
|
|
1696
3114
|
async function runAction(actionRef, options, projectRoot, initializeServices) {
|
|
1697
3115
|
let viteServer;
|
|
@@ -1859,9 +3277,7 @@ async function runSetup(pluginFilter, options, projectRoot, initializeServices)
|
|
|
1859
3277
|
if (pluginsWithSetup.length === 0) {
|
|
1860
3278
|
if (pluginFilter) {
|
|
1861
3279
|
logger.important(
|
|
1862
|
-
chalk.yellow(
|
|
1863
|
-
`⚠️ Plugin "${pluginFilter}" not found or has no setup handler.`
|
|
1864
|
-
)
|
|
3280
|
+
chalk.yellow(`⚠️ Plugin "${pluginFilter}" not found or has no setup handler.`)
|
|
1865
3281
|
);
|
|
1866
3282
|
} else {
|
|
1867
3283
|
logger.important(chalk.gray("No plugins with setup handlers found."));
|
|
@@ -1873,13 +3289,10 @@ async function runSetup(pluginFilter, options, projectRoot, initializeServices)
|
|
|
1873
3289
|
`Found ${pluginsWithSetup.length} plugin(s) with setup: ${pluginsWithSetup.map((p) => p.name).join(", ")}`
|
|
1874
3290
|
);
|
|
1875
3291
|
}
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
initError = error;
|
|
1881
|
-
if (options.verbose) {
|
|
1882
|
-
logger.info(chalk.yellow(`⚠️ Service init error: ${error.message}`));
|
|
3292
|
+
const { initErrors } = await initializeServices(projectRoot, viteServer);
|
|
3293
|
+
if (initErrors.size > 0 && options.verbose) {
|
|
3294
|
+
for (const [name, err] of initErrors) {
|
|
3295
|
+
logger.info(chalk.yellow(`⚠️ ${name} init error: ${err.message}`));
|
|
1883
3296
|
}
|
|
1884
3297
|
}
|
|
1885
3298
|
let configured = 0;
|
|
@@ -1895,7 +3308,7 @@ async function runSetup(pluginFilter, options, projectRoot, initializeServices)
|
|
|
1895
3308
|
projectRoot,
|
|
1896
3309
|
configDir,
|
|
1897
3310
|
force: options.force ?? false,
|
|
1898
|
-
initError,
|
|
3311
|
+
initError: initErrors.get(plugin.name),
|
|
1899
3312
|
viteServer,
|
|
1900
3313
|
verbose: options.verbose
|
|
1901
3314
|
});
|
|
@@ -1916,7 +3329,9 @@ async function runSetup(pluginFilter, options, projectRoot, initializeServices)
|
|
|
1916
3329
|
needsConfig++;
|
|
1917
3330
|
if (result.configCreated?.length) {
|
|
1918
3331
|
for (const cfg of result.configCreated) {
|
|
1919
|
-
logger.important(
|
|
3332
|
+
logger.important(
|
|
3333
|
+
chalk.yellow(` ⚠️ Config template created: ${cfg}`)
|
|
3334
|
+
);
|
|
1920
3335
|
}
|
|
1921
3336
|
}
|
|
1922
3337
|
if (result.message) {
|
|
@@ -1931,9 +3346,7 @@ async function runSetup(pluginFilter, options, projectRoot, initializeServices)
|
|
|
1931
3346
|
break;
|
|
1932
3347
|
case "error":
|
|
1933
3348
|
errors++;
|
|
1934
|
-
logger.important(
|
|
1935
|
-
chalk.red(` ❌ ${result.message || "Setup failed"}`)
|
|
1936
|
-
);
|
|
3349
|
+
logger.important(chalk.red(` ❌ ${result.message || "Setup failed"}`));
|
|
1937
3350
|
break;
|
|
1938
3351
|
}
|
|
1939
3352
|
} catch (error) {
|
|
@@ -2038,9 +3451,7 @@ async function ensureAgentKitDocs(projectRoot, force) {
|
|
|
2038
3451
|
try {
|
|
2039
3452
|
files = (await fs2.readdir(templateDir)).filter((f) => f.endsWith(".md"));
|
|
2040
3453
|
} catch {
|
|
2041
|
-
getLogger().warn(
|
|
2042
|
-
chalk.yellow(" Agent-kit template folder not found: " + templateDir)
|
|
2043
|
-
);
|
|
3454
|
+
getLogger().warn(chalk.yellow(" Agent-kit template folder not found: " + templateDir));
|
|
2044
3455
|
return;
|
|
2045
3456
|
}
|
|
2046
3457
|
for (const filename of files) {
|
|
@@ -2056,7 +3467,7 @@ async function ensureAgentKitDocs(projectRoot, force) {
|
|
|
2056
3467
|
getLogger().info(chalk.gray(` Created agent-kit/${filename}`));
|
|
2057
3468
|
}
|
|
2058
3469
|
}
|
|
2059
|
-
async function generatePluginReferences(projectRoot, options) {
|
|
3470
|
+
async function generatePluginReferences(projectRoot, options, initErrors, viteServer) {
|
|
2060
3471
|
const { discoverPluginsWithReferences, executePluginReferences } = await import("@jay-framework/stack-server-runtime");
|
|
2061
3472
|
const plugins = await discoverPluginsWithReferences({
|
|
2062
3473
|
projectRoot,
|
|
@@ -2069,10 +3480,20 @@ async function generatePluginReferences(projectRoot, options) {
|
|
|
2069
3480
|
logger.important("");
|
|
2070
3481
|
logger.important(chalk.bold("📚 Generating plugin references..."));
|
|
2071
3482
|
for (const plugin of plugins) {
|
|
3483
|
+
const pluginInitError = initErrors.get(plugin.name);
|
|
3484
|
+
if (pluginInitError) {
|
|
3485
|
+
logger.warn(
|
|
3486
|
+
chalk.yellow(
|
|
3487
|
+
` ⚠️ ${plugin.name}: references skipped — init failed: ${pluginInitError.message}`
|
|
3488
|
+
)
|
|
3489
|
+
);
|
|
3490
|
+
continue;
|
|
3491
|
+
}
|
|
2072
3492
|
try {
|
|
2073
3493
|
const result = await executePluginReferences(plugin, {
|
|
2074
3494
|
projectRoot,
|
|
2075
3495
|
force: options.force ?? false,
|
|
3496
|
+
viteServer,
|
|
2076
3497
|
verbose: options.verbose
|
|
2077
3498
|
});
|
|
2078
3499
|
if (result.referencesCreated.length > 0) {
|
|
@@ -2091,10 +3512,11 @@ async function generatePluginReferences(projectRoot, options) {
|
|
|
2091
3512
|
}
|
|
2092
3513
|
}
|
|
2093
3514
|
}
|
|
2094
|
-
async function runMaterialize(projectRoot, options, defaultOutputRelative) {
|
|
3515
|
+
async function runMaterialize(projectRoot, options, defaultOutputRelative, keepViteAlive = false) {
|
|
2095
3516
|
const path2 = await import("node:path");
|
|
2096
3517
|
const outputDir = options.output ?? path2.join(projectRoot, defaultOutputRelative);
|
|
2097
3518
|
let viteServer;
|
|
3519
|
+
let initErrors = /* @__PURE__ */ new Map();
|
|
2098
3520
|
try {
|
|
2099
3521
|
if (options.list) {
|
|
2100
3522
|
const index = await listContracts({
|
|
@@ -2107,13 +3529,17 @@ async function runMaterialize(projectRoot, options, defaultOutputRelative) {
|
|
|
2107
3529
|
} else {
|
|
2108
3530
|
printContractList(index);
|
|
2109
3531
|
}
|
|
2110
|
-
return;
|
|
3532
|
+
return { initErrors };
|
|
2111
3533
|
}
|
|
2112
3534
|
if (options.verbose) {
|
|
2113
3535
|
getLogger().info("Starting Vite for TypeScript support...");
|
|
2114
3536
|
}
|
|
2115
3537
|
viteServer = await createViteForCli({ projectRoot });
|
|
2116
|
-
const services = await initializeServicesForCli(
|
|
3538
|
+
const { services, initErrors: errors } = await initializeServicesForCli(
|
|
3539
|
+
projectRoot,
|
|
3540
|
+
viteServer
|
|
3541
|
+
);
|
|
3542
|
+
initErrors = errors;
|
|
2117
3543
|
const result = await materializeContracts(
|
|
2118
3544
|
{
|
|
2119
3545
|
projectRoot,
|
|
@@ -2137,6 +3563,7 @@ async function runMaterialize(projectRoot, options, defaultOutputRelative) {
|
|
|
2137
3563
|
getLogger().important(` Dynamic: ${result.dynamicCount}`);
|
|
2138
3564
|
getLogger().important(` Output: ${result.outputDir}`);
|
|
2139
3565
|
}
|
|
3566
|
+
return { initErrors, viteServer: keepViteAlive ? viteServer : void 0 };
|
|
2140
3567
|
} catch (error) {
|
|
2141
3568
|
getLogger().error(chalk.red("❌ Failed to materialize contracts:") + " " + error.message);
|
|
2142
3569
|
if (options.verbose) {
|
|
@@ -2144,10 +3571,11 @@ async function runMaterialize(projectRoot, options, defaultOutputRelative) {
|
|
|
2144
3571
|
}
|
|
2145
3572
|
process.exit(1);
|
|
2146
3573
|
} finally {
|
|
2147
|
-
if (viteServer) {
|
|
3574
|
+
if (viteServer && !keepViteAlive) {
|
|
2148
3575
|
await viteServer.close();
|
|
2149
3576
|
}
|
|
2150
3577
|
}
|
|
3578
|
+
return { initErrors };
|
|
2151
3579
|
}
|
|
2152
3580
|
program.command("setup [plugin]").description(
|
|
2153
3581
|
"Run plugin setup: create config templates, validate credentials, generate reference data"
|
|
@@ -2158,11 +3586,23 @@ program.command("agent-kit").description(
|
|
|
2158
3586
|
"Prepare the agent kit: materialize contracts, generate references, and write docs to agent-kit/"
|
|
2159
3587
|
).option("-o, --output <dir>", "Output directory (default: agent-kit/materialized-contracts)").option("--yaml", "Output contract index as YAML to stdout").option("--list", "List contracts without writing files").option("--plugin <name>", "Filter to specific plugin").option("--dynamic-only", "Only process dynamic contracts").option("--force", "Force re-materialization").option("--no-references", "Skip reference data generation").option("-v, --verbose", "Show detailed output").action(async (options) => {
|
|
2160
3588
|
const projectRoot = process.cwd();
|
|
2161
|
-
await runMaterialize(
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
3589
|
+
const { initErrors, viteServer } = await runMaterialize(
|
|
3590
|
+
projectRoot,
|
|
3591
|
+
options,
|
|
3592
|
+
"agent-kit/materialized-contracts",
|
|
3593
|
+
/* keepViteAlive */
|
|
3594
|
+
true
|
|
3595
|
+
);
|
|
3596
|
+
try {
|
|
3597
|
+
if (!options.list) {
|
|
3598
|
+
await ensureAgentKitDocs(projectRoot, options.force);
|
|
3599
|
+
if (options.references !== false) {
|
|
3600
|
+
await generatePluginReferences(projectRoot, options, initErrors, viteServer);
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
} finally {
|
|
3604
|
+
if (viteServer) {
|
|
3605
|
+
await viteServer.close();
|
|
2166
3606
|
}
|
|
2167
3607
|
}
|
|
2168
3608
|
});
|
|
@@ -2251,6 +3691,9 @@ function printContractList(index) {
|
|
|
2251
3691
|
export {
|
|
2252
3692
|
createEditorHandlers,
|
|
2253
3693
|
getConfigWithDefaults,
|
|
3694
|
+
getRegisteredVendors,
|
|
3695
|
+
getVendor,
|
|
3696
|
+
hasVendor,
|
|
2254
3697
|
listContracts2 as listContracts,
|
|
2255
3698
|
loadConfig,
|
|
2256
3699
|
materializeContracts2 as materializeContracts,
|