@jay-framework/jay-stack-cli 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 { parse } from "node-html-parser";
11
- import { createRequire } from "module";
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
+ "&": "&amp;",
500
+ "<": "&lt;",
501
+ ">": "&gt;",
502
+ '"': "&quot;",
503
+ "'": "&#39;"
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
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 protocolTag = {
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: tag.type.length === 1 ? ContractTagType[tag.type[0]] : tag.type.map((t) => ContractTagType[t])
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(entries) {
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 entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
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}: ${error}`);
1622
+ getLogger().warn(`Failed to scan directory ${dirPath}:`, error);
165
1623
  }
166
1624
  }
167
1625
  await scanDirectory(pagesBasePath);
168
1626
  }
169
- async function parseContractFile(contractFilePath) {
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 linkedPath = path.resolve(baseDir, tag.link);
199
- const linkedContract = await parseContractFile(linkedPath);
200
- if (linkedContract) {
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.length === 1 ? ContractTagType[tag.type[0]] : tag.type.map((t) => ContractTagType[t]),
204
- tags: linkedContract.tags
205
- // Use tags from linked contract
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(convertContractTagToProtocol(tag));
1655
+ resolvedTags.push(tag);
217
1656
  }
218
1657
  } catch (error) {
219
- getLogger().warn(`Error resolving linked contract ${tag.link}: ${error}`);
220
- resolvedTags.push(convertContractTagToProtocol(tag));
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 = await resolveLinkedTags(tag.tags, baseDir);
224
- const protocolTag = convertContractTagToProtocol(tag);
225
- protocolTag.tags = resolvedSubTags;
226
- resolvedTags.push(protocolTag);
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(convertContractTagToProtocol(tag));
1669
+ resolvedTags.push(tag);
229
1670
  }
230
1671
  }
231
1672
  return resolvedTags;
232
1673
  }
233
- function resolveAppContractPath(appModule, contractFileName, projectRootPath) {
1674
+ function loadAndExpandContract(contractFilePath) {
234
1675
  try {
235
- const require2 = createRequire(path.join(projectRootPath, "package.json"));
236
- const modulePath = `${appModule}/${contractFileName}`;
237
- const resolvedPath = require2.resolve(modulePath);
238
- return resolvedPath;
1676
+ const loadResult = JAY_IMPORT_RESOLVER.loadContract(contractFilePath);
1677
+ if (loadResult.validations.length > 0) {
1678
+ getLogger().warn(
1679
+ `Contract validation errors in ${contractFilePath}:`,
1680
+ loadResult.validations
1681
+ );
1682
+ }
1683
+ if (loadResult.val) {
1684
+ const resolvedTags = expandContractTags(
1685
+ loadResult.val.tags,
1686
+ path.dirname(contractFilePath)
1687
+ );
1688
+ return convertContractToProtocol({
1689
+ name: loadResult.val.name,
1690
+ tags: resolvedTags
1691
+ });
1692
+ }
239
1693
  } catch (error) {
240
- getLogger().warn(
241
- `Failed to resolve contract: ${appModule}/${contractFileName} - ${error instanceof Error ? error.message : error}`
242
- );
243
- return null;
1694
+ getLogger().warn(`Failed to parse contract file ${contractFilePath}:`, error);
244
1695
  }
1696
+ return null;
245
1697
  }
246
- async function scanInstalledAppContracts(configBasePath, projectRootPath) {
247
- const installedAppContracts = {};
248
- const installedAppsPath = path.join(configBasePath, "installedApps");
1698
+ async function extractHeadlessComponentsFromJayHtml(jayHtmlContent, pageFilePath, projectRootPath) {
249
1699
  try {
250
- if (!fs.existsSync(installedAppsPath)) {
251
- return installedAppContracts;
1700
+ const parsedJayHtml = await parseJayFile(
1701
+ jayHtmlContent,
1702
+ path.basename(pageFilePath),
1703
+ path.dirname(pageFilePath),
1704
+ { relativePath: "" },
1705
+ // We don't need TypeScript config for headless extraction
1706
+ JAY_IMPORT_RESOLVER,
1707
+ projectRootPath
1708
+ );
1709
+ if (parsedJayHtml.validations.length > 0) {
1710
+ getLogger().warn(
1711
+ `Jay-HTML parsing warnings for ${pageFilePath}:`,
1712
+ parsedJayHtml.validations
1713
+ );
252
1714
  }
253
- 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}`);
1715
+ if (!parsedJayHtml.val) {
1716
+ getLogger().warn(`Failed to parse jay-html file: ${pageFilePath}`);
1717
+ return [];
1718
+ }
1719
+ const resolvedComponents = [];
1720
+ for (const headlessImport of parsedJayHtml.val.headlessImports) {
1721
+ if (headlessImport.codeLink) {
1722
+ let pluginName = headlessImport.codeLink.module;
1723
+ const nodeModulesMatch = pluginName.match(/node_modules\/([^/]+)/);
1724
+ if (nodeModulesMatch) {
1725
+ pluginName = nodeModulesMatch[1];
321
1726
  }
1727
+ const componentName = headlessImport.contract?.name || "unknown";
1728
+ resolvedComponents.push({
1729
+ appName: pluginName,
1730
+ componentName,
1731
+ key: headlessImport.key
1732
+ });
322
1733
  }
323
1734
  }
