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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
- }
233
- function resolveAppContractPath(appModule, contractFileName, projectRootPath) {
234
- try {
235
- const require2 = createRequire(path.join(projectRootPath, "package.json"));
236
- const modulePath = `${appModule}/${contractFileName}`;
237
- const resolvedPath = require2.resolve(modulePath);
238
- return resolvedPath;
239
- } catch (error) {
240
- getLogger().warn(
241
- `Failed to resolve contract: ${appModule}/${contractFileName} - ${error instanceof Error ? error.message : error}`
242
- );
243
- return null;
244
- }
245
- }
246
- async function scanInstalledAppContracts(configBasePath, projectRootPath) {
247
- const installedAppContracts = {};
248
- const installedAppsPath = path.join(configBasePath, "installedApps");
249
- try {
250
- if (!fs.existsSync(installedAppsPath)) {
251
- return installedAppContracts;
252
- }
253
- const appDirs = await fs.promises.readdir(installedAppsPath, { withFileTypes: true });
254
- for (const appDir of appDirs) {
255
- if (appDir.isDirectory()) {
256
- const appConfigPath = path.join(installedAppsPath, appDir.name, "app.conf.yaml");
257
- try {
258
- if (fs.existsSync(appConfigPath)) {
259
- const configContent = await fs.promises.readFile(appConfigPath, "utf-8");
260
- const appConfig = YAML.parse(configContent);
261
- const appName = appConfig.name || appDir.name;
262
- const appModule = appConfig.module || appDir.name;
263
- const appContracts = {
264
- appName,
265
- module: appModule,
266
- pages: [],
267
- components: []
268
- };
269
- if (appConfig.pages && Array.isArray(appConfig.pages)) {
270
- for (const page of appConfig.pages) {
271
- if (page.headless_components && Array.isArray(page.headless_components)) {
272
- for (const component of page.headless_components) {
273
- if (component.contract) {
274
- const contractPath = resolveAppContractPath(
275
- appModule,
276
- component.contract,
277
- projectRootPath
278
- );
279
- if (contractPath) {
280
- const contractSchema = await parseContractFile(contractPath);
281
- if (contractSchema) {
282
- appContracts.pages.push({
283
- pageName: page.name,
284
- contractSchema
285
- });
286
- }
287
- }
288
- }
289
- }
290
- }
291
- }
292
- }
293
- if (appConfig.components && Array.isArray(appConfig.components)) {
294
- for (const component of appConfig.components) {
295
- if (component.headless_components && Array.isArray(component.headless_components)) {
296
- for (const headlessComp of component.headless_components) {
297
- if (headlessComp.contract) {
298
- const contractPath = resolveAppContractPath(
299
- appModule,
300
- headlessComp.contract,
301
- projectRootPath
302
- );
303
- if (contractPath) {
304
- const contractSchema = await parseContractFile(contractPath);
305
- if (contractSchema) {
306
- appContracts.components.push({
307
- componentName: component.name,
308
- contractSchema
309
- });
310
- }
311
- }
312
- }
313
- }
314
- }
315
- }
316
- }
317
- installedAppContracts[appName] = appContracts;
318
- }
319
- } catch (error) {
320
- getLogger().warn(`Failed to parse app config ${appConfigPath}: ${error}`);
321
- }
322
- }
323
- }
324
- } catch (error) {
325
- getLogger().warn(`Failed to scan installed apps directory ${installedAppsPath}: ${error}`);
326
- }
327
- return installedAppContracts;
328
- }
329
- function extractHeadlessComponents(jayHtmlContent, installedApps, installedAppContracts) {
330
- const root = parse(jayHtmlContent);
331
- const headlessScripts = root.querySelectorAll('script[type="application/jay-headless"]');
332
- const resolvedComponents = [];
333
- for (const script of headlessScripts) {
334
- const src = script.getAttribute("src") || "";
335
- const name = script.getAttribute("name") || "";
336
- const key = script.getAttribute("key") || "";
337
- let resolved = false;
338
- for (const app of installedApps) {
339
- if (app.module !== src && app.name !== src) {
340
- continue;
341
- }
342
- for (const appPage of app.pages) {
343
- for (const headlessComp of appPage.headless_components) {
344
- if (headlessComp.name === name && headlessComp.key === key) {
345
- const appContracts = installedAppContracts[app.name];
346
- if (appContracts) {
347
- const matchingPageContract = appContracts.pages.find(
348
- (pc) => pc.pageName === appPage.name
349
- );
350
- if (matchingPageContract) {
351
- resolvedComponents.push({
352
- appName: app.name,
353
- componentName: appPage.name,
354
- key
355
- });
356
- resolved = true;
357
- break;
358
- }
359
- }
360
- }
361
- }
362
- if (resolved)
363
- break;
364
- }
365
- if (resolved)
366
- break;
367
- for (const appComponent of app.components) {
368
- for (const headlessComp of appComponent.headless_components) {
369
- if (headlessComp.name === name && headlessComp.key === key) {
370
- const appContracts = installedAppContracts[app.name];
371
- if (appContracts) {
372
- const matchingComponentContract = appContracts.components.find(
373
- (cc) => cc.componentName === appComponent.name
374
- );
375
- if (matchingComponentContract) {
376
- resolvedComponents.push({
377
- appName: app.name,
378
- componentName: appComponent.name,
379
- key
380
- });
381
- resolved = true;
382
- break;
383
- }
384
- }
385
- }
386
- }
387
- if (resolved)
388
- break;
389
- }
390
- if (resolved)
391
- break;
1673
+ }
1674
+ function loadAndExpandContract(contractFilePath) {
1675
+ try {
1676
+ const loadResult = JAY_IMPORT_RESOLVER.loadContract(contractFilePath);
1677
+ if (loadResult.validations.length > 0) {
1678
+ getLogger().warn(
1679
+ `Contract validation errors in ${contractFilePath}:`,
1680
+ loadResult.validations
1681
+ );
392
1682
  }
393
- if (!resolved) {
394
- resolvedComponents.push({
395
- appName: src,
396
- componentName: name,
397
- key
1683
+ if (loadResult.val) {
1684
+ const resolvedTags = expandContractTags(
1685
+ loadResult.val.tags,
1686
+ path.dirname(contractFilePath)
1687
+ );
1688
+ return convertContractToProtocol({
1689
+ name: loadResult.val.name,
1690
+ tags: resolvedTags
398
1691
  });
399
1692
  }
1693
+ } catch (error) {
1694
+ getLogger().warn(`Failed to parse contract file ${contractFilePath}:`, error);
1695
+ }
1696
+ return null;
1697
+ }
1698
+ async function extractHeadlessComponentsFromJayHtml(jayHtmlContent, pageFilePath, projectRootPath) {
1699
+ try {
1700
+ const parsedJayHtml = await parseJayFile(
1701
+ jayHtmlContent,
1702
+ path.basename(pageFilePath),
1703
+ path.dirname(pageFilePath),
1704
+ { relativePath: "" },
1705
+ // We don't need TypeScript config for headless extraction
1706
+ JAY_IMPORT_RESOLVER,
1707
+ projectRootPath
1708
+ );
1709
+ if (parsedJayHtml.validations.length > 0) {
1710
+ getLogger().warn(
1711
+ `Jay-HTML parsing warnings for ${pageFilePath}:`,
1712
+ parsedJayHtml.validations
1713
+ );
1714
+ }
1715
+ if (!parsedJayHtml.val) {
1716
+ getLogger().warn(`Failed to parse jay-html file: ${pageFilePath}`);
1717
+ return [];
1718
+ }
1719
+ const resolvedComponents = [];
1720
+ for (const headlessImport of parsedJayHtml.val.headlessImports) {
1721
+ if (headlessImport.codeLink) {
1722
+ let pluginName = headlessImport.codeLink.module;
1723
+ const nodeModulesMatch = pluginName.match(/node_modules\/([^/]+)/);
1724
+ if (nodeModulesMatch) {
1725
+ pluginName = nodeModulesMatch[1];
1726
+ }
1727
+ const componentName = headlessImport.contract?.name || "unknown";
1728
+ resolvedComponents.push({
1729
+ appName: pluginName,
1730
+ componentName,
1731
+ key: headlessImport.key
1732
+ });
1733
+ }
1734
+ }
1735
+ return resolvedComponents;
1736
+ } catch (error) {
1737
+ getLogger().warn(`Failed to parse jay-html content for ${pageFilePath}:`, error);
1738
+ return [];
400
1739
  }
401
- return resolvedComponents;
402
1740
  }
403
1741
  async function scanProjectComponents(componentsBasePath) {
404
1742
  const components = [];
@@ -421,43 +1759,10 @@ async function scanProjectComponents(componentsBasePath) {
421
1759
  }
422
1760
  }
423
1761
  } catch (error) {
424
- getLogger().warn(`Failed to scan components directory ${componentsBasePath}: ${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) {
@@ -993,7 +2402,7 @@ async function startDevServer(options = {}) {
993
2402
  const resolvedConfig = getConfigWithDefaults(config);
994
2403
  const jayOptions = {
995
2404
  tsConfigFilePath: "./tsconfig.json",
996
- outputDir: "build/jay-runtime"
2405
+ outputDir: "build"
997
2406
  };
998
2407
  const app = express();
999
2408
  const devServerPort = await getPort({ port: resolvedConfig.devServer.portRange });
@@ -1011,6 +2420,12 @@ async function startDevServer(options = {}) {
1011
2420
  }
1012
2421
  });
1013
2422
  const { port: editorPort, editorId } = await editorServer.start();
2423
+ const registeredVendors = getRegisteredVendors();
2424
+ if (registeredVendors.length > 0) {
2425
+ log.info(
2426
+ `📦 Registered ${registeredVendors.length} vendor(s): ${registeredVendors.join(", ")}`
2427
+ );
2428
+ }
1014
2429
  const handlers = createEditorHandlers(
1015
2430
  resolvedConfig,
1016
2431
  jayOptions.tsConfigFilePath,
@@ -1020,11 +2435,12 @@ async function startDevServer(options = {}) {
1020
2435
  editorServer.onSaveImage(handlers.onSaveImage);
1021
2436
  editorServer.onHasImage(handlers.onHasImage);
1022
2437
  editorServer.onGetProjectInfo(handlers.onGetProjectInfo);
2438
+ editorServer.onExport(handlers.onExport);
2439
+ editorServer.onImport(handlers.onImport);
1023
2440
  const { server, viteServer, routes } = await mkDevServer({
1024
2441
  pagesRootFolder: path.resolve(resolvedConfig.devServer.pagesBase),
1025
2442
  projectRootFolder: process.cwd(),
1026
2443
  publicBaseUrlPath: "/",
1027
- dontCacheSlowly: false,
1028
2444
  jayRollupConfig: jayOptions,
1029
2445
  logLevel: options.logLevel
1030
2446
  });
@@ -1517,13 +2933,206 @@ async function findJayFiles(dir) {
1517
2933
  async function findContractFiles(dir) {
1518
2934
  return await glob(`${dir}/**/*${JAY_CONTRACT_EXTENSION}`);
1519
2935
  }
2936
+ function flattenContractTags(tags, prefix) {
2937
+ const result = [];
2938
+ for (const tag of tags) {
2939
+ const tagPath = prefix ? `${prefix}.${tag.tag}` : tag.tag;
2940
+ result.push({ path: tagPath, required: tag.required === true });
2941
+ if (tag.tags) {
2942
+ result.push(...flattenContractTags(tag.tags, tagPath));
2943
+ }
2944
+ }
2945
+ return result;
2946
+ }
2947
+ function extractExpressions(text) {
2948
+ const results = [];
2949
+ const regex = /\{([^}]+)\}/g;
2950
+ let match;
2951
+ while ((match = regex.exec(text)) !== null) {
2952
+ results.push(match[1].trim());
2953
+ }
2954
+ return results;
2955
+ }
2956
+ function extractTagPath(expr) {
2957
+ let cleaned = expr.replace(/^!/, "").trim();
2958
+ cleaned = cleaned.split(/\s*[!=]==?\s*/)[0].trim();
2959
+ if (cleaned === "." || cleaned === "")
2960
+ return null;
2961
+ if (/^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*$/.test(cleaned)) {
2962
+ return cleaned;
2963
+ }
2964
+ return null;
2965
+ }
2966
+ const SKIP_ATTRS = /* @__PURE__ */ new Set([
2967
+ "forEach",
2968
+ "if",
2969
+ "ref",
2970
+ "trackBy",
2971
+ "slowForEach",
2972
+ "jayIndex",
2973
+ "jayTrackBy",
2974
+ "when-resolved",
2975
+ "when-loading",
2976
+ "when-rejected",
2977
+ "accessor"
2978
+ ]);
2979
+ function collectUsedTags(jayHtml) {
2980
+ const imports = jayHtml.headlessImports;
2981
+ const usedTags = /* @__PURE__ */ new Map();
2982
+ const keyMap = /* @__PURE__ */ new Map();
2983
+ for (let i = 0; i < imports.length; i++) {
2984
+ if (imports[i].contract) {
2985
+ usedTags.set(i, /* @__PURE__ */ new Set());
2986
+ if (imports[i].key) {
2987
+ keyMap.set(imports[i].key, i);
2988
+ }
2989
+ }
2990
+ }
2991
+ function markUsed(importIndex, tagPath) {
2992
+ usedTags.get(importIndex)?.add(tagPath);
2993
+ }
2994
+ function resolvePath(path2, scopes) {
2995
+ const dot = path2.indexOf(".");
2996
+ if (dot !== -1) {
2997
+ const key = path2.substring(0, dot);
2998
+ const idx = keyMap.get(key);
2999
+ if (idx !== void 0) {
3000
+ markUsed(idx, path2.substring(dot + 1));
3001
+ return;
3002
+ }
3003
+ }
3004
+ if (scopes.length > 0) {
3005
+ const scope = scopes[scopes.length - 1];
3006
+ const full = scope.prefix ? `${scope.prefix}.${path2}` : path2;
3007
+ markUsed(scope.importIndex, full);
3008
+ }
3009
+ }
3010
+ function walkElement(element, scopes) {
3011
+ const tagName = element.rawTagName?.toLowerCase();
3012
+ let childScopes = scopes;
3013
+ if (tagName?.startsWith("jay:")) {
3014
+ const contractName = tagName.substring(4);
3015
+ const idx = imports.findIndex(
3016
+ (imp) => imp.contractName === contractName && imp.contract
3017
+ );
3018
+ if (idx !== -1) {
3019
+ childScopes = [...scopes, { importIndex: idx, prefix: "" }];
3020
+ }
3021
+ }
3022
+ const forEachVal = element.getAttribute?.("forEach");
3023
+ if (forEachVal) {
3024
+ const fePath = extractTagPath(forEachVal);
3025
+ if (fePath) {
3026
+ resolvePath(fePath, childScopes);
3027
+ const dot = fePath.indexOf(".");
3028
+ if (dot !== -1) {
3029
+ const key = fePath.substring(0, dot);
3030
+ const idx = keyMap.get(key);
3031
+ if (idx !== void 0) {
3032
+ childScopes = [
3033
+ ...childScopes,
3034
+ { importIndex: idx, prefix: fePath.substring(dot + 1) }
3035
+ ];
3036
+ }
3037
+ } else if (childScopes.length > 0) {
3038
+ const scope = childScopes[childScopes.length - 1];
3039
+ const newPrefix = scope.prefix ? `${scope.prefix}.${fePath}` : fePath;
3040
+ childScopes = [
3041
+ ...childScopes,
3042
+ { importIndex: scope.importIndex, prefix: newPrefix }
3043
+ ];
3044
+ }
3045
+ }
3046
+ }
3047
+ if (tagName === "with-data") {
3048
+ const accessor = element.getAttribute?.("accessor");
3049
+ if (accessor && accessor !== "." && childScopes.length > 0) {
3050
+ resolvePath(accessor, childScopes);
3051
+ const scope = childScopes[childScopes.length - 1];
3052
+ const newPrefix = scope.prefix ? `${scope.prefix}.${accessor}` : accessor;
3053
+ childScopes = [
3054
+ ...childScopes,
3055
+ { importIndex: scope.importIndex, prefix: newPrefix }
3056
+ ];
3057
+ }
3058
+ }
3059
+ const ifVal = element.getAttribute?.("if");
3060
+ if (ifVal) {
3061
+ const ifPath = extractTagPath(ifVal);
3062
+ if (ifPath)
3063
+ resolvePath(ifPath, scopes);
3064
+ }
3065
+ const refVal = element.getAttribute?.("ref");
3066
+ if (refVal) {
3067
+ resolvePath(refVal, scopes);
3068
+ }
3069
+ const attrs = element.attributes ?? {};
3070
+ for (const [name, value] of Object.entries(attrs)) {
3071
+ if (SKIP_ATTRS.has(name))
3072
+ continue;
3073
+ for (const expr of extractExpressions(value)) {
3074
+ const p = extractTagPath(expr);
3075
+ if (p)
3076
+ resolvePath(p, scopes);
3077
+ }
3078
+ }
3079
+ for (const child of element.childNodes ?? []) {
3080
+ if (child.nodeType === 3) {
3081
+ const text = child.rawText ?? child.text ?? "";
3082
+ for (const expr of extractExpressions(text)) {
3083
+ const p = extractTagPath(expr);
3084
+ if (p)
3085
+ resolvePath(p, childScopes);
3086
+ }
3087
+ } else if (child.nodeType === 1) {
3088
+ walkElement(child, childScopes);
3089
+ }
3090
+ }
3091
+ }
3092
+ walkElement(jayHtml.body, []);
3093
+ return usedTags;
3094
+ }
3095
+ function analyzeTagCoverage(jayHtml, file) {
3096
+ const imports = jayHtml.headlessImports;
3097
+ const withContracts = imports.filter((imp) => imp.contract);
3098
+ if (withContracts.length === 0)
3099
+ return null;
3100
+ const usedTagsMap = collectUsedTags(jayHtml);
3101
+ const contracts = [];
3102
+ for (let i = 0; i < imports.length; i++) {
3103
+ const imp = imports[i];
3104
+ if (!imp.contract)
3105
+ continue;
3106
+ const allTags = flattenContractTags(imp.contract.tags);
3107
+ const usedSet = usedTagsMap.get(i) ?? /* @__PURE__ */ new Set();
3108
+ const expanded = new Set(usedSet);
3109
+ for (const usedPath of usedSet) {
3110
+ const segments = usedPath.split(".");
3111
+ for (let j = 1; j < segments.length; j++) {
3112
+ expanded.add(segments.slice(0, j).join("."));
3113
+ }
3114
+ }
3115
+ const unused = allTags.filter((t) => !expanded.has(t.path));
3116
+ const requiredUnused = unused.filter((t) => t.required);
3117
+ contracts.push({
3118
+ key: imp.key,
3119
+ contractName: imp.contractName,
3120
+ totalTags: allTags.length,
3121
+ usedTags: allTags.length - unused.length,
3122
+ unusedTags: unused.map((t) => t.path),
3123
+ requiredUnusedTags: requiredUnused.map((t) => t.path)
3124
+ });
3125
+ }
3126
+ return { file, contracts };
3127
+ }
1520
3128
  async function validateJayFiles(options = {}) {
1521
3129
  const config = loadConfig();
1522
3130
  const resolvedConfig = getConfigWithDefaults(config);
1523
- const projectRoot = process.cwd();
3131
+ const projectRoot = options.projectRoot ?? process.cwd();
1524
3132
  const scanDir = options.path ? path.resolve(options.path) : path.resolve(resolvedConfig.devServer.pagesBase);
1525
3133
  const errors = [];
1526
3134
  const warnings = [];
3135
+ const coverage = [];
1527
3136
  const jayHtmlFiles = await findJayFiles(scanDir);
1528
3137
  const contractFiles = await findContractFiles(scanDir);
1529
3138
  if (options.verbose) {
@@ -1589,6 +3198,10 @@ async function validateJayFiles(options = {}) {
1589
3198
  }
1590
3199
  continue;
1591
3200
  }
3201
+ const fileCoverage = analyzeTagCoverage(parsedFile.val, relativePath);
3202
+ if (fileCoverage) {
3203
+ coverage.push(fileCoverage);
3204
+ }
1592
3205
  const generatedFile = generateElementFile(
1593
3206
  parsedFile.val,
1594
3207
  RuntimeMode.MainTrusted,
@@ -1624,7 +3237,8 @@ async function validateJayFiles(options = {}) {
1624
3237
  jayHtmlFilesScanned: jayHtmlFiles.length,
1625
3238
  contractFilesScanned: contractFiles.length,
1626
3239
  errors,
1627
- warnings
3240
+ warnings,
3241
+ coverage
1628
3242
  };
1629
3243
  }
1630
3244
  function printJayValidationResult(result, options) {
@@ -1653,6 +3267,29 @@ function printJayValidationResult(result, options) {
1653
3267
  chalk.red(`${result.errors.length} error(s) found, ${validFiles} file(s) valid.`)
1654
3268
  );
1655
3269
  }
3270
+ if (result.coverage.length > 0) {
3271
+ logger.important("");
3272
+ logger.important("Tag Coverage:");
3273
+ for (const fileCov of result.coverage) {
3274
+ logger.important(` ${fileCov.file}`);
3275
+ for (const contract of fileCov.contracts) {
3276
+ const label = contract.key ? `${contract.key} (${contract.contractName})` : contract.contractName;
3277
+ logger.important(
3278
+ ` ${label}: ${contract.usedTags}/${contract.totalTags} tags used`
3279
+ );
3280
+ if (contract.unusedTags.length > 0) {
3281
+ logger.important(chalk.gray(` Unused: ${contract.unusedTags.join(", ")}`));
3282
+ }
3283
+ if (contract.requiredUnusedTags.length > 0) {
3284
+ logger.important(
3285
+ chalk.yellow(
3286
+ ` ⚠ Required unused: ${contract.requiredUnusedTags.join(", ")}`
3287
+ )
3288
+ );
3289
+ }
3290
+ }
3291
+ }
3292
+ }
1656
3293
  }
1657
3294
  async function initializeServicesForCli(projectRoot, viteServer) {
1658
3295
  const path2 = await import("node:path");
@@ -1664,6 +3301,7 @@ async function initializeServicesForCli(projectRoot, viteServer) {
1664
3301
  sortPluginsByDependencies,
1665
3302
  executePluginServerInits
1666
3303
  } = await import("@jay-framework/stack-server-runtime");
3304
+ let initErrors = /* @__PURE__ */ new Map();
1667
3305
  try {
1668
3306
  const discoveredPlugins = await discoverPluginsWithInit({
1669
3307
  projectRoot,
@@ -1671,7 +3309,7 @@ async function initializeServicesForCli(projectRoot, viteServer) {
1671
3309
  });
1672
3310
  const pluginsWithInit = sortPluginsByDependencies(discoveredPlugins);
1673
3311
  try {
1674
- await executePluginServerInits(pluginsWithInit, viteServer, false);
3312
+ initErrors = await executePluginServerInits(pluginsWithInit, viteServer, false);
1675
3313
  } catch (error) {
1676
3314
  getLogger().warn(chalk.yellow(`⚠️ Plugin initialization skipped: ${error.message}`));
1677
3315
  }
@@ -1691,7 +3329,7 @@ async function initializeServicesForCli(projectRoot, viteServer) {
1691
3329
  getLogger().warn(chalk.yellow(`⚠️ Service initialization failed: ${error.message}`));
1692
3330
  getLogger().warn(chalk.gray(" Static contracts will still be listed."));
1693
3331
  }
1694
- return getServiceRegistry();
3332
+ return { services: getServiceRegistry(), initErrors };
1695
3333
  }
1696
3334
  async function runAction(actionRef, options, projectRoot, initializeServices) {
1697
3335
  let viteServer;
@@ -1859,9 +3497,7 @@ async function runSetup(pluginFilter, options, projectRoot, initializeServices)
1859
3497
  if (pluginsWithSetup.length === 0) {
1860
3498
  if (pluginFilter) {
1861
3499
  logger.important(
1862
- chalk.yellow(
1863
- `⚠️ Plugin "${pluginFilter}" not found or has no setup handler.`
1864
- )
3500
+ chalk.yellow(`⚠️ Plugin "${pluginFilter}" not found or has no setup handler.`)
1865
3501
  );
1866
3502
  } else {
1867
3503
  logger.important(chalk.gray("No plugins with setup handlers found."));
@@ -1873,13 +3509,10 @@ async function runSetup(pluginFilter, options, projectRoot, initializeServices)
1873
3509
  `Found ${pluginsWithSetup.length} plugin(s) with setup: ${pluginsWithSetup.map((p) => p.name).join(", ")}`
1874
3510
  );
1875
3511
  }
1876
- 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}`));
3512
+ const { initErrors } = await initializeServices(projectRoot, viteServer);
3513
+ if (initErrors.size > 0 && options.verbose) {
3514
+ for (const [name, err] of initErrors) {
3515
+ logger.info(chalk.yellow(`⚠️ ${name} init error: ${err.message}`));
1883
3516
  }
1884
3517
  }
1885
3518
  let configured = 0;
@@ -1895,7 +3528,7 @@ async function runSetup(pluginFilter, options, projectRoot, initializeServices)
1895
3528
  projectRoot,
1896
3529
  configDir,
1897
3530
  force: options.force ?? false,
1898
- initError,
3531
+ initError: initErrors.get(plugin.name),
1899
3532
  viteServer,
1900
3533
  verbose: options.verbose
1901
3534
  });
@@ -1916,7 +3549,9 @@ async function runSetup(pluginFilter, options, projectRoot, initializeServices)
1916
3549
  needsConfig++;
1917
3550
  if (result.configCreated?.length) {
1918
3551
  for (const cfg of result.configCreated) {
1919
- logger.important(chalk.yellow(` ⚠️ Config template created: ${cfg}`));
3552
+ logger.important(
3553
+ chalk.yellow(` ⚠️ Config template created: ${cfg}`)
3554
+ );
1920
3555
  }
1921
3556
  }
1922
3557
  if (result.message) {
@@ -1931,9 +3566,7 @@ async function runSetup(pluginFilter, options, projectRoot, initializeServices)
1931
3566
  break;
1932
3567
  case "error":
1933
3568
  errors++;
1934
- logger.important(
1935
- chalk.red(` ❌ ${result.message || "Setup failed"}`)
1936
- );
3569
+ logger.important(chalk.red(` ❌ ${result.message || "Setup failed"}`));
1937
3570
  break;
1938
3571
  }
1939
3572
  } catch (error) {
@@ -2038,9 +3671,7 @@ async function ensureAgentKitDocs(projectRoot, force) {
2038
3671
  try {
2039
3672
  files = (await fs2.readdir(templateDir)).filter((f) => f.endsWith(".md"));
2040
3673
  } catch {
2041
- getLogger().warn(
2042
- chalk.yellow(" Agent-kit template folder not found: " + templateDir)
2043
- );
3674
+ getLogger().warn(chalk.yellow(" Agent-kit template folder not found: " + templateDir));
2044
3675
  return;
2045
3676
  }
2046
3677
  for (const filename of files) {
@@ -2056,7 +3687,7 @@ async function ensureAgentKitDocs(projectRoot, force) {
2056
3687
  getLogger().info(chalk.gray(` Created agent-kit/${filename}`));
2057
3688
  }
2058
3689
  }
2059
- async function generatePluginReferences(projectRoot, options) {
3690
+ async function generatePluginReferences(projectRoot, options, initErrors, viteServer) {
2060
3691
  const { discoverPluginsWithReferences, executePluginReferences } = await import("@jay-framework/stack-server-runtime");
2061
3692
  const plugins = await discoverPluginsWithReferences({
2062
3693
  projectRoot,
@@ -2069,10 +3700,20 @@ async function generatePluginReferences(projectRoot, options) {
2069
3700
  logger.important("");
2070
3701
  logger.important(chalk.bold("📚 Generating plugin references..."));
2071
3702
  for (const plugin of plugins) {
3703
+ const pluginInitError = initErrors.get(plugin.name);
3704
+ if (pluginInitError) {
3705
+ logger.warn(
3706
+ chalk.yellow(
3707
+ ` ⚠️ ${plugin.name}: references skipped — init failed: ${pluginInitError.message}`
3708
+ )
3709
+ );
3710
+ continue;
3711
+ }
2072
3712
  try {
2073
3713
  const result = await executePluginReferences(plugin, {
2074
3714
  projectRoot,
2075
3715
  force: options.force ?? false,
3716
+ viteServer,
2076
3717
  verbose: options.verbose
2077
3718
  });
2078
3719
  if (result.referencesCreated.length > 0) {
@@ -2091,10 +3732,11 @@ async function generatePluginReferences(projectRoot, options) {
2091
3732
  }
2092
3733
  }
2093
3734
  }
2094
- async function runMaterialize(projectRoot, options, defaultOutputRelative) {
3735
+ async function runMaterialize(projectRoot, options, defaultOutputRelative, keepViteAlive = false) {
2095
3736
  const path2 = await import("node:path");
2096
3737
  const outputDir = options.output ?? path2.join(projectRoot, defaultOutputRelative);
2097
3738
  let viteServer;
3739
+ let initErrors = /* @__PURE__ */ new Map();
2098
3740
  try {
2099
3741
  if (options.list) {
2100
3742
  const index = await listContracts({
@@ -2107,13 +3749,17 @@ async function runMaterialize(projectRoot, options, defaultOutputRelative) {
2107
3749
  } else {
2108
3750
  printContractList(index);
2109
3751
  }
2110
- return;
3752
+ return { initErrors };
2111
3753
  }
2112
3754
  if (options.verbose) {
2113
3755
  getLogger().info("Starting Vite for TypeScript support...");
2114
3756
  }
2115
3757
  viteServer = await createViteForCli({ projectRoot });
2116
- const services = await initializeServicesForCli(projectRoot, viteServer);
3758
+ const { services, initErrors: errors } = await initializeServicesForCli(
3759
+ projectRoot,
3760
+ viteServer
3761
+ );
3762
+ initErrors = errors;
2117
3763
  const result = await materializeContracts(
2118
3764
  {
2119
3765
  projectRoot,
@@ -2127,16 +3773,19 @@ async function runMaterialize(projectRoot, options, defaultOutputRelative) {
2127
3773
  services
2128
3774
  );
2129
3775
  if (options.yaml) {
2130
- getLogger().important(YAML.stringify(result.index));
3776
+ getLogger().important(YAML.stringify(result.pluginsIndex));
2131
3777
  } else {
2132
- getLogger().important(
2133
- chalk.green(`
2134
- ✅ Materialized ${result.index.contracts.length} contracts`)
3778
+ const totalContracts = result.pluginsIndex.plugins.reduce(
3779
+ (sum, p) => sum + p.contracts.length,
3780
+ 0
2135
3781
  );
3782
+ getLogger().important(chalk.green(`
3783
+ ✅ Materialized ${totalContracts} contracts`));
2136
3784
  getLogger().important(` Static: ${result.staticCount}`);
2137
3785
  getLogger().important(` Dynamic: ${result.dynamicCount}`);
2138
3786
  getLogger().important(` Output: ${result.outputDir}`);
2139
3787
  }
3788
+ return { initErrors, viteServer: keepViteAlive ? viteServer : void 0 };
2140
3789
  } catch (error) {
2141
3790
  getLogger().error(chalk.red("❌ Failed to materialize contracts:") + " " + error.message);
2142
3791
  if (options.verbose) {
@@ -2144,10 +3793,11 @@ async function runMaterialize(projectRoot, options, defaultOutputRelative) {
2144
3793
  }
2145
3794
  process.exit(1);
2146
3795
  } finally {
2147
- if (viteServer) {
3796
+ if (viteServer && !keepViteAlive) {
2148
3797
  await viteServer.close();
2149
3798
  }
2150
3799
  }
3800
+ return { initErrors };
2151
3801
  }
2152
3802
  program.command("setup [plugin]").description(
2153
3803
  "Run plugin setup: create config templates, validate credentials, generate reference data"
@@ -2158,11 +3808,23 @@ program.command("agent-kit").description(
2158
3808
  "Prepare the agent kit: materialize contracts, generate references, and write docs to agent-kit/"
2159
3809
  ).option("-o, --output <dir>", "Output directory (default: agent-kit/materialized-contracts)").option("--yaml", "Output contract index as YAML to stdout").option("--list", "List contracts without writing files").option("--plugin <name>", "Filter to specific plugin").option("--dynamic-only", "Only process dynamic contracts").option("--force", "Force re-materialization").option("--no-references", "Skip reference data generation").option("-v, --verbose", "Show detailed output").action(async (options) => {
2160
3810
  const projectRoot = process.cwd();
2161
- await runMaterialize(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);
3811
+ const { initErrors, viteServer } = await runMaterialize(
3812
+ projectRoot,
3813
+ options,
3814
+ "agent-kit/materialized-contracts",
3815
+ /* keepViteAlive */
3816
+ true
3817
+ );
3818
+ try {
3819
+ if (!options.list) {
3820
+ await ensureAgentKitDocs(projectRoot, options.force);
3821
+ if (options.references !== false) {
3822
+ await generatePluginReferences(projectRoot, options, initErrors, viteServer);
3823
+ }
3824
+ }
3825
+ } finally {
3826
+ if (viteServer) {
3827
+ await viteServer.close();
2166
3828
  }
2167
3829
  }
2168
3830
  });
@@ -2230,27 +3892,24 @@ function printValidationResult(result, verbose) {
2230
3892
  function printContractList(index) {
2231
3893
  const logger = getLogger();
2232
3894
  logger.important("\nAvailable Contracts:\n");
2233
- const byPlugin = /* @__PURE__ */ new Map();
2234
- for (const contract of index.contracts) {
2235
- const existing = byPlugin.get(contract.plugin) || [];
2236
- existing.push(contract);
2237
- byPlugin.set(contract.plugin, existing);
2238
- }
2239
- for (const [plugin, contracts] of byPlugin) {
2240
- logger.important(chalk.bold(`📦 ${plugin}`));
2241
- for (const contract of contracts) {
3895
+ for (const plugin of index.plugins) {
3896
+ logger.important(chalk.bold(`📦 ${plugin.name}`));
3897
+ for (const contract of plugin.contracts) {
2242
3898
  const typeIcon = contract.type === "static" ? "📄" : "⚡";
2243
3899
  logger.important(` ${typeIcon} ${contract.name}`);
2244
3900
  }
2245
3901
  logger.important("");
2246
3902
  }
2247
- if (index.contracts.length === 0) {
3903
+ if (index.plugins.length === 0) {
2248
3904
  logger.important(chalk.gray("No contracts found."));
2249
3905
  }
2250
3906
  }
2251
3907
  export {
2252
3908
  createEditorHandlers,
2253
3909
  getConfigWithDefaults,
3910
+ getRegisteredVendors,
3911
+ getVendor,
3912
+ hasVendor,
2254
3913
  listContracts2 as listContracts,
2255
3914
  loadConfig,
2256
3915
  materializeContracts2 as materializeContracts,