@jay-framework/jay-stack-cli 0.12.0 → 0.14.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 +10 -6
- package/agent-kit-template/cli-commands.md +2 -3
- package/agent-kit-template/contracts-and-plugins.md +69 -18
- package/dist/index.d.ts +75 -3
- package/dist/index.js +2215 -556
- 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
|
-
}
|
|
233
|
-
function
|
|
234
|
-
try {
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
`Failed to resolve contract: ${appModule}/${contractFileName} - ${error instanceof Error ? error.message : error}`
|
|
242
|
-
);
|
|
243
|
-
return null;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
async function scanInstalledAppContracts(configBasePath, projectRootPath) {
|
|
247
|
-
const installedAppContracts = {};
|
|
248
|
-
const installedAppsPath = path.join(configBasePath, "installedApps");
|
|
249
|
-
try {
|
|
250
|
-
if (!fs.existsSync(installedAppsPath)) {
|
|
251
|
-
return installedAppContracts;
|
|
252
|
-
}
|
|
253
|
-
const appDirs = await fs.promises.readdir(installedAppsPath, { withFileTypes: true });
|
|
254
|
-
for (const appDir of appDirs) {
|
|
255
|
-
if (appDir.isDirectory()) {
|
|
256
|
-
const appConfigPath = path.join(installedAppsPath, appDir.name, "app.conf.yaml");
|
|
257
|
-
try {
|
|
258
|
-
if (fs.existsSync(appConfigPath)) {
|
|
259
|
-
const configContent = await fs.promises.readFile(appConfigPath, "utf-8");
|
|
260
|
-
const appConfig = YAML.parse(configContent);
|
|
261
|
-
const appName = appConfig.name || appDir.name;
|
|
262
|
-
const appModule = appConfig.module || appDir.name;
|
|
263
|
-
const appContracts = {
|
|
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}`);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
} catch (error) {
|
|
325
|
-
getLogger().warn(`Failed to scan installed apps directory ${installedAppsPath}: ${error}`);
|
|
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;
|
|
1673
|
+
}
|
|
1674
|
+
function loadAndExpandContract(contractFilePath) {
|
|
1675
|
+
try {
|
|
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
|
+
);
|
|
392
1682
|
}
|
|
393
|
-
if (
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
|
398
1691
|
});
|
|
399
1692
|
}
|
|
1693
|
+
} catch (error) {
|
|
1694
|
+
getLogger().warn(`Failed to parse contract file ${contractFilePath}:`, error);
|
|
1695
|
+
}
|
|
1696
|
+
return null;
|
|
1697
|
+
}
|
|
1698
|
+
async function extractHeadlessComponentsFromJayHtml(jayHtmlContent, pageFilePath, projectRootPath) {
|
|
1699
|
+
try {
|
|
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
|
+
);
|
|
1714
|
+
}
|
|
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];
|
|
1726
|
+
}
|
|
1727
|
+
const componentName = headlessImport.contract?.name || "unknown";
|
|
1728
|
+
resolvedComponents.push({
|
|
1729
|
+
appName: pluginName,
|
|
1730
|
+
componentName,
|
|
1731
|
+
key: headlessImport.key
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
return resolvedComponents;
|
|
1736
|
+
} catch (error) {
|
|
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) {
|
|
@@ -993,7 +2402,7 @@ async function startDevServer(options = {}) {
|
|
|
993
2402
|
const resolvedConfig = getConfigWithDefaults(config);
|
|
994
2403
|
const jayOptions = {
|
|
995
2404
|
tsConfigFilePath: "./tsconfig.json",
|
|
996
|
-
outputDir: "build
|
|
2405
|
+
outputDir: "build"
|
|
997
2406
|
};
|
|
998
2407
|
const app = express();
|
|
999
2408
|
const devServerPort = await getPort({ port: resolvedConfig.devServer.portRange });
|
|
@@ -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,11 +2435,12 @@ 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(),
|
|
1026
2443
|
publicBaseUrlPath: "/",
|
|
1027
|
-
dontCacheSlowly: false,
|
|
1028
2444
|
jayRollupConfig: jayOptions,
|
|
1029
2445
|
logLevel: options.logLevel
|
|
1030
2446
|
});
|
|
@@ -1517,13 +2933,206 @@ async function findJayFiles(dir) {
|
|
|
1517
2933
|
async function findContractFiles(dir) {
|
|
1518
2934
|
return await glob(`${dir}/**/*${JAY_CONTRACT_EXTENSION}`);
|
|
1519
2935
|
}
|
|
2936
|
+
function flattenContractTags(tags, prefix) {
|
|
2937
|
+
const result = [];
|
|
2938
|
+
for (const tag of tags) {
|
|
2939
|
+
const tagPath = prefix ? `${prefix}.${tag.tag}` : tag.tag;
|
|
2940
|
+
result.push({ path: tagPath, required: tag.required === true });
|
|
2941
|
+
if (tag.tags) {
|
|
2942
|
+
result.push(...flattenContractTags(tag.tags, tagPath));
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
return result;
|
|
2946
|
+
}
|
|
2947
|
+
function extractExpressions(text) {
|
|
2948
|
+
const results = [];
|
|
2949
|
+
const regex = /\{([^}]+)\}/g;
|
|
2950
|
+
let match;
|
|
2951
|
+
while ((match = regex.exec(text)) !== null) {
|
|
2952
|
+
results.push(match[1].trim());
|
|
2953
|
+
}
|
|
2954
|
+
return results;
|
|
2955
|
+
}
|
|
2956
|
+
function extractTagPath(expr) {
|
|
2957
|
+
let cleaned = expr.replace(/^!/, "").trim();
|
|
2958
|
+
cleaned = cleaned.split(/\s*[!=]==?\s*/)[0].trim();
|
|
2959
|
+
if (cleaned === "." || cleaned === "")
|
|
2960
|
+
return null;
|
|
2961
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*$/.test(cleaned)) {
|
|
2962
|
+
return cleaned;
|
|
2963
|
+
}
|
|
2964
|
+
return null;
|
|
2965
|
+
}
|
|
2966
|
+
const SKIP_ATTRS = /* @__PURE__ */ new Set([
|
|
2967
|
+
"forEach",
|
|
2968
|
+
"if",
|
|
2969
|
+
"ref",
|
|
2970
|
+
"trackBy",
|
|
2971
|
+
"slowForEach",
|
|
2972
|
+
"jayIndex",
|
|
2973
|
+
"jayTrackBy",
|
|
2974
|
+
"when-resolved",
|
|
2975
|
+
"when-loading",
|
|
2976
|
+
"when-rejected",
|
|
2977
|
+
"accessor"
|
|
2978
|
+
]);
|
|
2979
|
+
function collectUsedTags(jayHtml) {
|
|
2980
|
+
const imports = jayHtml.headlessImports;
|
|
2981
|
+
const usedTags = /* @__PURE__ */ new Map();
|
|
2982
|
+
const keyMap = /* @__PURE__ */ new Map();
|
|
2983
|
+
for (let i = 0; i < imports.length; i++) {
|
|
2984
|
+
if (imports[i].contract) {
|
|
2985
|
+
usedTags.set(i, /* @__PURE__ */ new Set());
|
|
2986
|
+
if (imports[i].key) {
|
|
2987
|
+
keyMap.set(imports[i].key, i);
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
function markUsed(importIndex, tagPath) {
|
|
2992
|
+
usedTags.get(importIndex)?.add(tagPath);
|
|
2993
|
+
}
|
|
2994
|
+
function resolvePath(path2, scopes) {
|
|
2995
|
+
const dot = path2.indexOf(".");
|
|
2996
|
+
if (dot !== -1) {
|
|
2997
|
+
const key = path2.substring(0, dot);
|
|
2998
|
+
const idx = keyMap.get(key);
|
|
2999
|
+
if (idx !== void 0) {
|
|
3000
|
+
markUsed(idx, path2.substring(dot + 1));
|
|
3001
|
+
return;
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
if (scopes.length > 0) {
|
|
3005
|
+
const scope = scopes[scopes.length - 1];
|
|
3006
|
+
const full = scope.prefix ? `${scope.prefix}.${path2}` : path2;
|
|
3007
|
+
markUsed(scope.importIndex, full);
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
function walkElement(element, scopes) {
|
|
3011
|
+
const tagName = element.rawTagName?.toLowerCase();
|
|
3012
|
+
let childScopes = scopes;
|
|
3013
|
+
if (tagName?.startsWith("jay:")) {
|
|
3014
|
+
const contractName = tagName.substring(4);
|
|
3015
|
+
const idx = imports.findIndex(
|
|
3016
|
+
(imp) => imp.contractName === contractName && imp.contract
|
|
3017
|
+
);
|
|
3018
|
+
if (idx !== -1) {
|
|
3019
|
+
childScopes = [...scopes, { importIndex: idx, prefix: "" }];
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
const forEachVal = element.getAttribute?.("forEach");
|
|
3023
|
+
if (forEachVal) {
|
|
3024
|
+
const fePath = extractTagPath(forEachVal);
|
|
3025
|
+
if (fePath) {
|
|
3026
|
+
resolvePath(fePath, childScopes);
|
|
3027
|
+
const dot = fePath.indexOf(".");
|
|
3028
|
+
if (dot !== -1) {
|
|
3029
|
+
const key = fePath.substring(0, dot);
|
|
3030
|
+
const idx = keyMap.get(key);
|
|
3031
|
+
if (idx !== void 0) {
|
|
3032
|
+
childScopes = [
|
|
3033
|
+
...childScopes,
|
|
3034
|
+
{ importIndex: idx, prefix: fePath.substring(dot + 1) }
|
|
3035
|
+
];
|
|
3036
|
+
}
|
|
3037
|
+
} else if (childScopes.length > 0) {
|
|
3038
|
+
const scope = childScopes[childScopes.length - 1];
|
|
3039
|
+
const newPrefix = scope.prefix ? `${scope.prefix}.${fePath}` : fePath;
|
|
3040
|
+
childScopes = [
|
|
3041
|
+
...childScopes,
|
|
3042
|
+
{ importIndex: scope.importIndex, prefix: newPrefix }
|
|
3043
|
+
];
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
if (tagName === "with-data") {
|
|
3048
|
+
const accessor = element.getAttribute?.("accessor");
|
|
3049
|
+
if (accessor && accessor !== "." && childScopes.length > 0) {
|
|
3050
|
+
resolvePath(accessor, childScopes);
|
|
3051
|
+
const scope = childScopes[childScopes.length - 1];
|
|
3052
|
+
const newPrefix = scope.prefix ? `${scope.prefix}.${accessor}` : accessor;
|
|
3053
|
+
childScopes = [
|
|
3054
|
+
...childScopes,
|
|
3055
|
+
{ importIndex: scope.importIndex, prefix: newPrefix }
|
|
3056
|
+
];
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
const ifVal = element.getAttribute?.("if");
|
|
3060
|
+
if (ifVal) {
|
|
3061
|
+
const ifPath = extractTagPath(ifVal);
|
|
3062
|
+
if (ifPath)
|
|
3063
|
+
resolvePath(ifPath, scopes);
|
|
3064
|
+
}
|
|
3065
|
+
const refVal = element.getAttribute?.("ref");
|
|
3066
|
+
if (refVal) {
|
|
3067
|
+
resolvePath(refVal, scopes);
|
|
3068
|
+
}
|
|
3069
|
+
const attrs = element.attributes ?? {};
|
|
3070
|
+
for (const [name, value] of Object.entries(attrs)) {
|
|
3071
|
+
if (SKIP_ATTRS.has(name))
|
|
3072
|
+
continue;
|
|
3073
|
+
for (const expr of extractExpressions(value)) {
|
|
3074
|
+
const p = extractTagPath(expr);
|
|
3075
|
+
if (p)
|
|
3076
|
+
resolvePath(p, scopes);
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
for (const child of element.childNodes ?? []) {
|
|
3080
|
+
if (child.nodeType === 3) {
|
|
3081
|
+
const text = child.rawText ?? child.text ?? "";
|
|
3082
|
+
for (const expr of extractExpressions(text)) {
|
|
3083
|
+
const p = extractTagPath(expr);
|
|
3084
|
+
if (p)
|
|
3085
|
+
resolvePath(p, childScopes);
|
|
3086
|
+
}
|
|
3087
|
+
} else if (child.nodeType === 1) {
|
|
3088
|
+
walkElement(child, childScopes);
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
walkElement(jayHtml.body, []);
|
|
3093
|
+
return usedTags;
|
|
3094
|
+
}
|
|
3095
|
+
function analyzeTagCoverage(jayHtml, file) {
|
|
3096
|
+
const imports = jayHtml.headlessImports;
|
|
3097
|
+
const withContracts = imports.filter((imp) => imp.contract);
|
|
3098
|
+
if (withContracts.length === 0)
|
|
3099
|
+
return null;
|
|
3100
|
+
const usedTagsMap = collectUsedTags(jayHtml);
|
|
3101
|
+
const contracts = [];
|
|
3102
|
+
for (let i = 0; i < imports.length; i++) {
|
|
3103
|
+
const imp = imports[i];
|
|
3104
|
+
if (!imp.contract)
|
|
3105
|
+
continue;
|
|
3106
|
+
const allTags = flattenContractTags(imp.contract.tags);
|
|
3107
|
+
const usedSet = usedTagsMap.get(i) ?? /* @__PURE__ */ new Set();
|
|
3108
|
+
const expanded = new Set(usedSet);
|
|
3109
|
+
for (const usedPath of usedSet) {
|
|
3110
|
+
const segments = usedPath.split(".");
|
|
3111
|
+
for (let j = 1; j < segments.length; j++) {
|
|
3112
|
+
expanded.add(segments.slice(0, j).join("."));
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
const unused = allTags.filter((t) => !expanded.has(t.path));
|
|
3116
|
+
const requiredUnused = unused.filter((t) => t.required);
|
|
3117
|
+
contracts.push({
|
|
3118
|
+
key: imp.key,
|
|
3119
|
+
contractName: imp.contractName,
|
|
3120
|
+
totalTags: allTags.length,
|
|
3121
|
+
usedTags: allTags.length - unused.length,
|
|
3122
|
+
unusedTags: unused.map((t) => t.path),
|
|
3123
|
+
requiredUnusedTags: requiredUnused.map((t) => t.path)
|
|
3124
|
+
});
|
|
3125
|
+
}
|
|
3126
|
+
return { file, contracts };
|
|
3127
|
+
}
|
|
1520
3128
|
async function validateJayFiles(options = {}) {
|
|
1521
3129
|
const config = loadConfig();
|
|
1522
3130
|
const resolvedConfig = getConfigWithDefaults(config);
|
|
1523
|
-
const projectRoot = process.cwd();
|
|
3131
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
1524
3132
|
const scanDir = options.path ? path.resolve(options.path) : path.resolve(resolvedConfig.devServer.pagesBase);
|
|
1525
3133
|
const errors = [];
|
|
1526
3134
|
const warnings = [];
|
|
3135
|
+
const coverage = [];
|
|
1527
3136
|
const jayHtmlFiles = await findJayFiles(scanDir);
|
|
1528
3137
|
const contractFiles = await findContractFiles(scanDir);
|
|
1529
3138
|
if (options.verbose) {
|
|
@@ -1589,6 +3198,10 @@ async function validateJayFiles(options = {}) {
|
|
|
1589
3198
|
}
|
|
1590
3199
|
continue;
|
|
1591
3200
|
}
|
|
3201
|
+
const fileCoverage = analyzeTagCoverage(parsedFile.val, relativePath);
|
|
3202
|
+
if (fileCoverage) {
|
|
3203
|
+
coverage.push(fileCoverage);
|
|
3204
|
+
}
|
|
1592
3205
|
const generatedFile = generateElementFile(
|
|
1593
3206
|
parsedFile.val,
|
|
1594
3207
|
RuntimeMode.MainTrusted,
|
|
@@ -1624,7 +3237,8 @@ async function validateJayFiles(options = {}) {
|
|
|
1624
3237
|
jayHtmlFilesScanned: jayHtmlFiles.length,
|
|
1625
3238
|
contractFilesScanned: contractFiles.length,
|
|
1626
3239
|
errors,
|
|
1627
|
-
warnings
|
|
3240
|
+
warnings,
|
|
3241
|
+
coverage
|
|
1628
3242
|
};
|
|
1629
3243
|
}
|
|
1630
3244
|
function printJayValidationResult(result, options) {
|
|
@@ -1653,6 +3267,29 @@ function printJayValidationResult(result, options) {
|
|
|
1653
3267
|
chalk.red(`${result.errors.length} error(s) found, ${validFiles} file(s) valid.`)
|
|
1654
3268
|
);
|
|
1655
3269
|
}
|
|
3270
|
+
if (result.coverage.length > 0) {
|
|
3271
|
+
logger.important("");
|
|
3272
|
+
logger.important("Tag Coverage:");
|
|
3273
|
+
for (const fileCov of result.coverage) {
|
|
3274
|
+
logger.important(` ${fileCov.file}`);
|
|
3275
|
+
for (const contract of fileCov.contracts) {
|
|
3276
|
+
const label = contract.key ? `${contract.key} (${contract.contractName})` : contract.contractName;
|
|
3277
|
+
logger.important(
|
|
3278
|
+
` ${label}: ${contract.usedTags}/${contract.totalTags} tags used`
|
|
3279
|
+
);
|
|
3280
|
+
if (contract.unusedTags.length > 0) {
|
|
3281
|
+
logger.important(chalk.gray(` Unused: ${contract.unusedTags.join(", ")}`));
|
|
3282
|
+
}
|
|
3283
|
+
if (contract.requiredUnusedTags.length > 0) {
|
|
3284
|
+
logger.important(
|
|
3285
|
+
chalk.yellow(
|
|
3286
|
+
` ⚠ Required unused: ${contract.requiredUnusedTags.join(", ")}`
|
|
3287
|
+
)
|
|
3288
|
+
);
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
1656
3293
|
}
|
|
1657
3294
|
async function initializeServicesForCli(projectRoot, viteServer) {
|
|
1658
3295
|
const path2 = await import("node:path");
|
|
@@ -1664,6 +3301,7 @@ async function initializeServicesForCli(projectRoot, viteServer) {
|
|
|
1664
3301
|
sortPluginsByDependencies,
|
|
1665
3302
|
executePluginServerInits
|
|
1666
3303
|
} = await import("@jay-framework/stack-server-runtime");
|
|
3304
|
+
let initErrors = /* @__PURE__ */ new Map();
|
|
1667
3305
|
try {
|
|
1668
3306
|
const discoveredPlugins = await discoverPluginsWithInit({
|
|
1669
3307
|
projectRoot,
|
|
@@ -1671,7 +3309,7 @@ async function initializeServicesForCli(projectRoot, viteServer) {
|
|
|
1671
3309
|
});
|
|
1672
3310
|
const pluginsWithInit = sortPluginsByDependencies(discoveredPlugins);
|
|
1673
3311
|
try {
|
|
1674
|
-
await executePluginServerInits(pluginsWithInit, viteServer, false);
|
|
3312
|
+
initErrors = await executePluginServerInits(pluginsWithInit, viteServer, false);
|
|
1675
3313
|
} catch (error) {
|
|
1676
3314
|
getLogger().warn(chalk.yellow(`⚠️ Plugin initialization skipped: ${error.message}`));
|
|
1677
3315
|
}
|
|
@@ -1691,7 +3329,7 @@ async function initializeServicesForCli(projectRoot, viteServer) {
|
|
|
1691
3329
|
getLogger().warn(chalk.yellow(`⚠️ Service initialization failed: ${error.message}`));
|
|
1692
3330
|
getLogger().warn(chalk.gray(" Static contracts will still be listed."));
|
|
1693
3331
|
}
|
|
1694
|
-
return getServiceRegistry();
|
|
3332
|
+
return { services: getServiceRegistry(), initErrors };
|
|
1695
3333
|
}
|
|
1696
3334
|
async function runAction(actionRef, options, projectRoot, initializeServices) {
|
|
1697
3335
|
let viteServer;
|
|
@@ -1859,9 +3497,7 @@ async function runSetup(pluginFilter, options, projectRoot, initializeServices)
|
|
|
1859
3497
|
if (pluginsWithSetup.length === 0) {
|
|
1860
3498
|
if (pluginFilter) {
|
|
1861
3499
|
logger.important(
|
|
1862
|
-
chalk.yellow(
|
|
1863
|
-
`⚠️ Plugin "${pluginFilter}" not found or has no setup handler.`
|
|
1864
|
-
)
|
|
3500
|
+
chalk.yellow(`⚠️ Plugin "${pluginFilter}" not found or has no setup handler.`)
|
|
1865
3501
|
);
|
|
1866
3502
|
} else {
|
|
1867
3503
|
logger.important(chalk.gray("No plugins with setup handlers found."));
|
|
@@ -1873,13 +3509,10 @@ async function runSetup(pluginFilter, options, projectRoot, initializeServices)
|
|
|
1873
3509
|
`Found ${pluginsWithSetup.length} plugin(s) with setup: ${pluginsWithSetup.map((p) => p.name).join(", ")}`
|
|
1874
3510
|
);
|
|
1875
3511
|
}
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
initError = error;
|
|
1881
|
-
if (options.verbose) {
|
|
1882
|
-
logger.info(chalk.yellow(`⚠️ Service init error: ${error.message}`));
|
|
3512
|
+
const { initErrors } = await initializeServices(projectRoot, viteServer);
|
|
3513
|
+
if (initErrors.size > 0 && options.verbose) {
|
|
3514
|
+
for (const [name, err] of initErrors) {
|
|
3515
|
+
logger.info(chalk.yellow(`⚠️ ${name} init error: ${err.message}`));
|
|
1883
3516
|
}
|
|
1884
3517
|
}
|
|
1885
3518
|
let configured = 0;
|
|
@@ -1895,7 +3528,7 @@ async function runSetup(pluginFilter, options, projectRoot, initializeServices)
|
|
|
1895
3528
|
projectRoot,
|
|
1896
3529
|
configDir,
|
|
1897
3530
|
force: options.force ?? false,
|
|
1898
|
-
initError,
|
|
3531
|
+
initError: initErrors.get(plugin.name),
|
|
1899
3532
|
viteServer,
|
|
1900
3533
|
verbose: options.verbose
|
|
1901
3534
|
});
|
|
@@ -1916,7 +3549,9 @@ async function runSetup(pluginFilter, options, projectRoot, initializeServices)
|
|
|
1916
3549
|
needsConfig++;
|
|
1917
3550
|
if (result.configCreated?.length) {
|
|
1918
3551
|
for (const cfg of result.configCreated) {
|
|
1919
|
-
logger.important(
|
|
3552
|
+
logger.important(
|
|
3553
|
+
chalk.yellow(` ⚠️ Config template created: ${cfg}`)
|
|
3554
|
+
);
|
|
1920
3555
|
}
|
|
1921
3556
|
}
|
|
1922
3557
|
if (result.message) {
|
|
@@ -1931,9 +3566,7 @@ async function runSetup(pluginFilter, options, projectRoot, initializeServices)
|
|
|
1931
3566
|
break;
|
|
1932
3567
|
case "error":
|
|
1933
3568
|
errors++;
|
|
1934
|
-
logger.important(
|
|
1935
|
-
chalk.red(` ❌ ${result.message || "Setup failed"}`)
|
|
1936
|
-
);
|
|
3569
|
+
logger.important(chalk.red(` ❌ ${result.message || "Setup failed"}`));
|
|
1937
3570
|
break;
|
|
1938
3571
|
}
|
|
1939
3572
|
} catch (error) {
|
|
@@ -2038,9 +3671,7 @@ async function ensureAgentKitDocs(projectRoot, force) {
|
|
|
2038
3671
|
try {
|
|
2039
3672
|
files = (await fs2.readdir(templateDir)).filter((f) => f.endsWith(".md"));
|
|
2040
3673
|
} catch {
|
|
2041
|
-
getLogger().warn(
|
|
2042
|
-
chalk.yellow(" Agent-kit template folder not found: " + templateDir)
|
|
2043
|
-
);
|
|
3674
|
+
getLogger().warn(chalk.yellow(" Agent-kit template folder not found: " + templateDir));
|
|
2044
3675
|
return;
|
|
2045
3676
|
}
|
|
2046
3677
|
for (const filename of files) {
|
|
@@ -2056,7 +3687,7 @@ async function ensureAgentKitDocs(projectRoot, force) {
|
|
|
2056
3687
|
getLogger().info(chalk.gray(` Created agent-kit/${filename}`));
|
|
2057
3688
|
}
|
|
2058
3689
|
}
|
|
2059
|
-
async function generatePluginReferences(projectRoot, options) {
|
|
3690
|
+
async function generatePluginReferences(projectRoot, options, initErrors, viteServer) {
|
|
2060
3691
|
const { discoverPluginsWithReferences, executePluginReferences } = await import("@jay-framework/stack-server-runtime");
|
|
2061
3692
|
const plugins = await discoverPluginsWithReferences({
|
|
2062
3693
|
projectRoot,
|
|
@@ -2069,10 +3700,20 @@ async function generatePluginReferences(projectRoot, options) {
|
|
|
2069
3700
|
logger.important("");
|
|
2070
3701
|
logger.important(chalk.bold("📚 Generating plugin references..."));
|
|
2071
3702
|
for (const plugin of plugins) {
|
|
3703
|
+
const pluginInitError = initErrors.get(plugin.name);
|
|
3704
|
+
if (pluginInitError) {
|
|
3705
|
+
logger.warn(
|
|
3706
|
+
chalk.yellow(
|
|
3707
|
+
` ⚠️ ${plugin.name}: references skipped — init failed: ${pluginInitError.message}`
|
|
3708
|
+
)
|
|
3709
|
+
);
|
|
3710
|
+
continue;
|
|
3711
|
+
}
|
|
2072
3712
|
try {
|
|
2073
3713
|
const result = await executePluginReferences(plugin, {
|
|
2074
3714
|
projectRoot,
|
|
2075
3715
|
force: options.force ?? false,
|
|
3716
|
+
viteServer,
|
|
2076
3717
|
verbose: options.verbose
|
|
2077
3718
|
});
|
|
2078
3719
|
if (result.referencesCreated.length > 0) {
|
|
@@ -2091,10 +3732,11 @@ async function generatePluginReferences(projectRoot, options) {
|
|
|
2091
3732
|
}
|
|
2092
3733
|
}
|
|
2093
3734
|
}
|
|
2094
|
-
async function runMaterialize(projectRoot, options, defaultOutputRelative) {
|
|
3735
|
+
async function runMaterialize(projectRoot, options, defaultOutputRelative, keepViteAlive = false) {
|
|
2095
3736
|
const path2 = await import("node:path");
|
|
2096
3737
|
const outputDir = options.output ?? path2.join(projectRoot, defaultOutputRelative);
|
|
2097
3738
|
let viteServer;
|
|
3739
|
+
let initErrors = /* @__PURE__ */ new Map();
|
|
2098
3740
|
try {
|
|
2099
3741
|
if (options.list) {
|
|
2100
3742
|
const index = await listContracts({
|
|
@@ -2107,13 +3749,17 @@ async function runMaterialize(projectRoot, options, defaultOutputRelative) {
|
|
|
2107
3749
|
} else {
|
|
2108
3750
|
printContractList(index);
|
|
2109
3751
|
}
|
|
2110
|
-
return;
|
|
3752
|
+
return { initErrors };
|
|
2111
3753
|
}
|
|
2112
3754
|
if (options.verbose) {
|
|
2113
3755
|
getLogger().info("Starting Vite for TypeScript support...");
|
|
2114
3756
|
}
|
|
2115
3757
|
viteServer = await createViteForCli({ projectRoot });
|
|
2116
|
-
const services = await initializeServicesForCli(
|
|
3758
|
+
const { services, initErrors: errors } = await initializeServicesForCli(
|
|
3759
|
+
projectRoot,
|
|
3760
|
+
viteServer
|
|
3761
|
+
);
|
|
3762
|
+
initErrors = errors;
|
|
2117
3763
|
const result = await materializeContracts(
|
|
2118
3764
|
{
|
|
2119
3765
|
projectRoot,
|
|
@@ -2127,16 +3773,19 @@ async function runMaterialize(projectRoot, options, defaultOutputRelative) {
|
|
|
2127
3773
|
services
|
|
2128
3774
|
);
|
|
2129
3775
|
if (options.yaml) {
|
|
2130
|
-
getLogger().important(YAML.stringify(result.
|
|
3776
|
+
getLogger().important(YAML.stringify(result.pluginsIndex));
|
|
2131
3777
|
} else {
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
3778
|
+
const totalContracts = result.pluginsIndex.plugins.reduce(
|
|
3779
|
+
(sum, p) => sum + p.contracts.length,
|
|
3780
|
+
0
|
|
2135
3781
|
);
|
|
3782
|
+
getLogger().important(chalk.green(`
|
|
3783
|
+
✅ Materialized ${totalContracts} contracts`));
|
|
2136
3784
|
getLogger().important(` Static: ${result.staticCount}`);
|
|
2137
3785
|
getLogger().important(` Dynamic: ${result.dynamicCount}`);
|
|
2138
3786
|
getLogger().important(` Output: ${result.outputDir}`);
|
|
2139
3787
|
}
|
|
3788
|
+
return { initErrors, viteServer: keepViteAlive ? viteServer : void 0 };
|
|
2140
3789
|
} catch (error) {
|
|
2141
3790
|
getLogger().error(chalk.red("❌ Failed to materialize contracts:") + " " + error.message);
|
|
2142
3791
|
if (options.verbose) {
|
|
@@ -2144,10 +3793,11 @@ async function runMaterialize(projectRoot, options, defaultOutputRelative) {
|
|
|
2144
3793
|
}
|
|
2145
3794
|
process.exit(1);
|
|
2146
3795
|
} finally {
|
|
2147
|
-
if (viteServer) {
|
|
3796
|
+
if (viteServer && !keepViteAlive) {
|
|
2148
3797
|
await viteServer.close();
|
|
2149
3798
|
}
|
|
2150
3799
|
}
|
|
3800
|
+
return { initErrors };
|
|
2151
3801
|
}
|
|
2152
3802
|
program.command("setup [plugin]").description(
|
|
2153
3803
|
"Run plugin setup: create config templates, validate credentials, generate reference data"
|
|
@@ -2158,11 +3808,23 @@ program.command("agent-kit").description(
|
|
|
2158
3808
|
"Prepare the agent kit: materialize contracts, generate references, and write docs to agent-kit/"
|
|
2159
3809
|
).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
3810
|
const projectRoot = process.cwd();
|
|
2161
|
-
await runMaterialize(
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
3811
|
+
const { initErrors, viteServer } = await runMaterialize(
|
|
3812
|
+
projectRoot,
|
|
3813
|
+
options,
|
|
3814
|
+
"agent-kit/materialized-contracts",
|
|
3815
|
+
/* keepViteAlive */
|
|
3816
|
+
true
|
|
3817
|
+
);
|
|
3818
|
+
try {
|
|
3819
|
+
if (!options.list) {
|
|
3820
|
+
await ensureAgentKitDocs(projectRoot, options.force);
|
|
3821
|
+
if (options.references !== false) {
|
|
3822
|
+
await generatePluginReferences(projectRoot, options, initErrors, viteServer);
|
|
3823
|
+
}
|
|
3824
|
+
}
|
|
3825
|
+
} finally {
|
|
3826
|
+
if (viteServer) {
|
|
3827
|
+
await viteServer.close();
|
|
2166
3828
|
}
|
|
2167
3829
|
}
|
|
2168
3830
|
});
|
|
@@ -2230,27 +3892,24 @@ function printValidationResult(result, verbose) {
|
|
|
2230
3892
|
function printContractList(index) {
|
|
2231
3893
|
const logger = getLogger();
|
|
2232
3894
|
logger.important("\nAvailable Contracts:\n");
|
|
2233
|
-
const
|
|
2234
|
-
|
|
2235
|
-
const
|
|
2236
|
-
existing.push(contract);
|
|
2237
|
-
byPlugin.set(contract.plugin, existing);
|
|
2238
|
-
}
|
|
2239
|
-
for (const [plugin, contracts] of byPlugin) {
|
|
2240
|
-
logger.important(chalk.bold(`📦 ${plugin}`));
|
|
2241
|
-
for (const contract of contracts) {
|
|
3895
|
+
for (const plugin of index.plugins) {
|
|
3896
|
+
logger.important(chalk.bold(`📦 ${plugin.name}`));
|
|
3897
|
+
for (const contract of plugin.contracts) {
|
|
2242
3898
|
const typeIcon = contract.type === "static" ? "📄" : "⚡";
|
|
2243
3899
|
logger.important(` ${typeIcon} ${contract.name}`);
|
|
2244
3900
|
}
|
|
2245
3901
|
logger.important("");
|
|
2246
3902
|
}
|
|
2247
|
-
if (index.
|
|
3903
|
+
if (index.plugins.length === 0) {
|
|
2248
3904
|
logger.important(chalk.gray("No contracts found."));
|
|
2249
3905
|
}
|
|
2250
3906
|
}
|
|
2251
3907
|
export {
|
|
2252
3908
|
createEditorHandlers,
|
|
2253
3909
|
getConfigWithDefaults,
|
|
3910
|
+
getRegisteredVendors,
|
|
3911
|
+
getVendor,
|
|
3912
|
+
hasVendor,
|
|
2254
3913
|
listContracts2 as listContracts,
|
|
2255
3914
|
loadConfig,
|
|
2256
3915
|
materializeContracts2 as materializeContracts,
|