1735
+ return resolvedComponents;
324
1736
  } catch (error) {
325
- getLogger().warn(`Failed to 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;
392
- }
393
- if (!resolved) {
394
- resolvedComponents.push({
395
- appName: src,
396
- componentName: name,
397
- key
398
- });
399
- }
1737
+ getLogger().warn(`Failed to parse jay-html content for ${pageFilePath}:`, error);
1738
+ return [];
400
1739
  }
401
- return resolvedComponents;
402
1740
  }
403
1741
  async function scanProjectComponents(componentsBasePath) {
404
1742
  const components = [];
@@ -421,43 +1759,10 @@ async function scanProjectComponents(componentsBasePath) {
421
1759
  }
422
1760
  }
423
1761
  } catch (error) {
424
- getLogger().warn(`Failed to scan components directory ${componentsBasePath}: ${error}`);
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}: ${error}`);
1775
+ getLogger().warn(`Failed to read project config ${projectConfigPath}:`, error);
471
1776
  }
472
1777
  return "Unnamed Project";
473
1778
  }
474
- async function scanPlugins(projectRootPath) {
1779
+ async function scanLocalPluginNames(projectRoot) {
475
1780
  const plugins = [];
476
- const localPluginsPath = path.join(projectRootPath, "src/plugins");
477
- if (fs.existsSync(localPluginsPath)) {
478
- try {
479
- const pluginDirs = await fs.promises.readdir(localPluginsPath, { withFileTypes: true });
480
- for (const dir of pluginDirs) {
481
- if (!dir.isDirectory())
482
- continue;
483
- const pluginPath = path.join(localPluginsPath, dir.name);
484
- const pluginYamlPath = path.join(pluginPath, "plugin.yaml");
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
- try {
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
- const nodeModulesPath = path.join(projectRootPath, "node_modules");
508
- if (fs.existsSync(nodeModulesPath)) {
509
- try {
510
- const topLevelDirs = await fs.promises.readdir(nodeModulesPath, {
511
- withFileTypes: true
512
- });
513
- for (const entry of topLevelDirs) {
514
- if (!entry.isDirectory())
515
- continue;
516
- const packageDirs = [];
517
- if (entry.name.startsWith("@")) {
518
- const scopePath = path.join(nodeModulesPath, entry.name);
519
- const scopedPackages = await fs.promises.readdir(scopePath, {
520
- withFileTypes: true
521
- });
522
- for (const scopedPkg of scopedPackages) {
523
- if (scopedPkg.isDirectory()) {
524
- packageDirs.push(path.join(scopePath, scopedPkg.name));
525
- }
526
- }
527
- } else {
528
- packageDirs.push(path.join(nodeModulesPath, entry.name));
529
- }
530
- for (const pkgPath of packageDirs) {
531
- const pluginYamlPath = path.join(pkgPath, "plugin.yaml");
532
- if (fs.existsSync(pluginYamlPath)) {
533
- try {
534
- const yamlContent = await fs.promises.readFile(pluginYamlPath, "utf-8");
535
- const manifest = YAML.parse(yamlContent);
536
- const packageJsonPath = path.join(pkgPath, "package.json");
537
- let moduleName = manifest.module;
538
- if (fs.existsSync(packageJsonPath)) {
539
- const packageJson = JSON.parse(
540
- await fs.promises.readFile(packageJsonPath, "utf-8")
541
- );
542
- moduleName = packageJson.name;
543
- }
544
- plugins.push({
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 plugins;
1844
+ return pluginNames;
567
1845
  }
568
- async function scanProjectInfo(pagesBasePath, componentsBasePath, configBasePath, projectRootPath) {
569
- const [projectName, components, installedApps, plugins] = await Promise.all([
570
- getProjectName(configBasePath),
571
- scanProjectComponents(componentsBasePath),
572
- scanInstalledApps(configBasePath),
573
- scanPlugins(projectRootPath)
574
- ]);
575
- const installedAppContracts = await scanInstalledAppContracts(configBasePath, projectRootPath);
576
- const pages = [];
577
- await scanPageDirectories(pagesBasePath, async (context) => {
578
- const { dirPath, pageUrl, pageName, hasPageHtml, hasPageContract, hasPageConfig } = context;
579
- const pageFilePath = path.join(dirPath, PAGE_FILENAME);
580
- const pageConfigPath = path.join(dirPath, PAGE_CONFIG_FILENAME);
581
- const contractPath = path.join(dirPath, PAGE_CONTRACT_FILENAME);
582
- let usedComponents = [];
583
- let contractSchema;
584
- if (hasPageContract) {
585
- try {
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
- if (hasPageHtml) {
595
- try {
596
- const jayHtmlContent = await fs.promises.readFile(pageFilePath, "utf-8");
597
- usedComponents = extractHeadlessComponents(
598
- jayHtmlContent,
599
- installedApps,
600
- installedAppContracts
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
- } catch (error) {
603
- getLogger().warn(`Failed to read page file ${pageFilePath}: ${error}`);
1887
+ continue;
604
1888
  }
605
- } else if (hasPageConfig) {
606
- try {
607
- const configContent = await fs.promises.readFile(pageConfigPath, "utf-8");
608
- const pageConfig = YAML.parse(configContent);
609
- if (pageConfig.used_components && Array.isArray(pageConfig.used_components)) {
610
- for (const comp of pageConfig.used_components) {
611
- const key = comp.key || "";
612
- let src = "";
613
- let name = "";
614
- if (comp.plugin && comp.contract) {
615
- const plugin = plugins.find((p) => p.manifest.name === comp.plugin);
616
- if (plugin && plugin.manifest.contracts) {
617
- const contract = plugin.manifest.contracts.find(
618
- (c) => c.name === comp.contract
619
- );
620
- if (contract) {
621
- usedComponents.push({
622
- appName: comp.plugin,
623
- componentName: comp.contract,
624
- key
625
- });
626
- continue;
627
- }
628
- }
629
- usedComponents.push({
630
- appName: comp.plugin,
631
- componentName: comp.contract,
632
- key
633
- });
634
- continue;
635
- }
636
- src = comp.src || "";
637
- name = comp.name || "";
638
- let resolved = false;
639
- for (const app of installedApps) {
640
- if (app.module !== src && app.name !== src) {
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
- pages.push({
708
- name: pageName,
709
- url: pageUrl,
710
- filePath: pageFilePath,
711
- contractSchema,
712
- usedComponents
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 routePath = page.route === "/" ? "" : page.route;
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}: ${error}`);
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}: ${error}`);
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().warn(
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(`Failed to save image: ${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(`Failed to check image: ${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
- projectRootPath
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(` Installed Apps: ${info.installedApps.length}`);
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(`Failed to get project info: ${error}`);
2228
+ getLogger().error("Failed to get project info:", error);
920
2229
  return {
921
2230
  type: "getProjectInfo",
922
2231
  success: false,
@@ -926,18 +2235,118 @@ function createEditorHandlers(config, tsConfigPath, projectRoot) {
926
2235
  localPath: process.cwd(),
927
2236
  pages: [],
928
2237
  components: [],
929
- installedApps: [],
930
- installedAppContracts: {},
931
2238
  plugins: []
932
2239
  }
933
2240
  };
934
2241
  }
935
2242
  };
2243
+ const onExport = async (params) => {
2244
+ try {
2245
+ const pagesBasePath = path.resolve(config.devServer.pagesBase);
2246
+ const { vendorId, pageUrl, vendorDoc } = params;
2247
+ const dirname = pageUrlToDirectoryPath(pageUrl, pagesBasePath);
2248
+ const vendorFilename = `page.${vendorId}.json`;
2249
+ const vendorFilePath = path.join(dirname, vendorFilename);
2250
+ await fs.promises.mkdir(dirname, { recursive: true });
2251
+ await fs.promises.writeFile(
2252
+ vendorFilePath,
2253
+ JSON.stringify(vendorDoc, null, 2),
2254
+ "utf-8"
2255
+ );
2256
+ getLogger().info(`📦 Exported ${vendorId} document to: ${vendorFilePath}`);
2257
+ if (hasVendor(vendorId)) {
2258
+ getLogger().info(`🔄 Converting ${vendorId} document to Jay HTML...`);
2259
+ const vendor = getVendor(vendorId);
2260
+ try {
2261
+ const { projectPage, plugins } = await loadPageContracts(
2262
+ dirname,
2263
+ pageUrl,
2264
+ projectRoot
2265
+ );
2266
+ const conversionResult = await vendor.convertToBodyHtml(
2267
+ vendorDoc,
2268
+ pageUrl,
2269
+ projectPage,
2270
+ plugins
2271
+ );
2272
+ const fullJayHtml = await buildJayHtmlFromVendorResult(
2273
+ conversionResult,
2274
+ dirname,
2275
+ path.basename(dirname)
2276
+ );
2277
+ const jayHtmlPath = path.join(dirname, "page.jay-html");
2278
+ await fs.promises.writeFile(jayHtmlPath, fullJayHtml, "utf-8");
2279
+ getLogger().info(`✅ Successfully converted to Jay HTML: ${jayHtmlPath}`);
2280
+ return {
2281
+ type: "export",
2282
+ success: true,
2283
+ vendorSourcePath: vendorFilePath,
2284
+ jayHtmlPath
2285
+ };
2286
+ } catch (conversionError) {
2287
+ getLogger().error(`❌ Vendor conversion threw an error:`, conversionError);
2288
+ return {
2289
+ type: "export",
2290
+ success: false,
2291
+ vendorSourcePath: vendorFilePath,
2292
+ error: conversionError instanceof Error ? conversionError.message : "Unknown conversion error"
2293
+ };
2294
+ }
2295
+ } else {
2296
+ getLogger().info(`ℹ️ No vendor found for '${vendorId}'. Skipping conversion.`);
2297
+ }
2298
+ return {
2299
+ type: "export",
2300
+ success: true,
2301
+ vendorSourcePath: vendorFilePath
2302
+ };
2303
+ } catch (error) {
2304
+ getLogger().error("Failed to export vendor document:", error);
2305
+ return {
2306
+ type: "export",
2307
+ success: false,
2308
+ error: error instanceof Error ? error.message : "Unknown error"
2309
+ };
2310
+ }
2311
+ };
2312
+ const onImport = async (params) => {
2313
+ try {
2314
+ const pagesBasePath = path.resolve(config.devServer.pagesBase);
2315
+ const { vendorId, pageUrl } = params;
2316
+ const dirname = pageUrlToDirectoryPath(pageUrl, pagesBasePath);
2317
+ const vendorFilename = `page.${vendorId}.json`;
2318
+ const vendorFilePath = path.join(dirname, vendorFilename);
2319
+ if (!fs.existsSync(vendorFilePath)) {
2320
+ return {
2321
+ type: "import",
2322
+ success: false,
2323
+ error: `No ${vendorId} document found at ${pageUrl}. File not found: ${vendorFilePath}`
2324
+ };
2325
+ }
2326
+ const fileContent = await fs.promises.readFile(vendorFilePath, "utf-8");
2327
+ const vendorDoc = JSON.parse(fileContent);
2328
+ getLogger().info(`📥 Imported ${vendorId} document from: ${vendorFilePath}`);
2329
+ return {
2330
+ type: "import",
2331
+ success: true,
2332
+ vendorDoc
2333
+ };
2334
+ } catch (error) {
2335
+ getLogger().error("Failed to import vendor document:", error);
2336
+ return {
2337
+ type: "import",
2338
+ success: false,
2339
+ error: error instanceof Error ? error.message : "Unknown error"
2340
+ };
2341
+ }
2342
+ };
936
2343
  return {
937
2344
  onPublish,
938
2345
  onSaveImage,
939
2346
  onHasImage,
940
- onGetProjectInfo
2347
+ onGetProjectInfo,
2348
+ onExport,
2349
+ onImport
941
2350
  };
942
2351
  }
943
2352
  async function generatePageDefinitionFiles(routes, tsConfigPath, projectRoot) {
@@ -1011,6 +2420,12 @@ async function startDevServer(options = {}) {
1011
2420
  }
1012
2421
  });
1013
2422
  const { port: editorPort, editorId } = await editorServer.start();
2423
+ const registeredVendors = getRegisteredVendors();
2424
+ if (registeredVendors.length > 0) {
2425
+ log.info(
2426
+ `📦 Registered ${registeredVendors.length} vendor(s): ${registeredVendors.join(", ")}`
2427
+ );
2428
+ }
1014
2429
  const handlers = createEditorHandlers(
1015
2430
  resolvedConfig,
1016
2431
  jayOptions.tsConfigFilePath,
@@ -1020,6 +2435,8 @@ async function startDevServer(options = {}) {
1020
2435
  editorServer.onSaveImage(handlers.onSaveImage);
1021
2436
  editorServer.onHasImage(handlers.onHasImage);
1022
2437
  editorServer.onGetProjectInfo(handlers.onGetProjectInfo);
2438
+ editorServer.onExport(handlers.onExport);
2439
+ editorServer.onImport(handlers.onImport);
1023
2440
  const { server, viteServer, routes } = await mkDevServer({
1024
2441
  pagesRootFolder: path.resolve(resolvedConfig.devServer.pagesBase),
1025
2442
  projectRootFolder: process.cwd(),
@@ -1664,6 +3081,7 @@ async function initializeServicesForCli(projectRoot, viteServer) {
1664
3081
  sortPluginsByDependencies,
1665
3082
  executePluginServerInits
1666
3083
  } = await import("@jay-framework/stack-server-runtime");
3084
+ let initErrors = /* @__PURE__ */ new Map();
1667
3085
  try {
1668
3086
  const discoveredPlugins = await discoverPluginsWithInit({
1669
3087
  projectRoot,
@@ -1671,7 +3089,7 @@ async function initializeServicesForCli(projectRoot, viteServer) {
1671
3089
  });
1672
3090
  const pluginsWithInit = sortPluginsByDependencies(discoveredPlugins);
1673
3091
  try {
1674
- await executePluginServerInits(pluginsWithInit, viteServer, false);
3092
+ initErrors = await executePluginServerInits(pluginsWithInit, viteServer, false);
1675
3093
  } catch (error) {
1676
3094
  getLogger().warn(chalk.yellow(`⚠️ Plugin initialization skipped: ${error.message}`));
1677
3095
  }
@@ -1691,7 +3109,7 @@ async function initializeServicesForCli(projectRoot, viteServer) {
1691
3109
  getLogger().warn(chalk.yellow(`⚠️ Service initialization failed: ${error.message}`));
1692
3110
  getLogger().warn(chalk.gray(" Static contracts will still be listed."));
1693
3111
  }
1694
- return getServiceRegistry();
3112
+ return { services: getServiceRegistry(), initErrors };
1695
3113
  }
1696
3114
  async function runAction(actionRef, options, projectRoot, initializeServices) {
1697
3115
  let viteServer;
@@ -1859,9 +3277,7 @@ async function runSetup(pluginFilter, options, projectRoot, initializeServices)
1859
3277
  if (pluginsWithSetup.length === 0) {
1860
3278
  if (pluginFilter) {
1861
3279
  logger.important(
1862
- chalk.yellow(
1863
- `⚠️ Plugin "${pluginFilter}" not found or has no setup handler.`
1864
- )
3280
+ chalk.yellow(`⚠️ Plugin "${pluginFilter}" not found or has no setup handler.`)
1865
3281
  );
1866
3282
  } else {
1867
3283
  logger.important(chalk.gray("No plugins with setup handlers found."));
@@ -1873,13 +3289,10 @@ async function runSetup(pluginFilter, options, projectRoot, initializeServices)
1873
3289
  `Found ${pluginsWithSetup.length} plugin(s) with setup: ${pluginsWithSetup.map((p) => p.name).join(", ")}`
1874
3290
  );
1875
3291
  }
1876
- let initError;
1877
- try {
1878
- await initializeServices(projectRoot, viteServer);
1879
- } catch (error) {
1880
- initError = error;
1881
- if (options.verbose) {
1882
- logger.info(chalk.yellow(`⚠️ Service init error: ${error.message}`));
3292
+ const { initErrors } = await initializeServices(projectRoot, viteServer);
3293
+ if (initErrors.size > 0 && options.verbose) {
3294
+ for (const [name, err] of initErrors) {
3295
+ logger.info(chalk.yellow(`⚠️ ${name} init error: ${err.message}`));
1883
3296
  }
1884
3297
  }
1885
3298
  let configured = 0;
@@ -1895,7 +3308,7 @@ async function runSetup(pluginFilter, options, projectRoot, initializeServices)
1895
3308
  projectRoot,
1896
3309
  configDir,
1897
3310
  force: options.force ?? false,
1898
- initError,
3311
+ initError: initErrors.get(plugin.name),
1899
3312
  viteServer,
1900
3313
  verbose: options.verbose
1901
3314
  });
@@ -1916,7 +3329,9 @@ async function runSetup(pluginFilter, options, projectRoot, initializeServices)
1916
3329
  needsConfig++;
1917
3330
  if (result.configCreated?.length) {
1918
3331
  for (const cfg of result.configCreated) {
1919
- logger.important(chalk.yellow(` ⚠️ Config template created: ${cfg}`));
3332
+ logger.important(
3333
+ chalk.yellow(` ⚠️ Config template created: ${cfg}`)
3334
+ );
1920
3335
  }
1921
3336
  }
1922
3337
  if (result.message) {
@@ -1931,9 +3346,7 @@ async function runSetup(pluginFilter, options, projectRoot, initializeServices)
1931
3346
  break;
1932
3347
  case "error":
1933
3348
  errors++;
1934
- logger.important(
1935
- chalk.red(` ❌ ${result.message || "Setup failed"}`)
1936
- );
3349
+ logger.important(chalk.red(` ❌ ${result.message || "Setup failed"}`));
1937
3350
  break;
1938
3351
  }
1939
3352
  } catch (error) {
@@ -2038,9 +3451,7 @@ async function ensureAgentKitDocs(projectRoot, force) {
2038
3451
  try {
2039
3452
  files = (await fs2.readdir(templateDir)).filter((f) => f.endsWith(".md"));
2040
3453
  } catch {
2041
- getLogger().warn(
2042
- chalk.yellow(" Agent-kit template folder not found: " + templateDir)
2043
- );
3454
+ getLogger().warn(chalk.yellow(" Agent-kit template folder not found: " + templateDir));
2044
3455
  return;
2045
3456
  }
2046
3457
  for (const filename of files) {
@@ -2056,7 +3467,7 @@ async function ensureAgentKitDocs(projectRoot, force) {
2056
3467
  getLogger().info(chalk.gray(` Created agent-kit/${filename}`));
2057
3468
  }
2058
3469
  }
2059
- async function generatePluginReferences(projectRoot, options) {
3470
+ async function generatePluginReferences(projectRoot, options, initErrors, viteServer) {
2060
3471
  const { discoverPluginsWithReferences, executePluginReferences } = await import("@jay-framework/stack-server-runtime");
2061
3472
  const plugins = await discoverPluginsWithReferences({
2062
3473
  projectRoot,
@@ -2069,10 +3480,20 @@ async function generatePluginReferences(projectRoot, options) {
2069
3480
  logger.important("");
2070
3481
  logger.important(chalk.bold("📚 Generating plugin references..."));
2071
3482
  for (const plugin of plugins) {
3483
+ const pluginInitError = initErrors.get(plugin.name);
3484
+ if (pluginInitError) {
3485
+ logger.warn(
3486
+ chalk.yellow(
3487
+ ` ⚠️ ${plugin.name}: references skipped — init failed: ${pluginInitError.message}`
3488
+ )
3489
+ );
3490
+ continue;
3491
+ }
2072
3492
  try {
2073
3493
  const result = await executePluginReferences(plugin, {
2074
3494
  projectRoot,
2075
3495
  force: options.force ?? false,
3496
+ viteServer,
2076
3497
  verbose: options.verbose
2077
3498
  });
2078
3499
  if (result.referencesCreated.length > 0) {
@@ -2091,10 +3512,11 @@ async function generatePluginReferences(projectRoot, options) {
2091
3512
  }
2092
3513
  }
2093
3514
  }
2094
- async function runMaterialize(projectRoot, options, defaultOutputRelative) {
3515
+ async function runMaterialize(projectRoot, options, defaultOutputRelative, keepViteAlive = false) {
2095
3516
  const path2 = await import("node:path");
2096
3517
  const outputDir = options.output ?? path2.join(projectRoot, defaultOutputRelative);
2097
3518
  let viteServer;
3519
+ let initErrors = /* @__PURE__ */ new Map();
2098
3520
  try {
2099
3521
  if (options.list) {
2100
3522
  const index = await listContracts({
@@ -2107,13 +3529,17 @@ async function runMaterialize(projectRoot, options, defaultOutputRelative) {
2107
3529
  } else {
2108
3530
  printContractList(index);
2109
3531
  }
2110
- return;
3532
+ return { initErrors };
2111
3533
  }
2112
3534
  if (options.verbose) {
2113
3535
  getLogger().info("Starting Vite for TypeScript support...");
2114
3536
  }
2115
3537
  viteServer = await createViteForCli({ projectRoot });
2116
- const services = await initializeServicesForCli(projectRoot, viteServer);
3538
+ const { services, initErrors: errors } = await initializeServicesForCli(
3539
+ projectRoot,
3540
+ viteServer
3541
+ );
3542
+ initErrors = errors;
2117
3543
  const result = await materializeContracts(
2118
3544
  {
2119
3545
  projectRoot,
@@ -2137,6 +3563,7 @@ async function runMaterialize(projectRoot, options, defaultOutputRelative) {
2137
3563
  getLogger().important(` Dynamic: ${result.dynamicCount}`);
2138
3564
  getLogger().important(` Output: ${result.outputDir}`);
2139
3565
  }
3566
+ return { initErrors, viteServer: keepViteAlive ? viteServer : void 0 };
2140
3567
  } catch (error) {
2141
3568
  getLogger().error(chalk.red("❌ Failed to materialize contracts:") + " " + error.message);
2142
3569
  if (options.verbose) {
@@ -2144,10 +3571,11 @@ async function runMaterialize(projectRoot, options, defaultOutputRelative) {
2144
3571
  }
2145
3572
  process.exit(1);
2146
3573
  } finally {
2147
- if (viteServer) {
3574
+ if (viteServer && !keepViteAlive) {
2148
3575
  await viteServer.close();
2149
3576
  }
2150
3577
  }
3578
+ return { initErrors };
2151
3579
  }
2152
3580
  program.command("setup [plugin]").description(
2153
3581
  "Run plugin setup: create config templates, validate credentials, generate reference data"
@@ -2158,11 +3586,23 @@ program.command("agent-kit").description(
2158
3586
  "Prepare the agent kit: materialize contracts, generate references, and write docs to agent-kit/"
2159
3587
  ).option("-o, --output <dir>", "Output directory (default: agent-kit/materialized-contracts)").option("--yaml", "Output contract index as YAML to stdout").option("--list", "List contracts without writing files").option("--plugin <name>", "Filter to specific plugin").option("--dynamic-only", "Only process dynamic contracts").option("--force", "Force re-materialization").option("--no-references", "Skip reference data generation").option("-v, --verbose", "Show detailed output").action(async (options) => {
2160
3588
  const projectRoot = process.cwd();
2161
- await runMaterialize(projectRoot, options, "agent-kit/materialized-contracts");
2162
- if (!options.list) {
2163
- await ensureAgentKitDocs(projectRoot, options.force);
2164
- if (options.references !== false) {
2165
- await generatePluginReferences(projectRoot, options);
3589
+ const { initErrors, viteServer } = await runMaterialize(
3590
+ projectRoot,
3591
+ options,
3592
+ "agent-kit/materialized-contracts",
3593
+ /* keepViteAlive */
3594
+ true
3595
+ );
3596
+ try {
3597
+ if (!options.list) {
3598
+ await ensureAgentKitDocs(projectRoot, options.force);
3599
+ if (options.references !== false) {
3600
+ await generatePluginReferences(projectRoot, options, initErrors, viteServer);
3601
+ }
3602
+ }
3603
+ } finally {
3604
+ if (viteServer) {
3605
+ await viteServer.close();
2166
3606
  }
2167
3607
  }
2168
3608
  });
@@ -2251,6 +3691,9 @@ function printContractList(index) {
2251
3691
  export {
2252
3692
  createEditorHandlers,
2253
3693
  getConfigWithDefaults,
3694
+ getRegisteredVendors,
3695
+ getVendor,
3696
+ hasVendor,
2254
3697
  listContracts2 as listContracts,
2255
3698
  loadConfig,
2256
3699
  materializeContracts2 as materializeContracts,