@jay-framework/jay-stack-cli 0.22.1 → 0.23.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
@@ -2,20 +2,19 @@
2
2
  import express from "express";
3
3
  import http from "node:http";
4
4
  import { mkDevServer, createViteForCli } from "@jay-framework/dev-server";
5
- import { createEditorServer } from "@jay-framework/editor-server";
6
5
  import getPort from "get-port";
7
6
  import path from "path";
8
7
  import fs, { promises } from "fs";
9
8
  import YAML from "yaml";
10
9
  import { getLogger, setDevLogger, createDevLogger } from "@jay-framework/logger";
11
- import { parseJayFile, JAY_IMPORT_RESOLVER, generateElementDefinitionFile, ContractTagType, parseContract, generateElementFile, generateServerElementFile, htmlElementTagNameMap, loadLinkedContract, getLinkedContractDir } from "@jay-framework/compiler-jay-html";
12
- import { JAY_CONTRACT_EXTENSION, JAY_EXTENSION, resolvePluginManifest, LOCAL_PLUGIN_PATH, JayAtomicType, JayEnumType, loadPluginManifest, RuntimeMode, GenerateTarget, findDynamicContract } from "@jay-framework/compiler-shared";
13
- import { scanPlugins as scanPlugins$1, listContracts, materializeContracts, SetupNeedsAnswerError } from "@jay-framework/stack-server-runtime";
10
+ import { JAY_IMPORT_RESOLVER, parseJayFile, generateElementDefinitionFile, parseContract, generateElementFile, generateServerElementFile, htmlElementTagNameMap, loadLinkedContract, getLinkedContractDir } from "@jay-framework/compiler-jay-html";
11
+ import { scanPlugins, listContracts, materializeContracts, SetupNeedsAnswerError, discoverPluginsWithSetup, discoverPluginsWithInit, sortPluginsByDependencies, executePluginSetup, executePluginServerInits, runInitCallbacks } from "@jay-framework/stack-server-runtime";
14
12
  import { listContracts as listContracts2, materializeContracts as materializeContracts2 } from "@jay-framework/stack-server-runtime";
15
13
  import { Command } from "commander";
16
14
  import chalk from "chalk";
17
15
  import path$1 from "node:path";
18
16
  import fs$1 from "node:fs/promises";
17
+ import { loadPluginManifest, JAY_EXTENSION, RuntimeMode, GenerateTarget, JAY_CONTRACT_EXTENSION, findDynamicContract } from "@jay-framework/compiler-shared";
19
18
  import { createRequire } from "module";
20
19
  import { glob } from "glob";
21
20
  import fsSync from "node:fs";
@@ -28,9 +27,6 @@ const DEFAULT_CONFIG = {
28
27
  componentsBase: "./src/components",
29
28
  publicFolder: "./public",
30
29
  configBase: "./config"
31
- },
32
- editorServer: {
33
- portRange: [3101, 3200]
34
30
  }
35
31
  };
36
32
  function loadConfig() {
@@ -45,2317 +41,42 @@ function loadConfig() {
45
41
  devServer: {
46
42
  ...DEFAULT_CONFIG.devServer,
47
43
  ...userConfig.devServer
48
- },
49
- editorServer: {
50
- ...DEFAULT_CONFIG.editorServer,
51
- ...userConfig.editorServer
52
- }
53
- };
54
- } catch (error) {
55
- getLogger().warn(`Failed to parse .jay YAML config file, using defaults: ${error}`);
56
- return DEFAULT_CONFIG;
57
- }
58
- }
59
- function getConfigWithDefaults(config) {
60
- return {
61
- devServer: {
62
- portRange: config.devServer?.portRange || DEFAULT_CONFIG.devServer.portRange,
63
- pagesBase: config.devServer?.pagesBase || DEFAULT_CONFIG.devServer.pagesBase,
64
- componentsBase: config.devServer?.componentsBase || DEFAULT_CONFIG.devServer.componentsBase,
65
- publicFolder: config.devServer?.publicFolder || DEFAULT_CONFIG.devServer.publicFolder,
66
- configBase: config.devServer?.configBase || DEFAULT_CONFIG.devServer.configBase
67
- },
68
- editorServer: {
69
- portRange: config.editorServer?.portRange || DEFAULT_CONFIG.editorServer.portRange,
70
- editorId: config.editorServer?.editorId
71
- }
72
- };
73
- }
74
- function updateConfig(updates) {
75
- const configPath = path.resolve(".jay");
76
- try {
77
- const existingConfig = loadConfig();
78
- const updatedConfig = {
79
- ...existingConfig,
80
- ...updates,
81
- devServer: {
82
- ...existingConfig.devServer,
83
- ...updates.devServer
84
- },
85
- editorServer: {
86
- ...existingConfig.editorServer,
87
- ...updates.editorServer
88
- }
89
- };
90
- const yamlContent = YAML.stringify(updatedConfig, { indent: 2 });
91
- fs.writeFileSync(configPath, yamlContent);
92
- } catch (error) {
93
- getLogger().warn(`Failed to update .jay config file: ${error}`);
94
- }
95
- }
96
- function rgbToHex(color, opacity) {
97
- const r = Math.round(color.r * 255);
98
- const g = Math.round(color.g * 255);
99
- const b = Math.round(color.b * 255);
100
- if (opacity !== void 0 && opacity < 1) {
101
- const alphaHex = Math.round(opacity * 255).toString(16).padStart(2, "0");
102
- return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}${alphaHex}`;
103
- } else {
104
- return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
105
- }
106
- }
107
- function getPositionType(node) {
108
- if (node.layoutPositioning === "ABSOLUTE") {
109
- return "absolute";
110
- }
111
- if (node.parentOverflowDirection && node.parentOverflowDirection !== "NONE") {
112
- if (node.parentNumberOfFixedChildren && node.parentChildIndex !== void 0) {
113
- if (node.parentChildIndex >= 0 && node.parentChildIndex < node.parentNumberOfFixedChildren) {
114
- return "sticky";
115
- }
116
- }
117
- }
118
- if (node.scrollBehavior === "FIXED" && node.parentLayoutMode === "NONE") {
119
- return "fixed";
120
- }
121
- if (node.parentType === "SECTION") {
122
- return "absolute";
123
- }
124
- if (node.parentLayoutMode === "NONE") {
125
- return "absolute";
126
- }
127
- if (node.parentLayoutMode && (node.parentLayoutMode === "HORIZONTAL" || node.parentLayoutMode === "VERTICAL")) {
128
- return "relative";
129
- }
130
- return "static";
131
- }
132
- function getPositionStyle(node) {
133
- if (node.type === "COMPONENT") {
134
- return "";
135
- }
136
- const positionType = getPositionType(node);
137
- if (positionType === "static") {
138
- return "";
139
- }
140
- if (positionType === "absolute" || positionType === "fixed") {
141
- const top = node.y !== void 0 ? node.y : 0;
142
- const left = node.x !== void 0 ? node.x : 0;
143
- return `position: ${positionType};top: ${top}px;left: ${left}px;`;
144
- }
145
- if (positionType === "sticky") {
146
- return `position: ${positionType};top: 0;z-index: 10;`;
147
- }
148
- return `position: ${positionType};`;
149
- }
150
- function getAutoLayoutChildSizeStyles(node) {
151
- if (!node.parentLayoutMode || node.parentLayoutMode === "NONE") {
152
- const width2 = node.width !== void 0 ? node.width : 0;
153
- const height2 = node.height !== void 0 ? node.height : 0;
154
- return `width: ${width2}px;height: ${height2}px;`;
155
- }
156
- let styles = "";
157
- if (!node.layoutGrow && !node.layoutAlign) {
158
- const width2 = node.width !== void 0 ? node.width : 0;
159
- const height2 = node.height !== void 0 ? node.height : 0;
160
- return `width: ${width2}px;height: ${height2}px;`;
161
- }
162
- const isHorizontalLayout = node.parentLayoutMode === "HORIZONTAL";
163
- const width = node.width !== void 0 ? node.width : 0;
164
- const height = node.height !== void 0 ? node.height : 0;
165
- if (node.layoutSizingHorizontal) {
166
- switch (node.layoutSizingHorizontal) {
167
- case "FIXED":
168
- styles += `width: ${width}px;`;
169
- break;
170
- case "HUG":
171
- styles += "width: fit-content;";
172
- break;
173
- case "FILL":
174
- if (node.type === "TEXT") {
175
- styles += "width: auto;";
176
- } else if (isHorizontalLayout) {
177
- styles += "flex-grow: 1;";
178
- } else {
179
- styles += "width: 100%;";
180
- }
181
- break;
182
- }
183
- } else {
184
- if (isHorizontalLayout && node.layoutGrow && node.layoutGrow > 0) {
185
- styles += `flex-grow: ${node.layoutGrow};width: 0;`;
186
- } else if (!isHorizontalLayout && node.layoutAlign === "STRETCH") {
187
- styles += "width: 100%;";
188
- } else {
189
- styles += `width: ${width}px;`;
190
- }
191
- }
192
- if (node.layoutSizingVertical) {
193
- switch (node.layoutSizingVertical) {
194
- case "FIXED":
195
- styles += `height: ${height}px;`;
196
- break;
197
- case "HUG":
198
- styles += "height: fit-content;";
199
- break;
200
- case "FILL":
201
- if (!isHorizontalLayout) {
202
- styles += "flex-grow: 1;";
203
- } else {
204
- styles += "height: 100%;";
205
- }
206
- break;
207
- }
208
- } else {
209
- if (!isHorizontalLayout && node.layoutGrow && node.layoutGrow > 0) {
210
- styles += `flex-grow: ${node.layoutGrow};height: 0;`;
211
- } else if (isHorizontalLayout && node.layoutAlign === "STRETCH") {
212
- styles += "height: 100%;";
213
- } else {
214
- styles += `height: ${height}px;`;
215
- }
216
- }
217
- if (node.layoutAlign) {
218
- switch (node.layoutAlign) {
219
- case "MIN":
220
- styles += "align-self: flex-start;";
221
- break;
222
- case "CENTER":
223
- styles += "align-self: center;";
224
- break;
225
- case "MAX":
226
- styles += "align-self: flex-end;";
227
- break;
228
- case "STRETCH":
229
- styles += "align-self: stretch;";
230
- break;
231
- }
232
- }
233
- return styles;
234
- }
235
- function getNodeSizeStyles(node) {
236
- if (node.parentType === "SECTION") {
237
- const height = node.height !== void 0 ? node.height : 0;
238
- return `width: 100%;height: ${height}px;`;
239
- }
240
- return getAutoLayoutChildSizeStyles(node);
241
- }
242
- function getCommonStyles(node) {
243
- let styles = "";
244
- const transformStyles = [];
245
- if (node.opacity !== void 0 && node.opacity < 1) {
246
- styles += `opacity: ${node.opacity};`;
247
- }
248
- if (node.rotation !== void 0 && node.rotation !== 0) {
249
- transformStyles.push(`rotate(${node.rotation}deg)`);
250
- }
251
- if (node.effects && Array.isArray(node.effects) && node.effects.length > 0) {
252
- const visibleEffects = node.effects.filter((e2) => e2.visible !== false).reverse();
253
- const filterFunctions = [];
254
- const boxShadows = [];
255
- for (const effect of visibleEffects) {
256
- switch (effect.type) {
257
- case "DROP_SHADOW":
258
- case "INNER_SHADOW": {
259
- if (effect.color && effect.offset && effect.radius !== void 0) {
260
- const { offset, radius, color, spread } = effect;
261
- const shadowColor = `rgba(${Math.round(color.r * 255)}, ${Math.round(color.g * 255)}, ${Math.round(color.b * 255)}, ${color.a ?? 1})`;
262
- const inset = effect.type === "INNER_SHADOW" ? "inset " : "";
263
- boxShadows.push(
264
- `${inset}${offset.x}px ${offset.y}px ${radius}px ${spread ?? 0}px ${shadowColor}`
265
- );
266
- }
267
- break;
268
- }
269
- case "LAYER_BLUR":
270
- if (effect.radius !== void 0) {
271
- filterFunctions.push(`blur(${effect.radius}px)`);
272
- }
273
- break;
274
- case "BACKGROUND_BLUR":
275
- if (effect.radius !== void 0) {
276
- styles += `backdrop-filter: blur(${effect.radius}px);`;
277
- styles += `-webkit-backdrop-filter: blur(${effect.radius}px);`;
278
- }
279
- break;
280
- }
281
- }
282
- if (boxShadows.length > 0) {
283
- styles += `box-shadow: ${boxShadows.join(", ")};`;
284
- }
285
- if (filterFunctions.length > 0) {
286
- styles += `filter: ${filterFunctions.join(" ")};`;
287
- }
288
- }
289
- if (transformStyles.length > 0) {
290
- styles += `transform: ${transformStyles.join(" ")};`;
291
- const width = node.width !== void 0 ? node.width : 0;
292
- const height = node.height !== void 0 ? node.height : 0;
293
- styles += `transform-origin: ${width / 2}px ${height / 2}px;`;
294
- }
295
- return styles;
296
- }
297
- function getBorderRadius(node) {
298
- if (typeof node.cornerRadius === "number") {
299
- return `border-radius: ${node.cornerRadius}px;`;
300
- } else if (node.cornerRadius === "MIXED" && node.topLeftRadius !== void 0) {
301
- return `border-radius: ${node.topLeftRadius}px ${node.topRightRadius}px ${node.bottomRightRadius}px ${node.bottomLeftRadius}px;`;
302
- }
303
- return "border-radius: 0px;";
304
- }
305
- function getAutoLayoutStyles(node) {
306
- if (node.layoutMode === "NONE" || !node.layoutMode) {
307
- return "";
308
- }
309
- let flexStyles = "display: flex;";
310
- if (node.layoutMode === "HORIZONTAL") {
311
- flexStyles += "flex-direction: row;";
312
- } else if (node.layoutMode === "VERTICAL") {
313
- flexStyles += "flex-direction: column;";
314
- }
315
- if (node.primaryAxisAlignItems) {
316
- switch (node.primaryAxisAlignItems) {
317
- case "MIN":
318
- flexStyles += "justify-content: flex-start;";
319
- break;
320
- case "CENTER":
321
- flexStyles += "justify-content: center;";
322
- break;
323
- case "MAX":
324
- flexStyles += "justify-content: flex-end;";
325
- break;
326
- case "SPACE_BETWEEN":
327
- flexStyles += "justify-content: space-between;";
328
- break;
329
- }
330
- }
331
- if (node.counterAxisAlignItems) {
332
- switch (node.counterAxisAlignItems) {
333
- case "MIN":
334
- flexStyles += "align-items: flex-start;";
335
- break;
336
- case "CENTER":
337
- flexStyles += "align-items: center;";
338
- break;
339
- case "MAX":
340
- flexStyles += "align-items: flex-end;";
341
- break;
342
- }
343
- }
344
- if (typeof node.itemSpacing === "number") {
345
- flexStyles += `gap: ${node.itemSpacing}px;`;
346
- }
347
- if (typeof node.paddingLeft === "number")
348
- flexStyles += `padding-left: ${node.paddingLeft}px;`;
349
- if (typeof node.paddingRight === "number")
350
- flexStyles += `padding-right: ${node.paddingRight}px;`;
351
- if (typeof node.paddingTop === "number")
352
- flexStyles += `padding-top: ${node.paddingTop}px;`;
353
- if (typeof node.paddingBottom === "number")
354
- flexStyles += `padding-bottom: ${node.paddingBottom}px;`;
355
- return flexStyles;
356
- }
357
- function getOverflowStyles(node) {
358
- let overflowStyles = "";
359
- const shouldClip = node.clipsContent;
360
- const overflowDirection = node.overflowDirection || "NONE";
361
- switch (overflowDirection) {
362
- case "HORIZONTAL":
363
- overflowStyles += shouldClip ? "overflow-x: auto; overflow-y: hidden;" : "overflow-x: auto; overflow-y: visible;";
364
- break;
365
- case "VERTICAL":
366
- overflowStyles += shouldClip ? "overflow-x: hidden; overflow-y: auto;" : "overflow-x: visible; overflow-y: auto;";
367
- break;
368
- case "BOTH":
369
- overflowStyles += "overflow: auto;";
370
- break;
371
- case "NONE":
372
- default:
373
- overflowStyles += shouldClip ? "overflow: hidden;" : "overflow: visible;";
374
- break;
375
- }
376
- if (overflowDirection !== "NONE") {
377
- overflowStyles += "scrollbar-width: thin; scrollbar-color: rgba(0, 0, 0, 0.3) transparent;";
378
- }
379
- return overflowStyles;
380
- }
381
- function getBackgroundFillsStyle(node) {
382
- if (!node.fills || !Array.isArray(node.fills) || node.fills.length === 0) {
383
- return "background: transparent;";
384
- }
385
- const backgrounds = [];
386
- const backgroundSizes = [];
387
- const backgroundPositions = [];
388
- const backgroundRepeats = [];
389
- for (const fill of [...node.fills].reverse()) {
390
- if (fill.visible === false)
391
- continue;
392
- if (fill.type === "SOLID" && fill.color) {
393
- const { r, g, b } = fill.color;
394
- const opacity = fill.opacity !== void 0 ? fill.opacity : 1;
395
- backgrounds.push(
396
- `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}))`
397
- );
398
- backgroundSizes.push("100% 100%");
399
- backgroundPositions.push("center");
400
- backgroundRepeats.push("no-repeat");
401
- } else if (fill.type === "IMAGE") {
402
- console.warn("Image fills are not yet supported in vendor conversion");
403
- }
404
- }
405
- if (backgrounds.length === 0) {
406
- return "background: transparent;";
407
- }
408
- let style = `background-image: ${backgrounds.join(", ")};`;
409
- style += `background-size: ${backgroundSizes.join(", ")};`;
410
- style += `background-position: ${backgroundPositions.join(", ")};`;
411
- style += `background-repeat: ${backgroundRepeats.join(", ")};`;
412
- return style;
413
- }
414
- function getStrokeStyles(node) {
415
- if (!node.strokes || node.strokes.length === 0) {
416
- return "";
417
- }
418
- const visibleStrokes = node.strokes.filter((s2) => s2.visible !== false);
419
- if (visibleStrokes.length === 0) {
420
- return "";
421
- }
422
- const stroke = visibleStrokes[0];
423
- let cssProps = [];
424
- if (stroke.type === "SOLID" && stroke.color) {
425
- const r = Math.round(stroke.color.r * 255);
426
- const g = Math.round(stroke.color.g * 255);
427
- const b = Math.round(stroke.color.b * 255);
428
- const a = stroke.opacity !== void 0 ? stroke.opacity : 1;
429
- if (a < 1) {
430
- cssProps.push(`border-color: rgba(${r}, ${g}, ${b}, ${a.toFixed(2)});`);
431
- } else {
432
- cssProps.push(`border-color: rgb(${r}, ${g}, ${b});`);
433
- }
434
- } else {
435
- return "";
436
- }
437
- const top = typeof node.strokeTopWeight === "number" ? node.strokeTopWeight : typeof node.strokeWeight === "number" ? node.strokeWeight : 0;
438
- const right = typeof node.strokeRightWeight === "number" ? node.strokeRightWeight : typeof node.strokeWeight === "number" ? node.strokeWeight : 0;
439
- const bottom = typeof node.strokeBottomWeight === "number" ? node.strokeBottomWeight : typeof node.strokeWeight === "number" ? node.strokeWeight : 0;
440
- const left = typeof node.strokeLeftWeight === "number" ? node.strokeLeftWeight : typeof node.strokeWeight === "number" ? node.strokeWeight : 0;
441
- if (top !== 0 || right !== 0 || bottom !== 0 || left !== 0) {
442
- if (top === right && right === bottom && bottom === left) {
443
- cssProps.push(`border-width: ${top}px;`);
444
- } else {
445
- cssProps.push(`border-width: ${top}px ${right}px ${bottom}px ${left}px;`);
446
- }
447
- if (node.dashPattern && node.dashPattern.length > 0) {
448
- cssProps.push("border-style: dashed;");
449
- } else {
450
- cssProps.push("border-style: solid;");
451
- }
452
- }
453
- return cssProps.join(" ");
454
- }
455
- function getFrameSizeStyles(node) {
456
- const isTopLevel = node.parentType === "SECTION";
457
- if (isTopLevel) {
458
- const height2 = node.height !== void 0 ? node.height : 0;
459
- return `width: 100%;height: ${height2}px;`;
460
- }
461
- if (node.layoutMode === "NONE" || !node.layoutMode) {
462
- const width2 = node.width !== void 0 ? node.width : 0;
463
- const height2 = node.height !== void 0 ? node.height : 0;
464
- return `width: ${width2}px;height: ${height2}px;`;
465
- }
466
- let sizeStyles = "";
467
- const width = node.width !== void 0 ? node.width : 0;
468
- const height = node.height !== void 0 ? node.height : 0;
469
- if (node.layoutSizingHorizontal) {
470
- switch (node.layoutSizingHorizontal) {
471
- case "FIXED":
472
- sizeStyles += `width: ${width}px;`;
473
- break;
474
- case "HUG":
475
- sizeStyles += "width: fit-content;";
476
- break;
477
- case "FILL":
478
- sizeStyles += "width: 100%;";
479
- break;
480
- }
481
- } else {
482
- sizeStyles += `width: ${width}px;`;
483
- }
484
- if (node.layoutSizingVertical) {
485
- switch (node.layoutSizingVertical) {
486
- case "FIXED":
487
- sizeStyles += `height: ${height}px;`;
488
- break;
489
- case "HUG":
490
- sizeStyles += "height: fit-content;";
491
- break;
492
- case "FILL":
493
- sizeStyles += "height: 100%;";
494
- break;
495
- }
496
- } else {
497
- sizeStyles += `height: ${height}px;`;
498
- }
499
- if (node.layoutWrap === "WRAP") {
500
- sizeStyles += `max-width: ${width}px;`;
501
- }
502
- return sizeStyles;
503
- }
504
- function escapeHtmlContent(text) {
505
- const map = {
506
- "&": "&amp;",
507
- "<": "&lt;",
508
- ">": "&gt;",
509
- '"': "&quot;",
510
- "'": "&#39;"
511
- };
512
- return text.replace(/[&<>"']/g, (char) => map[char]);
513
- }
514
- function convertTextNodeToHtml(node, indent, dynamicContent, refAttr, attributesHtml) {
515
- const {
516
- name,
517
- id,
518
- characters,
519
- fontName,
520
- fontSize,
521
- fontWeight,
522
- fills,
523
- textAlignHorizontal,
524
- textAlignVertical,
525
- letterSpacing,
526
- lineHeight,
527
- textDecoration,
528
- textCase,
529
- textTruncation,
530
- maxLines,
531
- maxWidth,
532
- textAutoResize,
533
- hasMissingFont,
534
- hyperlinks
535
- } = node;
536
- if (hasMissingFont || !characters) {
537
- if (hasMissingFont) {
538
- return `${indent}<!-- Text node "${name}" has missing fonts -->
539
- `;
540
- }
541
- return "";
542
- }
543
- let fontFamilyStyle = "font-family: sans-serif;";
544
- if (fontName && typeof fontName === "object" && fontName.family) {
545
- fontFamilyStyle = `font-family: '${fontName.family}', sans-serif;`;
546
- }
547
- const fontSizeValue = typeof fontSize === "number" ? fontSize : 16;
548
- const fontSizeStyle = `font-size: ${fontSizeValue}px;`;
549
- const fontWeightValue = typeof fontWeight === "number" ? fontWeight : 400;
550
- const fontWeightStyle = `font-weight: ${fontWeightValue};`;
551
- let textColor = "#000000";
552
- if (fills && Array.isArray(fills) && fills.length > 0 && fills[0].type === "SOLID" && fills[0].color) {
553
- textColor = rgbToHex(fills[0].color);
554
- }
555
- const colorStyle = `color: ${textColor};`;
556
- const textAlign = textAlignHorizontal ? textAlignHorizontal.toLowerCase() : "left";
557
- const textAlignStyle = `text-align: ${textAlign};`;
558
- let verticalAlignWrapperStyle = "";
559
- if (textAlignVertical) {
560
- verticalAlignWrapperStyle = "display: flex; flex-direction: column;";
561
- switch (textAlignVertical) {
562
- case "TOP":
563
- verticalAlignWrapperStyle += "justify-content: flex-start;";
564
- break;
565
- case "CENTER":
566
- verticalAlignWrapperStyle += "justify-content: center;";
567
- break;
568
- case "BOTTOM":
569
- verticalAlignWrapperStyle += "justify-content: flex-end;";
570
- break;
571
- }
572
- }
573
- let letterSpacingStyle = "";
574
- if (letterSpacing && letterSpacing.value !== 0) {
575
- const unit = letterSpacing.unit === "PIXELS" ? "px" : "%";
576
- letterSpacingStyle = `letter-spacing: ${letterSpacing.value}${unit};`;
577
- }
578
- let lineHeightStyle = "";
579
- if (lineHeight) {
580
- if (lineHeight.unit === "AUTO") {
581
- lineHeightStyle = "line-height: normal;";
582
- } else {
583
- const unit = lineHeight.unit === "PIXELS" ? "px" : "%";
584
- lineHeightStyle = `line-height: ${lineHeight.value}${unit};`;
585
- }
586
- }
587
- let textDecorationStyle = "";
588
- if (textDecoration === "UNDERLINE") {
589
- textDecorationStyle = "text-decoration: underline;";
590
- } else if (textDecoration === "STRIKETHROUGH") {
591
- textDecorationStyle = "text-decoration: line-through;";
592
- }
593
- let textTransformStyle = "";
594
- if (textCase && textCase !== "ORIGINAL") {
595
- switch (textCase) {
596
- case "UPPER":
597
- textTransformStyle = "text-transform: uppercase;";
598
- break;
599
- case "LOWER":
600
- textTransformStyle = "text-transform: lowercase;";
601
- break;
602
- case "TITLE":
603
- textTransformStyle = "text-transform: capitalize;";
604
- break;
605
- }
606
- }
607
- let truncationStyle = "";
608
- if (textTruncation === "ENDING") {
609
- if (maxLines && maxLines > 1) {
610
- truncationStyle = `display: -webkit-box; -webkit-line-clamp: ${maxLines}; -webkit-box-orient: vertical; overflow: hidden;`;
611
- } else {
612
- truncationStyle = "white-space: nowrap; overflow: hidden; text-overflow: ellipsis;";
613
- }
614
- }
615
- if (maxWidth && maxWidth > 0) {
616
- truncationStyle += `max-width: ${maxWidth}px;`;
617
- }
618
- const positionStyle = getPositionStyle(node);
619
- let sizeStyles = getNodeSizeStyles(node);
620
- const commonStyles = getCommonStyles(node);
621
- if (textAutoResize === "HEIGHT") {
622
- sizeStyles = sizeStyles.replace(/height: [^;]+;/, "height: auto;");
623
- }
624
- const textStyles = `${fontFamilyStyle}${fontSizeStyle}${fontWeightStyle}${colorStyle}${textAlignStyle}${letterSpacingStyle}${lineHeightStyle}${textDecorationStyle}${textTransformStyle}${truncationStyle}`;
625
- let htmlContent = "";
626
- if (dynamicContent) {
627
- htmlContent = dynamicContent;
628
- } else if (hyperlinks && hyperlinks.length > 0) {
629
- let lastEnd = 0;
630
- for (const link of hyperlinks) {
631
- if (link.start > lastEnd) {
632
- const beforeText = characters.substring(lastEnd, link.start);
633
- htmlContent += escapeHtmlContent(beforeText).replace(/\n/g, "<br>");
634
- }
635
- const linkText = characters.substring(link.start, link.end + 1);
636
- htmlContent += `<a href="${escapeHtmlContent(link.url)}" style="color: inherit;">${escapeHtmlContent(linkText).replace(/\n/g, "<br>")}</a>`;
637
- lastEnd = link.end + 1;
638
- }
639
- if (lastEnd < characters.length) {
640
- const afterText = characters.substring(lastEnd);
641
- htmlContent += escapeHtmlContent(afterText).replace(/\n/g, "<br>");
642
- }
643
- } else {
644
- htmlContent = escapeHtmlContent(characters).replace(/\n/g, "<br>");
645
- }
646
- const childIndent = indent + " ";
647
- const innerIndent = indent + " ";
648
- const styleAttr = `${positionStyle}${sizeStyles}${commonStyles}${textStyles}`;
649
- const refString = refAttr || "";
650
- const attrsString = attributesHtml || "";
651
- if (verticalAlignWrapperStyle) {
652
- return `${indent}<div data-figma-id="${id}"${refString}${attrsString} style="${styleAttr}${verticalAlignWrapperStyle}">
653
- ${childIndent}<div style="${textStyles}">
654
- ${innerIndent}${htmlContent}
655
- ${childIndent}</div>
656
- ${indent}</div>
657
- `;
658
- } else {
659
- return `${indent}<div data-figma-id="${id}"${refString}${attrsString} style="${styleAttr}">${htmlContent}</div>
660
- `;
661
- }
662
- }
663
- function convertImageNodeToHtml(node, indent, srcBinding, altBinding, refAttr, staticImageUrl) {
664
- const { name, id } = node;
665
- const positionStyle = getPositionStyle(node);
666
- const commonStyles = getCommonStyles(node);
667
- const borderRadius = getBorderRadius(node);
668
- const sizeStyles = getNodeSizeStyles(node);
669
- const styles = `${positionStyle}${sizeStyles}${borderRadius}${commonStyles}`.trim();
670
- let src = "";
671
- if (srcBinding) {
672
- src = srcBinding;
673
- } else if (staticImageUrl) {
674
- src = staticImageUrl;
675
- } else {
676
- src = "/placeholder-image.png";
677
- console.warn(`Image node "${name}" (${id}) has no src binding or static image`);
678
- }
679
- const alt = altBinding || name;
680
- const refAttribute = refAttr || "";
681
- const styleAttribute = styles ? ` style="${styles}"` : "";
682
- const dataAttribute = ` data-figma-id="${id}"`;
683
- return `${indent}<img${dataAttribute}${refAttribute} src="${src}" alt="${alt}"${styleAttribute} />
684
- `;
685
- }
686
- function extractStaticImageUrl(node) {
687
- if (!node.fills || !Array.isArray(node.fills)) {
688
- return void 0;
689
- }
690
- for (const fill of node.fills) {
691
- if (fill.visible !== false && fill.type === "IMAGE") {
692
- if (fill.imageUrl) {
693
- return fill.imageUrl;
694
- }
695
- if (fill.imageHash) {
696
- console.warn(
697
- `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.`
698
- );
699
- }
700
- return void 0;
701
- }
702
- }
703
- return void 0;
704
- }
705
- function convertRectangleToHtml(node, indent) {
706
- const { id } = node;
707
- const positionStyle = getPositionStyle(node);
708
- const sizeStyles = getNodeSizeStyles(node);
709
- const commonStyles = getCommonStyles(node);
710
- const backgroundStyle = getBackgroundFillsStyle(node);
711
- const borderRadius = getBorderRadius(node);
712
- const strokeStyles = getStrokeStyles(node);
713
- const allStyles = `${positionStyle}${sizeStyles}${backgroundStyle}${strokeStyles}${borderRadius}${commonStyles}box-sizing: border-box;`;
714
- return `${indent}<div data-figma-id="${id}" style="${allStyles}"></div>
715
- `;
716
- }
717
- function convertEllipseToHtml(node, indent) {
718
- const { id } = node;
719
- const positionStyle = getPositionStyle(node);
720
- const sizeStyles = getNodeSizeStyles(node);
721
- const commonStyles = getCommonStyles(node);
722
- const backgroundStyle = getBackgroundFillsStyle(node);
723
- const strokeStyles = getStrokeStyles(node);
724
- const borderRadius = "border-radius: 50%;";
725
- const allStyles = `${positionStyle}${sizeStyles}${backgroundStyle}${strokeStyles}${borderRadius}${commonStyles}box-sizing: border-box;`;
726
- return `${indent}<div data-figma-id="${id}" style="${allStyles}"></div>
727
- `;
728
- }
729
- function convertVectorToHtml(node, indent) {
730
- const { id, name, svgContent, svgExportFailed, width, height } = node;
731
- const positionStyle = getPositionStyle(node);
732
- const sizeStyles = getNodeSizeStyles(node);
733
- const commonStyles = getCommonStyles(node);
734
- let finalSvgContent;
735
- if (svgExportFailed || !svgContent) {
736
- 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>`;
737
- } else {
738
- finalSvgContent = svgContent;
739
- }
740
- const allStyles = `${positionStyle}${sizeStyles}${commonStyles}box-sizing: border-box;`;
741
- const childIndent = indent + " ";
742
- return `${indent}<div data-figma-id="${id}" data-figma-type="vector" style="${allStyles}">
743
- ${childIndent}${finalSvgContent}
744
- ${indent}</div>
745
- `;
746
- }
747
- function getComponentVariantValues(node, propertyBindings) {
748
- const values = /* @__PURE__ */ new Map();
749
- const filterPseudoVariants = (variantValues) => {
750
- return variantValues.filter((value) => !value.includes(":"));
751
- };
752
- if (node.componentPropertyDefinitions) {
753
- for (const binding of propertyBindings) {
754
- const propDef = node.componentPropertyDefinitions[binding.property];
755
- if (propDef && propDef.type === "VARIANT" && propDef.variantOptions) {
756
- const filtered = filterPseudoVariants(propDef.variantOptions);
757
- if (filtered.length > 0) {
758
- values.set(binding.property, filtered);
759
- }
760
- }
761
- }
762
- }
763
- if (values.size === 0 && node.variants && node.variants.length > 0) {
764
- const propertyValuesMap = /* @__PURE__ */ new Map();
765
- for (const variant of node.variants) {
766
- if (variant.variantProperties) {
767
- for (const binding of propertyBindings) {
768
- const propValue = variant.variantProperties[binding.property];
769
- if (propValue && !propValue.includes(":")) {
770
- if (!propertyValuesMap.has(binding.property)) {
771
- propertyValuesMap.set(binding.property, /* @__PURE__ */ new Set());
772
- }
773
- propertyValuesMap.get(binding.property).add(propValue);
774
- }
775
- }
776
- }
777
- }
778
- for (const [prop, valueSet] of propertyValuesMap) {
779
- if (valueSet.size > 0) {
780
- values.set(prop, Array.from(valueSet));
781
- }
782
- }
783
- }
784
- return values;
785
- }
786
- function isBooleanVariant(values, contractTag) {
787
- if (contractTag.dataType !== "boolean") {
788
- return false;
789
- }
790
- if (values.length !== 2) {
791
- return false;
792
- }
793
- const sortedValues = [...values].sort();
794
- return sortedValues[0] === "false" && sortedValues[1] === "true";
795
- }
796
- function generatePermutations(propertyValues, bindings) {
797
- const properties = Array.from(propertyValues.entries());
798
- if (properties.length === 0) {
799
- return [];
800
- }
801
- const permutations = [];
802
- function generate(index, current) {
803
- if (index === properties.length) {
804
- permutations.push([...current]);
805
- return;
806
- }
807
- const [propName, propValues] = properties[index];
808
- const binding = bindings.find((b) => b.property === propName);
809
- if (!binding)
810
- return;
811
- const isBoolean = isBooleanVariant(propValues, binding.contractTag);
812
- for (const value of propValues) {
813
- current.push({ property: propName, tagPath: binding.tagPath, value, isBoolean });
814
- generate(index + 1, current);
815
- current.pop();
816
- }
817
- }
818
- generate(0, []);
819
- return permutations;
820
- }
821
- function findComponentVariant(node, permutation) {
822
- if (!node.variants || node.variants.length === 0) {
823
- throw new Error(
824
- `Node "${node.name}" has no variants array - cannot find variant component`
825
- );
826
- }
827
- const targetProps = /* @__PURE__ */ new Map();
828
- for (const { property, value } of permutation) {
829
- targetProps.set(property, value);
830
- }
831
- const matchingVariant = node.variants.find((variant) => {
832
- if (!variant.variantProperties) {
833
- return false;
834
- }
835
- for (const [prop, value] of targetProps) {
836
- if (variant.variantProperties[prop] !== value) {
837
- return false;
838
- }
839
- }
840
- for (const [prop, value] of Object.entries(variant.variantProperties)) {
841
- if (targetProps.has(prop) && targetProps.get(prop) !== value) {
842
- return false;
843
- }
844
- }
845
- return true;
846
- });
847
- if (!matchingVariant) {
848
- console.log(
849
- `No matching variant found for "${node.name}" with properties:`,
850
- Object.fromEntries(targetProps),
851
- "\nAvailable variants:",
852
- node.variants.map((v) => v.variantProperties),
853
- "\nUsing first variant as fallback"
854
- );
855
- return node.variants[0] || node;
856
- }
857
- return matchingVariant;
858
- }
859
- function buildVariantCondition(permutation) {
860
- const conditions = permutation.map(({ tagPath, value, isBoolean }) => {
861
- if (isBoolean) {
862
- if (value === "true") {
863
- return tagPath;
864
- } else {
865
- return `!${tagPath}`;
866
- }
867
- } else {
868
- return `${tagPath} == ${value}`;
869
- }
870
- });
871
- return conditions.join(" && ");
872
- }
873
- function convertVariantNode(node, analysis, context, convertNodeToJayHtml2) {
874
- const indent = " ".repeat(context.indentLevel);
875
- const innerIndent = " ".repeat(context.indentLevel + 1);
876
- const propertyValues = getComponentVariantValues(node, analysis.propertyBindings);
877
- const permutations = generatePermutations(propertyValues, analysis.propertyBindings);
878
- if (permutations.length === 0) {
879
- throw new Error(
880
- `No permutations generated for variant node "${node.name}" - check property definitions`
881
- );
882
- }
883
- let variantHtml = "";
884
- for (const permutation of permutations) {
885
- const conditions = buildVariantCondition(permutation);
886
- const variantNode = findComponentVariant(node, permutation);
887
- variantHtml += `${innerIndent}<div if="${conditions}">
888
- `;
889
- const variantContext = {
890
- ...context,
891
- indentLevel: context.indentLevel + 2
892
- // +2 because we're inside wrapper and if div
893
- };
894
- if (variantNode.children && variantNode.children.length > 0) {
895
- for (const child of variantNode.children) {
896
- variantHtml += convertNodeToJayHtml2(child, variantContext);
897
- }
898
- }
899
- variantHtml += `${innerIndent}</div>
900
- `;
901
- }
902
- const positionStyle = getPositionStyle(node);
903
- const frameSizeStyles = getFrameSizeStyles(node);
904
- const backgroundStyle = getBackgroundFillsStyle(node);
905
- const borderRadius = getBorderRadius(node);
906
- const strokeStyles = getStrokeStyles(node);
907
- const flexStyles = getAutoLayoutStyles(node);
908
- const overflowStyles = getOverflowStyles(node);
909
- const commonStyles = getCommonStyles(node);
910
- const wrapperStyleAttr = `${positionStyle}${frameSizeStyles}${backgroundStyle}${strokeStyles}${borderRadius}${overflowStyles}${commonStyles}${flexStyles}box-sizing: border-box;`;
911
- let refAttr = "";
912
- if (analysis.refPath) {
913
- refAttr = ` ref="${analysis.refPath}"`;
914
- } else if (analysis.dualPath) {
915
- refAttr = ` ref="${analysis.dualPath}"`;
916
- } else if (analysis.interactiveVariantPath) {
917
- refAttr = ` ref="${analysis.interactiveVariantPath}"`;
918
- }
919
- return `${indent}<div id="${node.id}" data-figma-id="${node.id}" data-figma-type="variant-container"${refAttr} style="${wrapperStyleAttr}">
920
- ` + variantHtml + `${indent}</div>
921
- `;
922
- }
923
- function convertRepeaterNode(node, analysis, context, convertNodeToJayHtml2) {
924
- const { repeaterPath, trackByKey } = analysis;
925
- const indent = " ".repeat(context.indentLevel);
926
- const innerIndent = " ".repeat(context.indentLevel + 1);
927
- if (node.type !== "FRAME") {
928
- throw new Error(`Repeater node "${node.name}" must be a FRAME (got: ${node.type})`);
929
- }
930
- if (!node.layoutMode || node.layoutMode === "NONE") {
931
- throw new Error(
932
- `Repeater node "${node.name}" must have auto-layout (HORIZONTAL or VERTICAL)`
933
- );
934
- }
935
- const positionStyle = getPositionStyle(node);
936
- const frameSizeStyles = getFrameSizeStyles(node);
937
- const backgroundStyle = getBackgroundFillsStyle(node);
938
- const borderRadius = getBorderRadius(node);
939
- const strokeStyles = getStrokeStyles(node);
940
- const flexStyles = getAutoLayoutStyles(node);
941
- const overflowStyles = getOverflowStyles(node);
942
- const commonStyles = getCommonStyles(node);
943
- const outerStyleAttr = `${positionStyle}${frameSizeStyles}${backgroundStyle}${strokeStyles}${borderRadius}${overflowStyles}${commonStyles}${flexStyles}box-sizing: border-box;`;
944
- let innerDivSizeStyles = "";
945
- if (node.layoutWrap === "WRAP") {
946
- innerDivSizeStyles = "width: fit-content; height: fit-content;";
947
- } else if (node.layoutMode === "HORIZONTAL") {
948
- innerDivSizeStyles = "height: 100%;";
949
- } else if (node.layoutMode === "VERTICAL") {
950
- innerDivSizeStyles = "width: 100%;";
951
- }
952
- let html = `${indent}<div id="${node.id}" data-figma-id="${node.id}" data-figma-type="frame-repeater" style="${outerStyleAttr}">
953
- `;
954
- html += `${innerIndent}<div style="position: relative; ${innerDivSizeStyles}" forEach="${repeaterPath}" trackBy="${trackByKey}">
955
- `;
956
- const newContext = {
957
- ...context,
958
- repeaterPathStack: [...context.repeaterPathStack, repeaterPath.split(".")],
959
- indentLevel: context.indentLevel + 2
960
- // +2 because we're inside both divs
961
- };
962
- if (node.children && node.children.length > 0) {
963
- html += convertNodeToJayHtml2(node.children[0], newContext);
964
- } else {
965
- throw new Error(
966
- `Repeater node "${node.name}" has no children - repeater template is required`
967
- );
968
- }
969
- html += `${innerIndent}</div>
970
- `;
971
- html += `${indent}</div>
972
- `;
973
- return html;
974
- }
975
- function convertGroupNode(node, analysis, context, convertNodeToJayHtml2) {
976
- const indent = " ".repeat(context.indentLevel);
977
- const positionStyle = getPositionStyle(node);
978
- const sizeStyles = getNodeSizeStyles(node);
979
- const commonStyles = getCommonStyles(node);
980
- const styleAttr = `${positionStyle}${sizeStyles}${commonStyles}box-sizing: border-box;`;
981
- let refAttr = "";
982
- if (analysis.refPath) {
983
- refAttr = ` ref="${analysis.refPath}"`;
984
- } else if (analysis.dualPath) {
985
- refAttr = ` ref="${analysis.dualPath}"`;
986
- }
987
- let htmlAttrs = `id="${node.id}" data-figma-id="${node.id}" data-figma-type="group"${refAttr} style="${styleAttr}"`;
988
- for (const [attr, tagPath] of analysis.attributes) {
989
- htmlAttrs += ` ${attr}="{${tagPath}}"`;
990
- }
991
- let html = `${indent}<div ${htmlAttrs}>
992
- `;
993
- const childContext = {
994
- ...context,
995
- indentLevel: context.indentLevel + 1
996
- };
997
- if (node.children && node.children.length > 0) {
998
- for (const child of node.children) {
999
- html += convertNodeToJayHtml2(child, childContext);
1000
- }
1001
- }
1002
- html += `${indent}</div>
1003
- `;
1004
- return html;
1005
- }
1006
- function findContractTag(tags, tagPath) {
1007
- if (tagPath.length === 0) {
1008
- return void 0;
1009
- }
1010
- const tag = tags.find((t) => t.tag === tagPath[0]);
1011
- if (!tag) {
1012
- return void 0;
1013
- }
1014
- if (tagPath.length === 1) {
1015
- return tag;
1016
- }
1017
- if (!tag.tags || tag.tags.length === 0) {
1018
- return void 0;
1019
- }
1020
- return findContractTag(tag.tags, tagPath.slice(1));
1021
- }
1022
- function findPlugin(plugins, pluginName) {
1023
- return plugins.find((p) => p.name === pluginName);
1024
- }
1025
- function findPluginContract(plugin, componentName) {
1026
- const contract = plugin.contracts.find((c) => c.name === componentName);
1027
- return contract ? { tags: contract.tags } : void 0;
1028
- }
1029
- function findPageContract(projectPage) {
1030
- return projectPage.contract ? { tags: projectPage.contract.tags } : void 0;
1031
- }
1032
- function isDataTag(contractTag) {
1033
- if (Array.isArray(contractTag.type)) {
1034
- return contractTag.type.includes("data");
1035
- }
1036
- return contractTag.type === "data";
1037
- }
1038
- function isInteractiveTag(contractTag) {
1039
- if (Array.isArray(contractTag.type)) {
1040
- return contractTag.type.includes("interactive");
1041
- }
1042
- return contractTag.type === "interactive";
1043
- }
1044
- function isDualTag(contractTag) {
1045
- if (Array.isArray(contractTag.type)) {
1046
- return contractTag.type.includes("data") && contractTag.type.includes("interactive");
1047
- }
1048
- return false;
1049
- }
1050
- function isRepeaterTag(contractTag) {
1051
- return contractTag.type === "subContract" && contractTag.repeated === true;
1052
- }
1053
- function applyRepeaterContext(path2, repeaterStack) {
1054
- for (const repeaterPath of repeaterStack) {
1055
- const prefix = repeaterPath.join(".") + ".";
1056
- if (path2.startsWith(prefix)) {
1057
- path2 = path2.substring(prefix.length);
1058
- }
1059
- }
1060
- return path2;
1061
- }
1062
- function resolveBinding(binding, context) {
1063
- let contract;
1064
- let key;
1065
- let tagPathWithoutKey;
1066
- if (binding.pageContractPath.pluginName && binding.pageContractPath.componentName) {
1067
- const plugin = findPlugin(context.plugins, binding.pageContractPath.pluginName);
1068
- if (!plugin) {
1069
- throw new Error(`Plugin not found: ${binding.pageContractPath.pluginName}`);
1070
- }
1071
- contract = findPluginContract(plugin, binding.pageContractPath.componentName);
1072
- if (!contract) {
1073
- throw new Error(
1074
- `Contract not found in plugin ${binding.pageContractPath.pluginName}: ${binding.pageContractPath.componentName}`
1075
- );
1076
- }
1077
- const usedComponent = context.projectPage.usedComponents?.find(
1078
- (c) => c.componentName === binding.pageContractPath.componentName
1079
- );
1080
- if (!usedComponent) {
1081
- throw new Error(
1082
- `Used component not found in page: ${binding.pageContractPath.componentName}`
1083
- );
1084
- }
1085
- key = usedComponent.key;
1086
- tagPathWithoutKey = binding.tagPath.slice(1);
1087
- } else {
1088
- contract = findPageContract(context.projectPage);
1089
- if (!contract) {
1090
- throw new Error(`Page contract not found for page ${context.projectPage.url}`);
1091
- }
1092
- tagPathWithoutKey = binding.tagPath;
1093
- }
1094
- const contractTag = findContractTag(contract.tags, tagPathWithoutKey);
1095
- if (!contractTag) {
1096
- throw new Error(`Contract tag not found: ${tagPathWithoutKey.join(".")} in contract`);
1097
- }
1098
- let fullPath;
1099
- if (key) {
1100
- fullPath = [key, ...tagPathWithoutKey].join(".");
1101
- } else {
1102
- fullPath = binding.tagPath.join(".");
1103
- }
1104
- fullPath = applyRepeaterContext(fullPath, context.repeaterPathStack);
1105
- return { fullPath, contractTag };
1106
- }
1107
- function getBindingsData(node) {
1108
- const bindingsDataRaw = node.pluginData?.["jay-layer-bindings"];
1109
- if (bindingsDataRaw) {
1110
- try {
1111
- return JSON.parse(bindingsDataRaw);
1112
- } catch (error) {
1113
- console.warn(`Failed to parse bindings data for node ${node.name}:`, error);
1114
- return [];
1115
- }
1116
- }
1117
- return [];
1118
- }
1119
- function analyzeBindings(bindings, context) {
1120
- const analysis = {
1121
- type: "none",
1122
- attributes: /* @__PURE__ */ new Map(),
1123
- propertyBindings: [],
1124
- isRepeater: false
1125
- };
1126
- if (bindings.length === 0) {
1127
- return analysis;
1128
- }
1129
- const resolved = bindings.map((b) => ({
1130
- binding: b,
1131
- ...resolveBinding(b, context)
1132
- })).filter((r) => r.fullPath && r.contractTag);
1133
- if (resolved.length === 0) {
1134
- return analysis;
1135
- }
1136
- const repeaterBinding = resolved.find((r) => isRepeaterTag(r.contractTag));
1137
- if (repeaterBinding) {
1138
- analysis.type = "repeater";
1139
- analysis.isRepeater = true;
1140
- analysis.repeaterPath = repeaterBinding.fullPath;
1141
- analysis.repeaterTag = repeaterBinding.contractTag;
1142
- analysis.trackByKey = repeaterBinding.contractTag.trackBy || "id";
1143
- return analysis;
1144
- }
1145
- const propertyBindings = resolved.filter((r) => r.binding.property);
1146
- if (propertyBindings.length > 0) {
1147
- if (propertyBindings.length !== resolved.length) {
1148
- throw new Error(`Node has mixed property and non-property bindings - this is invalid`);
1149
- }
1150
- analysis.type = "property-variant";
1151
- analysis.propertyBindings = propertyBindings.map((r) => ({
1152
- property: r.binding.property,
1153
- tagPath: r.fullPath,
1154
- contractTag: r.contractTag
1155
- }));
1156
- for (const r of propertyBindings) {
1157
- if (isInteractiveTag(r.contractTag)) {
1158
- analysis.interactiveVariantPath = r.fullPath;
1159
- break;
1160
- }
1161
- }
1162
- return analysis;
1163
- }
1164
- const attributeBindings = resolved.filter((r) => r.binding.attribute);
1165
- if (attributeBindings.length > 0) {
1166
- analysis.type = "attribute";
1167
- for (const r of attributeBindings) {
1168
- analysis.attributes.set(r.binding.attribute, r.fullPath);
1169
- }
1170
- }
1171
- const contentBindings = resolved.filter((r) => !r.binding.attribute && !r.binding.property);
1172
- if (contentBindings.length > 0) {
1173
- const binding = contentBindings[0];
1174
- if (isDualTag(binding.contractTag)) {
1175
- analysis.type = "dual";
1176
- analysis.dualPath = binding.fullPath;
1177
- } else if (isInteractiveTag(binding.contractTag)) {
1178
- analysis.type = "interactive";
1179
- analysis.refPath = binding.fullPath;
1180
- } else if (isDataTag(binding.contractTag)) {
1181
- if (analysis.type === "attribute") {
1182
- analysis.dynamicContentPath = binding.fullPath;
1183
- analysis.dynamicContentTag = binding.contractTag;
1184
- } else {
1185
- analysis.type = "dynamic-content";
1186
- analysis.dynamicContentPath = binding.fullPath;
1187
- analysis.dynamicContentTag = binding.contractTag;
1188
- }
1189
- }
1190
- }
1191
- return analysis;
1192
- }
1193
- function validateBindings(analysis, node) {
1194
- if (analysis.type === "property-variant" && analysis.attributes.size > 0) {
1195
- throw new Error(
1196
- `Node "${node.name}" has both property and attribute bindings - this is invalid`
1197
- );
1198
- }
1199
- if (analysis.type === "interactive" && analysis.attributes.size > 0) {
1200
- throw new Error(
1201
- `Node "${node.name}" has interactive binding with attributes - this is invalid`
1202
- );
1203
- }
1204
- }
1205
- function convertRegularNode(node, analysis, context) {
1206
- const indent = " ".repeat(context.indentLevel);
1207
- const { type, children, pluginData } = node;
1208
- const semanticHtml = pluginData?.["semanticHtml"];
1209
- if (type === "TEXT") {
1210
- const dynamicContent = analysis.dynamicContentPath ? `{${analysis.dynamicContentPath}}` : "";
1211
- const refAttr = analysis.refPath ? ` ref="${analysis.refPath}"` : "";
1212
- const dualContent = analysis.dualPath ? `{${analysis.dualPath}}` : "";
1213
- const dualRef = analysis.dualPath ? ` ref="${analysis.dualPath}"` : "";
1214
- let attributesHtml = "";
1215
- for (const [attr, tagPath] of analysis.attributes) {
1216
- attributesHtml += ` ${attr}="{${tagPath}}"`;
1217
- }
1218
- return convertTextNodeToHtml(
1219
- node,
1220
- indent,
1221
- dynamicContent || dualContent,
1222
- refAttr || dualRef,
1223
- attributesHtml
1224
- );
1225
- }
1226
- if (semanticHtml === "img") {
1227
- let srcBinding;
1228
- let altBinding;
1229
- for (const [attr, tagPath] of analysis.attributes) {
1230
- if (attr === "src") {
1231
- srcBinding = `{${tagPath}}`;
1232
- } else if (attr === "alt") {
1233
- altBinding = `{${tagPath}}`;
1234
- }
1235
- }
1236
- let staticImageUrl;
1237
- if (!srcBinding) {
1238
- staticImageUrl = extractStaticImageUrl(node);
1239
- }
1240
- const refAttr = analysis.refPath ? ` ref="${analysis.refPath}"` : analysis.dualPath ? ` ref="${analysis.dualPath}"` : "";
1241
- return convertImageNodeToHtml(
1242
- node,
1243
- indent,
1244
- srcBinding,
1245
- altBinding,
1246
- refAttr,
1247
- staticImageUrl
1248
- );
1249
- }
1250
- const positionStyle = getPositionStyle(node);
1251
- const sizeStyles = getNodeSizeStyles(node);
1252
- const commonStyles = getCommonStyles(node);
1253
- let styleAttr = "";
1254
- if (type === "FRAME") {
1255
- const backgroundStyle = getBackgroundFillsStyle(node);
1256
- const borderRadius = getBorderRadius(node);
1257
- const strokeStyles = getStrokeStyles(node);
1258
- const flexStyles = getAutoLayoutStyles(node);
1259
- const overflowStyles = getOverflowStyles(node);
1260
- const frameSizeStyles = getFrameSizeStyles(node);
1261
- styleAttr = `${positionStyle}${frameSizeStyles}${backgroundStyle}${strokeStyles}${borderRadius}${overflowStyles}${commonStyles}${flexStyles}box-sizing: border-box;`;
1262
- } else {
1263
- styleAttr = `${positionStyle}${sizeStyles}${commonStyles}`;
1264
- }
1265
- const tag = semanticHtml || "div";
1266
- let htmlAttrs = `data-figma-id="${node.id}" data-figma-type="${type.toLowerCase()}" style="${styleAttr}"`;
1267
- if (analysis.refPath) {
1268
- htmlAttrs += ` ref="${analysis.refPath}"`;
1269
- } else if (analysis.dualPath) {
1270
- htmlAttrs += ` ref="${analysis.dualPath}"`;
1271
- }
1272
- for (const [attr, tagPath] of analysis.attributes) {
1273
- htmlAttrs += ` ${attr}="{${tagPath}}"`;
1274
- }
1275
- if (type === "RECTANGLE") {
1276
- return convertRectangleToHtml(node, indent);
1277
- } else if (type === "ELLIPSE") {
1278
- return convertEllipseToHtml(node, indent);
1279
- } else if (type === "GROUP") {
1280
- return convertGroupNode(node, analysis, context, convertNodeToJayHtml);
1281
- } else if (type === "VECTOR" || type === "STAR" || type === "POLYGON" || type === "LINE" || type === "BOOLEAN_OPERATION") {
1282
- return convertVectorToHtml(node, indent);
1283
- } else if (children && children.length > 0) {
1284
- let html = `${indent}<${tag} ${htmlAttrs}>
1285
- `;
1286
- html += `${indent} <!-- ${node.name} -->
1287
- `;
1288
- const childContext = {
1289
- ...context,
1290
- indentLevel: context.indentLevel + 1
1291
- };
1292
- for (const child of children) {
1293
- html += convertNodeToJayHtml(child, childContext);
1294
- }
1295
- html += `${indent}</${tag}>
1296
- `;
1297
- return html;
1298
- } else {
1299
- return `${indent}<!-- ${node.name} (${type}) -->
1300
- `;
1301
- }
1302
- }
1303
- function convertNodeToJayHtml(node, context) {
1304
- const { name, type, children, pluginData } = node;
1305
- const isJPage = pluginData?.["jpage"] === "true";
1306
- const urlRoute = pluginData?.["urlRoute"];
1307
- if (type === "TEXT" && node.fontName) {
1308
- if (typeof node.fontName === "object" && node.fontName.family) {
1309
- context.fontFamilies.add(node.fontName.family);
1310
- }
1311
- }
1312
- const indent = " ".repeat(context.indentLevel);
1313
- if (type === "SECTION" && isJPage) {
1314
- let html = `${indent}<section data-figma-id="${node.id}" data-page-url="${urlRoute || ""}">
1315
- `;
1316
- html += `${indent} <!-- Jay Page: ${name} -->
1317
- `;
1318
- if (children && children.length > 0) {
1319
- const childContext = {
1320
- ...context,
1321
- indentLevel: context.indentLevel + 1
1322
- };
1323
- for (const child of children) {
1324
- html += convertNodeToJayHtml(child, childContext);
1325
- }
1326
- }
1327
- html += `${indent}</section>
1328
- `;
1329
- return html;
1330
- }
1331
- const bindings = getBindingsData(node);
1332
- const analysis = analyzeBindings(bindings, context);
1333
- validateBindings(analysis, node);
1334
- if (analysis.isRepeater) {
1335
- return convertRepeaterNode(node, analysis, context, convertNodeToJayHtml);
1336
- }
1337
- if (analysis.type === "property-variant") {
1338
- return convertVariantNode(node, analysis, context, convertNodeToJayHtml);
1339
- }
1340
- return convertRegularNode(node, analysis, context);
1341
- }
1342
- function findContentFrame(section) {
1343
- if (!section.children || section.children.length === 0) {
1344
- return {
1345
- frame: null,
1346
- error: `Jay Page section "${section.name}" has no children`
1347
- };
1348
- }
1349
- const frameNodes = section.children.filter((child) => child.type === "FRAME");
1350
- if (frameNodes.length === 0) {
1351
- return {
1352
- frame: null,
1353
- error: `Jay Page section "${section.name}" has no FrameNode children. Found: ${section.children.map((c) => c.type).join(", ")}`
1354
- };
1355
- }
1356
- if (frameNodes.length > 1) {
1357
- return {
1358
- frame: frameNodes[0],
1359
- warning: `Jay Page section "${section.name}" has ${frameNodes.length} FrameNodes, using the first one`
1360
- };
1361
- }
1362
- return { frame: frameNodes[0] };
1363
- }
1364
- const figmaVendor = {
1365
- vendorId: "figma",
1366
- async convertToBodyHtml(vendorDoc, pageUrl, projectPage, plugins) {
1367
- console.log(`šŸŽØ Converting Figma document for page: ${pageUrl}`);
1368
- console.log(` Document type: ${vendorDoc.type}, name: ${vendorDoc.name}`);
1369
- const isJPage = vendorDoc.pluginData?.["jpage"] === "true";
1370
- if (!isJPage) {
1371
- throw new Error(
1372
- `Document "${vendorDoc.name}" is not marked as a Jay Page (missing jpage='true' in pluginData)`
1373
- );
1374
- }
1375
- const { frame, error, warning } = findContentFrame(vendorDoc);
1376
- if (error) {
1377
- throw new Error(`Cannot convert to Jay HTML: ${error}`);
1378
- }
1379
- if (warning) {
1380
- console.warn(`āš ļø ${warning}`);
1381
- }
1382
- if (!frame) {
1383
- throw new Error(`Cannot convert to Jay HTML: No content frame found`);
1384
- }
1385
- console.log(` Converting content frame: ${frame.name} (${frame.type})`);
1386
- const fontFamilies = /* @__PURE__ */ new Set();
1387
- const context = {
1388
- repeaterPathStack: [],
1389
- indentLevel: 1,
1390
- // Start at 1 for body content
1391
- fontFamilies,
1392
- projectPage,
1393
- plugins
1394
- };
1395
- const bodyHtml = convertNodeToJayHtml(frame, context);
1396
- if (fontFamilies.size > 0) {
1397
- console.log(
1398
- ` Found ${fontFamilies.size} font families: ${Array.from(fontFamilies).join(", ")}`
1399
- );
1400
- }
1401
- return {
1402
- bodyHtml,
1403
- fontFamilies,
1404
- // No contract data for now - Figma vendor doesn't generate contracts yet
1405
- contractData: void 0
1406
- };
1407
- }
1408
- };
1409
- const vendorRegistry = /* @__PURE__ */ new Map([
1410
- [figmaVendor.vendorId, figmaVendor]
1411
- // Add more vendors here as they are contributed
1412
- ]);
1413
- function getVendor(vendorId) {
1414
- return vendorRegistry.get(vendorId);
1415
- }
1416
- function hasVendor(vendorId) {
1417
- return vendorRegistry.has(vendorId);
1418
- }
1419
- function getRegisteredVendors() {
1420
- return Array.from(vendorRegistry.keys());
1421
- }
1422
- function escapeHtml(text) {
1423
- if (!text)
1424
- return "";
1425
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
1426
- }
1427
- function generateGoogleFontsLinks(fontFamilies) {
1428
- if (fontFamilies.size === 0) {
1429
- return "";
1430
- }
1431
- const families = Array.from(fontFamilies);
1432
- const googleFontsUrl = `https://fonts.googleapis.com/css2?${families.map((family) => {
1433
- const encodedFamily = encodeURIComponent(family).replace(/%20/g, "+");
1434
- return `family=${encodedFamily}:wght@100;200;300;400;500;600;700;800;900`;
1435
- }).join("&")}&display=swap`;
1436
- return ` <link rel="preconnect" href="https://fonts.googleapis.com">
1437
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
1438
- <link href="${googleFontsUrl}" rel="stylesheet">`;
1439
- }
1440
- function generateHeadlessComponentScripts(components) {
1441
- if (components.length === 0) {
1442
- return "";
1443
- }
1444
- const scriptTags = components.map(
1445
- (comp) => ` <script
1446
- type="application/jay-headless"
1447
- plugin="${comp.plugin}"
1448
- contract="${comp.contract}"
1449
- key="${comp.key}"
1450
- ><\/script>`
1451
- );
1452
- return "\n" + scriptTags.join("\n");
1453
- }
1454
- function generateJayDataScript(contractData) {
1455
- if (contractData) {
1456
- return ` <script type="application/jay-data">
1457
- data:
1458
- ${contractData.tagsYaml}
1459
- <\/script>`;
1460
- }
1461
- return ` <script type="application/jay-data">
1462
- data:
1463
- <\/script>`;
1464
- }
1465
- function buildJayHtml(options) {
1466
- const {
1467
- bodyHtml,
1468
- fontFamilies,
1469
- contractData,
1470
- headlessComponents = [],
1471
- title = "Page"
1472
- } = options;
1473
- const fontLinks = generateGoogleFontsLinks(fontFamilies);
1474
- const headlessScripts = generateHeadlessComponentScripts(headlessComponents);
1475
- const jayDataScript = generateJayDataScript(contractData);
1476
- return `<!DOCTYPE html>
1477
- <html lang="en">
1478
- <head>
1479
- <meta charset="UTF-8">
1480
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
1481
- ${fontLinks}${headlessScripts}
1482
- ${jayDataScript}
1483
- <title>${escapeHtml(title)}</title>
1484
- <style>
1485
- /* Basic reset */
1486
- body { margin: 0; font-family: sans-serif; }
1487
- a { color: inherit; text-decoration: none; }
1488
- a:hover { text-decoration: underline; }
1489
- div { box-sizing: border-box; }
1490
-
1491
- /* Scrollbar styling for Webkit browsers */
1492
- ::-webkit-scrollbar {
1493
- width: 8px;
1494
- height: 8px;
1495
- }
1496
-
1497
- ::-webkit-scrollbar-track {
1498
- background: transparent;
1499
- }
1500
-
1501
- ::-webkit-scrollbar-thumb {
1502
- background: rgba(0, 0, 0, 0.3);
1503
- border-radius: 4px;
1504
- }
1505
-
1506
- ::-webkit-scrollbar-thumb:hover {
1507
- background: rgba(0, 0, 0, 0.5);
1508
- }
1509
-
1510
- /* Smooth scrolling */
1511
- * {
1512
- scroll-behavior: smooth;
1513
- }
1514
- </style>
1515
- </head>
1516
- <body>
1517
- ${bodyHtml}
1518
- </body>
1519
- </html>`;
1520
- }
1521
- async function buildJayHtmlFromVendorResult(conversionResult, pageDirectory, pageTitle) {
1522
- const pageConfigPath = path.join(pageDirectory, "page.conf.yaml");
1523
- const headlessComponents = [];
1524
- if (fs.existsSync(pageConfigPath)) {
1525
- try {
1526
- const configContent = await fs.promises.readFile(pageConfigPath, "utf-8");
1527
- const pageConfig = YAML.parse(configContent);
1528
- if (pageConfig.used_components && Array.isArray(pageConfig.used_components)) {
1529
- for (const comp of pageConfig.used_components) {
1530
- if (comp.plugin && comp.contract && comp.key) {
1531
- headlessComponents.push({
1532
- plugin: comp.plugin,
1533
- contract: comp.contract,
1534
- key: comp.key
1535
- });
1536
- }
1537
- }
1538
- }
1539
- } catch (configError) {
1540
- console.warn(`Failed to read page config ${pageConfigPath}:`, configError);
1541
- }
1542
- }
1543
- const title = pageTitle || path.basename(pageDirectory);
1544
- return buildJayHtml({
1545
- bodyHtml: conversionResult.bodyHtml,
1546
- fontFamilies: conversionResult.fontFamilies,
1547
- contractData: conversionResult.contractData,
1548
- headlessComponents,
1549
- title
1550
- });
1551
- }
1552
- const PAGE_FILENAME = `page${JAY_EXTENSION}`;
1553
- const PAGE_CONTRACT_FILENAME = `page${JAY_CONTRACT_EXTENSION}`;
1554
- const PAGE_CONFIG_FILENAME = "page.conf.yaml";
1555
- function pageUrlToDirectoryPath(pageUrl, pagesBasePath) {
1556
- const routePath = pageUrl === "/" ? "" : pageUrl;
1557
- const fsPath = routePath.replace(/:([^/]+)/g, "[$1]");
1558
- return path.join(pagesBasePath, fsPath);
1559
- }
1560
- function jayTypeToString(jayType) {
1561
- if (!jayType)
1562
- return void 0;
1563
- if (jayType instanceof JayAtomicType) {
1564
- return jayType.name;
1565
- } else if (jayType instanceof JayEnumType) {
1566
- return `enum (${jayType.values.join(" | ")})`;
1567
- } else {
1568
- return jayType.name || "unknown";
1569
- }
1570
- }
1571
- function convertContractTagToProtocol(tag) {
1572
- const typeArray = Array.isArray(tag.type) ? tag.type : [tag.type];
1573
- const typeStrings = typeArray.map((t) => ContractTagType[t]);
1574
- return {
1575
- tag: tag.tag,
1576
- type: typeStrings.length === 1 ? typeStrings[0] : typeStrings,
1577
- dataType: tag.dataType ? jayTypeToString(tag.dataType) : void 0,
1578
- elementType: tag.elementType ? tag.elementType.join(" | ") : void 0,
1579
- required: tag.required,
1580
- repeated: tag.repeated,
1581
- trackBy: tag.trackBy,
1582
- async: tag.async,
1583
- phase: tag.phase,
1584
- link: tag.link,
1585
- tags: tag.tags ? tag.tags.map(convertContractTagToProtocol) : void 0
1586
- };
1587
- }
1588
- function convertContractToProtocol(contract) {
1589
- return {
1590
- name: contract.name,
1591
- tags: contract.tags.map(convertContractTagToProtocol)
1592
- };
1593
- }
1594
- async function isPageDirectory(dirPath) {
1595
- const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
1596
- const hasPageHtml = entries.some((e2) => e2.name === PAGE_FILENAME);
1597
- const hasPageContract = entries.some((e2) => e2.name === PAGE_CONTRACT_FILENAME);
1598
- const hasPageConfig = entries.some((e2) => e2.name === PAGE_CONFIG_FILENAME);
1599
- const isPage = hasPageHtml || hasPageContract || hasPageConfig;
1600
- return { isPage, hasPageHtml, hasPageContract, hasPageConfig };
1601
- }
1602
- async function scanPageDirectories(pagesBasePath, onPageFound) {
1603
- async function scanDirectory(dirPath, urlPath = "") {
1604
- try {
1605
- const { isPage, hasPageHtml, hasPageContract, hasPageConfig } = await isPageDirectory(dirPath);
1606
- if (isPage) {
1607
- const pageUrl = urlPath || "/";
1608
- const pageName = dirPath === pagesBasePath ? "Home" : path.basename(dirPath);
1609
- await onPageFound({
1610
- dirPath,
1611
- pageUrl,
1612
- pageName,
1613
- hasPageHtml,
1614
- hasPageContract,
1615
- hasPageConfig
1616
- });
1617
- }
1618
- const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
1619
- for (const entry of entries) {
1620
- const fullPath = path.join(dirPath, entry.name);
1621
- if (entry.isDirectory()) {
1622
- const isParam = entry.name.startsWith("[") && entry.name.endsWith("]");
1623
- const segmentUrl = isParam ? `:${entry.name.slice(1, -1)}` : entry.name;
1624
- const newUrlPath = urlPath + "/" + segmentUrl;
1625
- await scanDirectory(fullPath, newUrlPath);
1626
- }
1627
- }
1628
- } catch (error) {
1629
- getLogger().warn(`Failed to scan directory ${dirPath}:`, error);
1630
- }
1631
- }
1632
- await scanDirectory(pagesBasePath);
1633
- }
1634
- function expandContractTags(tags, baseDir) {
1635
- const resolvedTags = [];
1636
- for (const tag of tags) {
1637
- if (tag.link) {
1638
- try {
1639
- const linkWithExtension = tag.link.endsWith(JAY_CONTRACT_EXTENSION) ? tag.link : tag.link + JAY_CONTRACT_EXTENSION;
1640
- const linkedPath = JAY_IMPORT_RESOLVER.resolveLink(baseDir, linkWithExtension);
1641
- const loadResult = JAY_IMPORT_RESOLVER.loadContract(linkedPath);
1642
- if (loadResult.val) {
1643
- const expandedSubTags = expandContractTags(
1644
- loadResult.val.tags,
1645
- path.dirname(linkedPath)
1646
- );
1647
- const resolvedTag = {
1648
- tag: tag.tag,
1649
- type: tag.type,
1650
- // Keep the original enum type
1651
- tags: expandedSubTags,
1652
- required: tag.required,
1653
- repeated: tag.repeated,
1654
- trackBy: tag.trackBy,
1655
- async: tag.async,
1656
- phase: tag.phase,
1657
- link: tag.link
1658
- };
1659
- resolvedTags.push(resolvedTag);
1660
- } else {
1661
- getLogger().warn(`Failed to load linked contract: ${tag.link} from ${baseDir}`);
1662
- resolvedTags.push(tag);
1663
- }
1664
- } catch (error) {
1665
- getLogger().warn(`Error resolving linked contract ${tag.link}:`, error);
1666
- resolvedTags.push(tag);
1667
- }
1668
- } else if (tag.tags) {
1669
- const resolvedSubTags = expandContractTags(tag.tags, baseDir);
1670
- const resolvedTag = {
1671
- ...tag,
1672
- tags: resolvedSubTags
1673
- };
1674
- resolvedTags.push(resolvedTag);
1675
- } else {
1676
- resolvedTags.push(tag);
1677
- }
1678
- }
1679
- return resolvedTags;
1680
- }
1681
- function loadAndExpandContract(contractFilePath) {
1682
- try {
1683
- const loadResult = JAY_IMPORT_RESOLVER.loadContract(contractFilePath);
1684
- if (loadResult.validations.length > 0) {
1685
- getLogger().warn(
1686
- `Contract validation errors in ${contractFilePath}:`,
1687
- loadResult.validations
1688
- );
1689
- }
1690
- if (loadResult.val) {
1691
- const resolvedTags = expandContractTags(
1692
- loadResult.val.tags,
1693
- path.dirname(contractFilePath)
1694
- );
1695
- return convertContractToProtocol({
1696
- name: loadResult.val.name,
1697
- tags: resolvedTags
1698
- });
1699
- }
1700
- } catch (error) {
1701
- getLogger().warn(`Failed to parse contract file ${contractFilePath}:`, error);
1702
- }
1703
- return null;
1704
- }
1705
- async function extractHeadlessComponentsFromJayHtml(jayHtmlContent, pageFilePath, projectRootPath) {
1706
- try {
1707
- const parsedJayHtml = await parseJayFile(
1708
- jayHtmlContent,
1709
- path.basename(pageFilePath),
1710
- path.dirname(pageFilePath),
1711
- { relativePath: "" },
1712
- // We don't need TypeScript config for headless extraction
1713
- JAY_IMPORT_RESOLVER,
1714
- projectRootPath
1715
- );
1716
- if (parsedJayHtml.validations.length > 0) {
1717
- getLogger().warn(
1718
- `Jay-HTML parsing warnings for ${pageFilePath}:`,
1719
- parsedJayHtml.validations
1720
- );
1721
- }
1722
- if (!parsedJayHtml.val) {
1723
- getLogger().warn(`Failed to parse jay-html file: ${pageFilePath}`);
1724
- return [];
1725
- }
1726
- const resolvedComponents = [];
1727
- for (const headlessImport of parsedJayHtml.val.headlessImports) {
1728
- if (headlessImport.codeLink) {
1729
- let pluginName = headlessImport.codeLink.module;
1730
- const nodeModulesMatch = pluginName.match(/node_modules\/([^/]+)/);
1731
- if (nodeModulesMatch) {
1732
- pluginName = nodeModulesMatch[1];
1733
- }
1734
- const componentName = headlessImport.contract?.name || "unknown";
1735
- resolvedComponents.push({
1736
- appName: pluginName,
1737
- componentName,
1738
- key: headlessImport.key
1739
- });
1740
- }
1741
- }
1742
- return resolvedComponents;
1743
- } catch (error) {
1744
- getLogger().warn(`Failed to parse jay-html content for ${pageFilePath}:`, error);
1745
- return [];
1746
- }
1747
- }
1748
- async function scanProjectComponents(componentsBasePath) {
1749
- const components = [];
1750
- try {
1751
- const entries = await fs.promises.readdir(componentsBasePath, { withFileTypes: true });
1752
- for (const entry of entries) {
1753
- if (entry.isFile() && entry.name.endsWith(JAY_EXTENSION)) {
1754
- const componentName = path.basename(entry.name, JAY_EXTENSION);
1755
- const componentPath = path.join(componentsBasePath, entry.name);
1756
- const contractPath = path.join(
1757
- componentsBasePath,
1758
- `${componentName}${JAY_CONTRACT_EXTENSION}`
1759
- );
1760
- const hasContract = fs.existsSync(contractPath);
1761
- components.push({
1762
- name: componentName,
1763
- filePath: componentPath,
1764
- contractPath: hasContract ? contractPath : void 0
1765
- });
1766
- }
1767
- }
1768
- } catch (error) {
1769
- getLogger().warn(`Failed to scan components directory ${componentsBasePath}:`, error);
1770
- }
1771
- return components;
1772
- }
1773
- async function getProjectName(configBasePath) {
1774
- const projectConfigPath = path.join(configBasePath, "project.conf.yaml");
1775
- try {
1776
- if (fs.existsSync(projectConfigPath)) {
1777
- const configContent = await fs.promises.readFile(projectConfigPath, "utf-8");
1778
- const projectConfig = YAML.parse(configContent);
1779
- return projectConfig.name || "Unnamed Project";
1780
- }
1781
- } catch (error) {
1782
- getLogger().warn(`Failed to read project config ${projectConfigPath}:`, error);
1783
- }
1784
- return "Unnamed Project";
1785
- }
1786
- async function scanLocalPluginNames(projectRoot) {
1787
- const plugins = [];
1788
- const localPluginsDir = path.join(projectRoot, LOCAL_PLUGIN_PATH);
1789
- if (!fs.existsSync(localPluginsDir)) {
1790
- return plugins;
1791
- }
1792
- try {
1793
- const entries = await fs.promises.readdir(localPluginsDir, { withFileTypes: true });
1794
- for (const entry of entries) {
1795
- if (entry.isDirectory()) {
1796
- const pluginDir = path.join(localPluginsDir, entry.name);
1797
- const pluginYamlPath = path.join(pluginDir, "plugin.yaml");
1798
- if (fs.existsSync(pluginYamlPath)) {
1799
- plugins.push(entry.name);
1800
- }
1801
- }
1802
- }
1803
- } catch (error) {
1804
- getLogger().warn(`Failed to scan local plugins directory ${localPluginsDir}:`, error);
1805
- }
1806
- return plugins;
1807
- }
1808
- async function findPluginNamesFromPackageJson(projectRootPath) {
1809
- const pluginNames = [];
1810
- try {
1811
- const packageJsonPath = path.join(projectRootPath, "package.json");
1812
- if (!fs.existsSync(packageJsonPath)) {
1813
- getLogger().warn("package.json not found");
1814
- return pluginNames;
1815
- }
1816
- const packageJsonContent = await fs.promises.readFile(packageJsonPath, "utf-8");
1817
- const packageJson = JSON.parse(packageJsonContent);
1818
- const workspaceDependencies = /* @__PURE__ */ new Set();
1819
- const regularDependencies = /* @__PURE__ */ new Set();
1820
- for (const [depName, version] of Object.entries({
1821
- ...packageJson.dependencies
1822
- })) {
1823
- if (typeof version === "string" && version.startsWith("workspace:")) {
1824
- workspaceDependencies.add(depName);
1825
- } else {
1826
- regularDependencies.add(depName);
1827
- }
1828
- }
1829
- const nodeModulesPath = path.join(projectRootPath, "node_modules");
1830
- for (const depName of regularDependencies) {
1831
- if (await checkPackageForPlugin(nodeModulesPath, depName)) {
1832
- pluginNames.push(depName);
1833
- }
1834
- }
1835
- if (workspaceDependencies.size > 0) {
1836
- const workspaceNodeModules = await findWorkspaceNodeModulesPath(
1837
- projectRootPath,
1838
- Array.from(workspaceDependencies)
1839
- );
1840
- if (workspaceNodeModules) {
1841
- for (const depName of workspaceDependencies) {
1842
- if (await checkPackageForPlugin(workspaceNodeModules, depName)) {
1843
- pluginNames.push(depName);
1844
- }
1845
- }
1846
- }
1847
- }
1848
- } catch (error) {
1849
- getLogger().error("Error finding plugins from package.json:", error);
1850
- }
1851
- return pluginNames;
1852
- }
1853
- async function checkPackageForPlugin(nodeModulesDir, packageName) {
1854
- try {
1855
- const packageDir = path.join(nodeModulesDir, packageName);
1856
- const pluginYamlPath = path.join(packageDir, "plugin.yaml");
1857
- return fs.existsSync(pluginYamlPath);
1858
- } catch (error) {
1859
- return false;
1860
- }
1861
- }
1862
- async function findWorkspaceNodeModulesPath(startPath, workspaceDeps) {
1863
- let currentPath = startPath;
1864
- while (currentPath !== path.dirname(currentPath)) {
1865
- const nodeModulesPath = path.join(currentPath, "node_modules");
1866
- if (fs.existsSync(nodeModulesPath)) {
1867
- for (const depName of workspaceDeps) {
1868
- const depPath = path.join(nodeModulesPath, depName);
1869
- if (fs.existsSync(depPath)) {
1870
- return nodeModulesPath;
1871
- }
1872
- }
1873
- }
1874
- currentPath = path.dirname(currentPath);
1875
- }
1876
- return null;
1877
- }
1878
- async function scanPlugins(projectRootPath) {
1879
- const plugins = [];
1880
- try {
1881
- const [localPluginNames, dependencyPluginNames] = await Promise.all([
1882
- scanLocalPluginNames(projectRootPath),
1883
- findPluginNamesFromPackageJson(projectRootPath)
1884
- ]);
1885
- const allPluginNames = [.../* @__PURE__ */ new Set([...localPluginNames, ...dependencyPluginNames])];
1886
- getLogger().info(`Found ${allPluginNames.length} plugins: ${allPluginNames.join(", ")}`);
1887
- for (const pluginName of allPluginNames) {
1888
- const manifest = resolvePluginManifest(projectRootPath, pluginName);
1889
- if (manifest.validations.length > 0) {
1890
- getLogger().warn(
1891
- `Failed to resolve plugin manifest for ${pluginName}:`,
1892
- manifest.validations
1893
- );
1894
- continue;
1895
- }
1896
- if (!manifest.val) {
1897
- getLogger().warn(
1898
- `Failed to resolve plugin manifest for ${pluginName}:`,
1899
- manifest.validations
1900
- );
1901
- continue;
1902
- }
1903
- const contracts = manifest.val.contracts;
1904
- plugins.push({
1905
- name: pluginName,
1906
- contracts: contracts.map((contract) => {
1907
- const resolveResult = JAY_IMPORT_RESOLVER.resolvePluginComponent(
1908
- pluginName,
1909
- contract.name,
1910
- projectRootPath
1911
- );
1912
- if (resolveResult.validations.length > 0) {
1913
- getLogger().warn(
1914
- `Failed to resolve plugin component for ${pluginName}:${contract.name}:`,
1915
- resolveResult.validations
1916
- );
1917
- return null;
1918
- }
1919
- if (!resolveResult.val) {
1920
- getLogger().warn(
1921
- `Failed to resolve plugin component for ${pluginName}:${contract.name}:`,
1922
- resolveResult.validations
1923
- );
1924
- return null;
1925
- }
1926
- const expandedContract = loadAndExpandContract(resolveResult.val.contractPath);
1927
- if (!expandedContract) {
1928
- return null;
1929
- }
1930
- return expandedContract;
1931
- })
1932
- });
1933
- }
1934
- } catch (error) {
1935
- getLogger().error("Error scanning plugins:", error);
1936
- }
1937
- return plugins;
1938
- }
1939
- async function loadProjectPage(pageContext, plugins) {
1940
- const { dirPath, pageUrl, pageName, hasPageHtml, hasPageContract, hasPageConfig } = pageContext;
1941
- const pageFilePath = path.join(dirPath, PAGE_FILENAME);
1942
- const pageConfigPath = path.join(dirPath, PAGE_CONFIG_FILENAME);
1943
- const contractPath = path.join(dirPath, PAGE_CONTRACT_FILENAME);
1944
- const projectRootPath = process.cwd();
1945
- let usedComponents = [];
1946
- let contract;
1947
- if (hasPageContract) {
1948
- const parsedContract = loadAndExpandContract(contractPath);
1949
- if (parsedContract) {
1950
- contract = parsedContract;
1951
- }
1952
- }
1953
- if (hasPageHtml) {
1954
- try {
1955
- const jayHtmlContent = await fs.promises.readFile(pageFilePath, "utf-8");
1956
- usedComponents = await extractHeadlessComponentsFromJayHtml(
1957
- jayHtmlContent,
1958
- pageFilePath,
1959
- projectRootPath
1960
- );
1961
- } catch (error) {
1962
- getLogger().warn(`Failed to read page file ${pageFilePath}:`, error);
1963
- }
1964
- } else if (hasPageConfig) {
1965
- try {
1966
- const configContent = await fs.promises.readFile(pageConfigPath, "utf-8");
1967
- const pageConfig = YAML.parse(configContent);
1968
- if (pageConfig.used_components && Array.isArray(pageConfig.used_components)) {
1969
- for (const comp of pageConfig.used_components) {
1970
- const key = comp.key || "";
1971
- if (comp.plugin && comp.contract) {
1972
- const plugin = plugins.find((p) => p.name === comp.plugin);
1973
- if (plugin && plugin.contracts) {
1974
- const contract2 = plugin.contracts.find((c) => c.name === comp.contract);
1975
- if (contract2) {
1976
- usedComponents.push({
1977
- appName: comp.plugin,
1978
- componentName: comp.contract,
1979
- key
1980
- });
1981
- continue;
1982
- }
1983
- }
1984
- usedComponents.push({
1985
- appName: comp.plugin,
1986
- componentName: comp.contract,
1987
- key
1988
- });
1989
- } else {
1990
- getLogger().warn(
1991
- `Invalid component definition in ${pageConfigPath}: Only plugin/contract syntax is supported for headless components. Found:`,
1992
- comp
1993
- );
1994
- }
1995
- }
1996
44
  }
1997
- } catch (error) {
1998
- getLogger().warn(`Failed to parse page config ${pageConfigPath}:`, error);
1999
- }
45
+ };
46
+ } catch (error) {
47
+ getLogger().warn(`Failed to parse .jay YAML config file, using defaults: ${error}`);
48
+ return DEFAULT_CONFIG;
2000
49
  }
2001
- return {
2002
- name: pageName,
2003
- url: pageUrl,
2004
- filePath: pageFilePath,
2005
- contract,
2006
- usedComponents
2007
- };
2008
50
  }
2009
- async function scanProjectInfo(pagesBasePath, componentsBasePath, configBasePath, projectRootPath) {
2010
- const [projectName, components, plugins] = await Promise.all([
2011
- getProjectName(configBasePath),
2012
- scanProjectComponents(componentsBasePath),
2013
- scanPlugins(projectRootPath)
2014
- ]);
2015
- const pages = [];
2016
- await scanPageDirectories(pagesBasePath, async (context) => {
2017
- const page = await loadProjectPage(context, plugins);
2018
- pages.push(page);
2019
- });
51
+ function getConfigWithDefaults(config) {
2020
52
  return {
2021
- name: projectName,
2022
- localPath: projectRootPath,
2023
- pages,
2024
- components,
2025
- plugins
53
+ devServer: {
54
+ portRange: config.devServer?.portRange || DEFAULT_CONFIG.devServer.portRange,
55
+ pagesBase: config.devServer?.pagesBase || DEFAULT_CONFIG.devServer.pagesBase,
56
+ componentsBase: config.devServer?.componentsBase || DEFAULT_CONFIG.devServer.componentsBase,
57
+ publicFolder: config.devServer?.publicFolder || DEFAULT_CONFIG.devServer.publicFolder,
58
+ configBase: config.devServer?.configBase || DEFAULT_CONFIG.devServer.configBase
59
+ }
2026
60
  };
2027
61
  }
2028
- async function handlePagePublish(resolvedConfig, page) {
2029
- try {
2030
- const pagesBasePath = path.resolve(resolvedConfig.devServer.pagesBase);
2031
- const dirname = pageUrlToDirectoryPath(page.route, pagesBasePath);
2032
- const fullPath = path.join(dirname, PAGE_FILENAME);
2033
- await fs.promises.mkdir(dirname, { recursive: true });
2034
- await fs.promises.writeFile(fullPath, page.jayHtml, "utf-8");
2035
- let contractPath;
2036
- if (page.contract) {
2037
- contractPath = path.join(dirname, `page${JAY_CONTRACT_EXTENSION}`);
2038
- await fs.promises.writeFile(contractPath, page.contract, "utf-8");
2039
- getLogger().info(`šŸ“„ Published page contract: ${contractPath}`);
2040
- }
2041
- const createdJayHtml = {
2042
- jayHtml: page.jayHtml,
2043
- filename: PAGE_FILENAME,
2044
- dirname,
2045
- fullPath
2046
- };
2047
- getLogger().info(`šŸ“ Published page: ${fullPath}`);
2048
- return [
2049
- {
2050
- success: true,
2051
- filePath: fullPath,
2052
- contractPath
2053
- },
2054
- createdJayHtml
2055
- ];
2056
- } catch (error) {
2057
- getLogger().error(`Failed to publish page ${page.route}:`, error);
2058
- return [
2059
- {
2060
- success: false,
2061
- error: error instanceof Error ? error.message : "Unknown error"
2062
- },
2063
- void 0
2064
- ];
2065
- }
2066
- }
2067
- async function handleComponentPublish(resolvedConfig, component) {
62
+ function updateConfig(updates) {
63
+ const configPath = path.resolve(".jay");
2068
64
  try {
2069
- const dirname = path.resolve(resolvedConfig.devServer.componentsBase);
2070
- const filename = `${component.name}${JAY_EXTENSION}`;
2071
- const fullPath = path.join(dirname, filename);
2072
- await fs.promises.mkdir(dirname, { recursive: true });
2073
- await fs.promises.writeFile(fullPath, component.jayHtml, "utf-8");
2074
- let contractPath;
2075
- if (component.contract) {
2076
- contractPath = path.join(dirname, `${component.name}${JAY_CONTRACT_EXTENSION}`);
2077
- await fs.promises.writeFile(contractPath, component.contract, "utf-8");
2078
- }
2079
- const createdJayHtml = {
2080
- jayHtml: component.jayHtml,
2081
- filename,
2082
- dirname,
2083
- fullPath
65
+ const existingConfig = loadConfig();
66
+ const updatedConfig = {
67
+ ...existingConfig,
68
+ ...updates,
69
+ devServer: {
70
+ ...existingConfig.devServer,
71
+ ...updates.devServer
72
+ }
2084
73
  };
2085
- getLogger().info(`🧩 Published component: ${fullPath}`);
2086
- return [
2087
- {
2088
- success: true,
2089
- filePath: fullPath,
2090
- contractPath
2091
- },
2092
- createdJayHtml
2093
- ];
74
+ const yamlContent = YAML.stringify(updatedConfig, { indent: 2 });
75
+ fs.writeFileSync(configPath, yamlContent);
2094
76
  } catch (error) {
2095
- getLogger().error(`Failed to publish component ${component.name}:`, error);
2096
- return [
2097
- {
2098
- success: false,
2099
- error: error instanceof Error ? error.message : "Unknown error"
2100
- },
2101
- void 0
2102
- ];
77
+ getLogger().warn(`Failed to update .jay config file: ${error}`);
2103
78
  }
2104
79
  }
2105
- async function loadPageContracts(dirPath, pageUrl, projectRootPath) {
2106
- const { hasPageHtml, hasPageContract, hasPageConfig } = await isPageDirectory(dirPath);
2107
- const plugins = await scanPlugins(projectRootPath);
2108
- const pageInfo = await loadProjectPage(
2109
- {
2110
- dirPath,
2111
- pageUrl,
2112
- pageName: path.basename(dirPath),
2113
- hasPageHtml,
2114
- hasPageContract,
2115
- hasPageConfig
2116
- },
2117
- plugins
2118
- );
2119
- return { projectPage: pageInfo, plugins };
2120
- }
2121
- function createEditorHandlers(config, tsConfigPath, projectRoot) {
2122
- const onPublish = async (params) => {
2123
- const status = [];
2124
- const createdJayHtmls = [];
2125
- if (params.pages) {
2126
- for (const page of params.pages) {
2127
- const [pageStatus, createdJayHtml] = await handlePagePublish(config, page);
2128
- status.push(pageStatus);
2129
- if (pageStatus.success)
2130
- createdJayHtmls.push(createdJayHtml);
2131
- }
2132
- }
2133
- if (params.components) {
2134
- for (const component of params.components) {
2135
- const [compStatus, createdJayHtml] = await handleComponentPublish(
2136
- config,
2137
- component
2138
- );
2139
- status.push(compStatus);
2140
- if (compStatus.success)
2141
- createdJayHtmls.push(createdJayHtml);
2142
- }
2143
- }
2144
- for (const { jayHtml, dirname, filename, fullPath } of createdJayHtmls) {
2145
- const parsedJayHtml = await parseJayFile(
2146
- jayHtml,
2147
- filename,
2148
- dirname,
2149
- { relativePath: tsConfigPath },
2150
- JAY_IMPORT_RESOLVER,
2151
- projectRoot
2152
- );
2153
- const definitionFile = generateElementDefinitionFile(parsedJayHtml);
2154
- if (definitionFile.validations.length > 0)
2155
- getLogger().info(
2156
- `failed to generate .d.ts for ${fullPath} with validation errors: ${definitionFile.validations.join("\n")}`
2157
- );
2158
- else
2159
- await fs.promises.writeFile(fullPath + ".d.ts", definitionFile.val, "utf-8");
2160
- }
2161
- return {
2162
- type: "publish",
2163
- success: status.every((s2) => s2.success),
2164
- status
2165
- };
2166
- };
2167
- const onSaveImage = async (params) => {
2168
- try {
2169
- const imagesDir = path.join(path.resolve(config.devServer.publicFolder), "images");
2170
- await fs.promises.mkdir(imagesDir, { recursive: true });
2171
- const filename = `${params.imageId}.png`;
2172
- const imagePath = path.join(imagesDir, filename);
2173
- await fs.promises.writeFile(imagePath, Buffer.from(params.imageData, "base64"));
2174
- getLogger().info(`šŸ–¼ļø Saved image: ${imagePath}`);
2175
- return {
2176
- type: "saveImage",
2177
- success: true,
2178
- imageUrl: `/images/${filename}`
2179
- };
2180
- } catch (error) {
2181
- getLogger().error("Failed to save image:", error);
2182
- return {
2183
- type: "saveImage",
2184
- success: false,
2185
- error: error instanceof Error ? error.message : "Unknown error"
2186
- };
2187
- }
2188
- };
2189
- const onHasImage = async (params) => {
2190
- try {
2191
- const filename = `${params.imageId}.png`;
2192
- const imagePath = path.join(
2193
- path.resolve(config.devServer.publicFolder),
2194
- "images",
2195
- filename
2196
- );
2197
- const exists = fs.existsSync(imagePath);
2198
- return {
2199
- type: "hasImage",
2200
- success: true,
2201
- exists,
2202
- imageUrl: exists ? `/images/${filename}` : void 0
2203
- };
2204
- } catch (error) {
2205
- getLogger().error("Failed to check image:", error);
2206
- return {
2207
- type: "hasImage",
2208
- success: false,
2209
- exists: false,
2210
- error: error instanceof Error ? error.message : "Unknown error"
2211
- };
2212
- }
2213
- };
2214
- const onGetProjectInfo = async (params) => {
2215
- try {
2216
- const pagesBasePath = path.resolve(config.devServer.pagesBase);
2217
- const componentsBasePath = path.resolve(config.devServer.componentsBase);
2218
- const configBasePath = path.resolve(config.devServer.configBase);
2219
- const info = await scanProjectInfo(
2220
- pagesBasePath,
2221
- componentsBasePath,
2222
- configBasePath,
2223
- projectRoot
2224
- );
2225
- getLogger().info(`šŸ“‹ Retrieved project info: ${info.name}`);
2226
- getLogger().info(` Pages: ${info.pages.length}`);
2227
- getLogger().info(` Components: ${info.components.length}`);
2228
- getLogger().info(` plugins: ${info.plugins.length}`);
2229
- return {
2230
- type: "getProjectInfo",
2231
- success: true,
2232
- info
2233
- };
2234
- } catch (error) {
2235
- getLogger().error("Failed to get project info:", error);
2236
- return {
2237
- type: "getProjectInfo",
2238
- success: false,
2239
- error: error instanceof Error ? error.message : "Unknown error",
2240
- info: {
2241
- name: "Error",
2242
- localPath: process.cwd(),
2243
- pages: [],
2244
- components: [],
2245
- plugins: []
2246
- }
2247
- };
2248
- }
2249
- };
2250
- const onExport = async (params) => {
2251
- try {
2252
- const pagesBasePath = path.resolve(config.devServer.pagesBase);
2253
- const { vendorId, pageUrl, vendorDoc } = params;
2254
- const dirname = pageUrlToDirectoryPath(pageUrl, pagesBasePath);
2255
- const vendorFilename = `page.${vendorId}.json`;
2256
- const vendorFilePath = path.join(dirname, vendorFilename);
2257
- await fs.promises.mkdir(dirname, { recursive: true });
2258
- await fs.promises.writeFile(
2259
- vendorFilePath,
2260
- JSON.stringify(vendorDoc, null, 2),
2261
- "utf-8"
2262
- );
2263
- getLogger().info(`šŸ“¦ Exported ${vendorId} document to: ${vendorFilePath}`);
2264
- if (hasVendor(vendorId)) {
2265
- getLogger().info(`šŸ”„ Converting ${vendorId} document to Jay HTML...`);
2266
- const vendor = getVendor(vendorId);
2267
- try {
2268
- const { projectPage, plugins } = await loadPageContracts(
2269
- dirname,
2270
- pageUrl,
2271
- projectRoot
2272
- );
2273
- const conversionResult = await vendor.convertToBodyHtml(
2274
- vendorDoc,
2275
- pageUrl,
2276
- projectPage,
2277
- plugins
2278
- );
2279
- const fullJayHtml = await buildJayHtmlFromVendorResult(
2280
- conversionResult,
2281
- dirname,
2282
- path.basename(dirname)
2283
- );
2284
- const jayHtmlPath = path.join(dirname, "page.jay-html");
2285
- await fs.promises.writeFile(jayHtmlPath, fullJayHtml, "utf-8");
2286
- getLogger().info(`āœ… Successfully converted to Jay HTML: ${jayHtmlPath}`);
2287
- return {
2288
- type: "export",
2289
- success: true,
2290
- vendorSourcePath: vendorFilePath,
2291
- jayHtmlPath
2292
- };
2293
- } catch (conversionError) {
2294
- getLogger().error(`āŒ Vendor conversion threw an error:`, conversionError);
2295
- return {
2296
- type: "export",
2297
- success: false,
2298
- vendorSourcePath: vendorFilePath,
2299
- error: conversionError instanceof Error ? conversionError.message : "Unknown conversion error"
2300
- };
2301
- }
2302
- } else {
2303
- getLogger().info(`ā„¹ļø No vendor found for '${vendorId}'. Skipping conversion.`);
2304
- }
2305
- return {
2306
- type: "export",
2307
- success: true,
2308
- vendorSourcePath: vendorFilePath
2309
- };
2310
- } catch (error) {
2311
- getLogger().error("Failed to export vendor document:", error);
2312
- return {
2313
- type: "export",
2314
- success: false,
2315
- error: error instanceof Error ? error.message : "Unknown error"
2316
- };
2317
- }
2318
- };
2319
- const onImport = async (params) => {
2320
- try {
2321
- const pagesBasePath = path.resolve(config.devServer.pagesBase);
2322
- const { vendorId, pageUrl } = params;
2323
- const dirname = pageUrlToDirectoryPath(pageUrl, pagesBasePath);
2324
- const vendorFilename = `page.${vendorId}.json`;
2325
- const vendorFilePath = path.join(dirname, vendorFilename);
2326
- if (!fs.existsSync(vendorFilePath)) {
2327
- return {
2328
- type: "import",
2329
- success: false,
2330
- error: `No ${vendorId} document found at ${pageUrl}. File not found: ${vendorFilePath}`
2331
- };
2332
- }
2333
- const fileContent = await fs.promises.readFile(vendorFilePath, "utf-8");
2334
- const vendorDoc = JSON.parse(fileContent);
2335
- getLogger().info(`šŸ“„ Imported ${vendorId} document from: ${vendorFilePath}`);
2336
- return {
2337
- type: "import",
2338
- success: true,
2339
- vendorDoc
2340
- };
2341
- } catch (error) {
2342
- getLogger().error("Failed to import vendor document:", error);
2343
- return {
2344
- type: "import",
2345
- success: false,
2346
- error: error instanceof Error ? error.message : "Unknown error"
2347
- };
2348
- }
2349
- };
2350
- return {
2351
- onPublish,
2352
- onSaveImage,
2353
- onHasImage,
2354
- onGetProjectInfo,
2355
- onExport,
2356
- onImport
2357
- };
2358
- }
2359
80
  async function generatePageDefinitionFiles(routes, tsConfigPath, projectRoot) {
2360
81
  for (const route of routes) {
2361
82
  const jayHtmlPath = route.fsRoute.jayHtmlPath;
@@ -2415,36 +136,6 @@ async function startDevServer(options = {}) {
2415
136
  const httpServer = http.createServer(app);
2416
137
  const devServerPort = await getPort({ port: resolvedConfig.devServer.portRange });
2417
138
  const log = getLogger();
2418
- const editorServer = createEditorServer({
2419
- portRange: resolvedConfig.editorServer.portRange,
2420
- editorId: resolvedConfig.editorServer.editorId,
2421
- onEditorId: (editorId2) => {
2422
- log.info(`Editor connected with ID: ${editorId2}`);
2423
- updateConfig({
2424
- editorServer: {
2425
- editorId: editorId2
2426
- }
2427
- });
2428
- }
2429
- });
2430
- const { port: editorPort, editorId } = await editorServer.start();
2431
- const registeredVendors = getRegisteredVendors();
2432
- if (registeredVendors.length > 0) {
2433
- log.info(
2434
- `šŸ“¦ Registered ${registeredVendors.length} vendor(s): ${registeredVendors.join(", ")}`
2435
- );
2436
- }
2437
- const handlers = createEditorHandlers(
2438
- resolvedConfig,
2439
- jayOptions.tsConfigFilePath,
2440
- process.cwd()
2441
- );
2442
- editorServer.onPublish(handlers.onPublish);
2443
- editorServer.onSaveImage(handlers.onSaveImage);
2444
- editorServer.onHasImage(handlers.onHasImage);
2445
- editorServer.onGetProjectInfo(handlers.onGetProjectInfo);
2446
- editorServer.onExport(handlers.onExport);
2447
- editorServer.onImport(handlers.onImport);
2448
139
  const { server, viteServer, routes, service } = await mkDevServer({
2449
140
  pagesRootFolder: path.resolve(resolvedConfig.devServer.pagesBase),
2450
141
  projectRootFolder: process.cwd(),
@@ -2454,73 +145,6 @@ async function startDevServer(options = {}) {
2454
145
  httpServer
2455
146
  });
2456
147
  app.use(server);
2457
- const { freezeStore } = service;
2458
- editorServer.onListRoutes(async () => ({
2459
- type: "listRoutes",
2460
- success: true,
2461
- routes: service.listRoutes()
2462
- }));
2463
- if (freezeStore) {
2464
- editorServer.onListFreezes(async (params) => ({
2465
- type: "listFreezes",
2466
- success: true,
2467
- freezes: (await freezeStore.list(params.route)).map(
2468
- ({ id, name, route, routePattern, createdAt }) => ({
2469
- id,
2470
- name,
2471
- route,
2472
- routePattern,
2473
- createdAt
2474
- })
2475
- )
2476
- }));
2477
- editorServer.onRenameFreeze(async (params) => ({
2478
- type: "renameFreeze",
2479
- success: await freezeStore.rename(params.id, params.name)
2480
- }));
2481
- editorServer.onDeleteFreeze(async (params) => ({
2482
- type: "deleteFreeze",
2483
- success: await freezeStore.delete(params.id)
2484
- }));
2485
- viteServer.watcher.on("change", (changedPath) => {
2486
- if (changedPath.endsWith(".jay-html") || changedPath.endsWith(".css")) {
2487
- editorServer.emitFreezeChanged();
2488
- }
2489
- });
2490
- }
2491
- editorServer.onLoadRouteParams(async (params) => {
2492
- const routePath = params.route;
2493
- try {
2494
- (async () => {
2495
- try {
2496
- for await (const batch of service.loadRouteParams(routePath)) {
2497
- editorServer.emitRouteParamsBatch({
2498
- type: "routeParamsBatch",
2499
- route: routePath,
2500
- params: batch,
2501
- hasMore: true
2502
- });
2503
- }
2504
- editorServer.emitRouteParamsBatch({
2505
- type: "routeParamsBatch",
2506
- route: routePath,
2507
- params: [],
2508
- hasMore: false
2509
- });
2510
- } catch (err) {
2511
- editorServer.emitRouteParamsBatch({
2512
- type: "routeParamsBatch",
2513
- route: routePath,
2514
- params: [],
2515
- hasMore: false
2516
- });
2517
- }
2518
- })();
2519
- return { type: "loadRouteParams", success: true };
2520
- } catch (err) {
2521
- return { type: "loadRouteParams", success: false, error: err.message };
2522
- }
2523
- });
2524
148
  const publicPath = path.resolve(resolvedConfig.devServer.publicFolder);
2525
149
  if (fs.existsSync(publicPath)) {
2526
150
  app.use(express.static(publicPath));
@@ -2539,7 +163,6 @@ async function startDevServer(options = {}) {
2539
163
  httpServer.listen(devServerPort, () => {
2540
164
  log.important(`šŸš€ Jay Stack dev server started successfully!`);
2541
165
  log.important(`šŸ“± Dev Server: http://localhost:${devServerPort}`);
2542
- log.important(`šŸŽØ Editor Server: http://localhost:${editorPort} (ID: ${editorId})`);
2543
166
  log.important(`šŸ“ Pages directory: ${resolvedConfig.devServer.pagesBase}`);
2544
167
  if (fs.existsSync(publicPath)) {
2545
168
  log.important(`šŸ“ Public folder: ${resolvedConfig.devServer.publicFolder}`);
@@ -2557,7 +180,6 @@ async function startDevServer(options = {}) {
2557
180
  });
2558
181
  const shutdown = async () => {
2559
182
  log.important("\nšŸ›‘ Shutting down servers...");
2560
- await editorServer.stop();
2561
183
  httpServer.closeAllConnections();
2562
184
  await new Promise((resolve) => httpServer.close(resolve));
2563
185
  process.exit(0);
@@ -2567,7 +189,6 @@ async function startDevServer(options = {}) {
2567
189
  res.json({
2568
190
  status: "ready",
2569
191
  port: devServerPort,
2570
- editorPort,
2571
192
  uptime: process.uptime()
2572
193
  });
2573
194
  });
@@ -2588,25 +209,25 @@ async function startDevServer(options = {}) {
2588
209
  process.on("SIGTERM", shutdown);
2589
210
  process.on("SIGINT", shutdown);
2590
211
  }
2591
- async function initializeServicesForCli(projectRoot, viteServer) {
212
+ async function initializeServicesForCli(projectRoot, viteServer, quiet = false) {
2592
213
  const path2 = await import("node:path");
2593
214
  const fs2 = await import("node:fs");
2594
215
  const {
2595
- runInitCallbacks,
216
+ runInitCallbacks: runInitCallbacks2,
2596
217
  getServiceRegistry,
2597
- discoverPluginsWithInit,
2598
- sortPluginsByDependencies,
2599
- executePluginServerInits
218
+ discoverPluginsWithInit: discoverPluginsWithInit2,
219
+ sortPluginsByDependencies: sortPluginsByDependencies2,
220
+ executePluginServerInits: executePluginServerInits2
2600
221
  } = await import("@jay-framework/stack-server-runtime");
2601
222
  let initErrors = /* @__PURE__ */ new Map();
2602
223
  try {
2603
- const discoveredPlugins = await discoverPluginsWithInit({
224
+ const discoveredPlugins = await discoverPluginsWithInit2({
2604
225
  projectRoot,
2605
226
  verbose: false
2606
227
  });
2607
- const pluginsWithInit = sortPluginsByDependencies(discoveredPlugins);
228
+ const pluginsWithInit = sortPluginsByDependencies2(discoveredPlugins);
2608
229
  try {
2609
- initErrors = await executePluginServerInits(pluginsWithInit, viteServer, false);
230
+ initErrors = await executePluginServerInits2(pluginsWithInit, viteServer, false, quiet);
2610
231
  } catch (error) {
2611
232
  getLogger().warn(chalk.yellow(`āš ļø Plugin initialization skipped: ${error.message}`));
2612
233
  }
@@ -2621,7 +242,7 @@ async function initializeServicesForCli(projectRoot, viteServer) {
2621
242
  if (initModule?.init?._serverInit) {
2622
243
  await initModule.init._serverInit();
2623
244
  }
2624
- await runInitCallbacks();
245
+ await runInitCallbacks2();
2625
246
  } catch (error) {
2626
247
  getLogger().warn(chalk.yellow(`āš ļø Service initialization failed: ${error.message}`));
2627
248
  getLogger().warn(chalk.gray(" Static contracts will still be listed."));
@@ -3029,7 +650,7 @@ const FOLDER_PATH_MAX_SEGMENTS = 32;
3029
650
  const BLOCKED_TAGS = /<\s*(script|iframe|object|embed)\b[^>]*>[\s\S]*?<\/\s*\1\s*>|<\s*(script|iframe|object|embed)\b[^>]*\/?>/gi;
3030
651
  const EVENT_HANDLER_ATTR = /\s+on[a-z]+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi;
3031
652
  const JAVASCRIPT_URL = /\b(href|src|xlink:href)\s*=\s*("|')\s*javascript:/gi;
3032
- function isRecord(value) {
653
+ function isRecord$1(value) {
3033
654
  return typeof value === "object" && value !== null && !Array.isArray(value);
3034
655
  }
3035
656
  function byteLengthUtf8(value) {
@@ -3099,7 +720,7 @@ function optionalString(obj, field) {
3099
720
  function validateInteraction(raw, itemPath, errors) {
3100
721
  if (raw === void 0)
3101
722
  return void 0;
3102
- if (!isRecord(raw)) {
723
+ if (!isRecord$1(raw)) {
3103
724
  errors.push({
3104
725
  path: itemPath,
3105
726
  message: "interaction must be an object",
@@ -3124,7 +745,7 @@ function validateInteraction(raw, itemPath, errors) {
3124
745
  function validatePresentation(raw, itemPath, errors) {
3125
746
  if (raw === void 0)
3126
747
  return void 0;
3127
- if (!isRecord(raw)) {
748
+ if (!isRecord$1(raw)) {
3128
749
  errors.push({
3129
750
  path: itemPath,
3130
751
  message: "presentation must be an object",
@@ -3197,7 +818,7 @@ const BROWSE_SIZES = /* @__PURE__ */ new Set(["large", "medium", "small"]);
3197
818
  function validateBrowse(raw, itemPath, errors) {
3198
819
  if (raw === void 0)
3199
820
  return void 0;
3200
- if (!isRecord(raw)) {
821
+ if (!isRecord$1(raw)) {
3201
822
  errors.push({
3202
823
  path: itemPath,
3203
824
  message: "browse must be an object",
@@ -3280,7 +901,7 @@ function validateFolderPath(raw, itemPath, errors) {
3280
901
  }
3281
902
  function validateAddMenuItem(raw, itemPath) {
3282
903
  const errors = [];
3283
- if (!isRecord(raw)) {
904
+ if (!isRecord$1(raw)) {
3284
905
  return {
3285
906
  item: null,
3286
907
  errors: [
@@ -3331,7 +952,7 @@ function validateAddMenuItem(raw, itemPath) {
3331
952
  }
3332
953
  function validateAddMenuCatalogFile(raw, sourcePath) {
3333
954
  const errors = [];
3334
- if (!isRecord(raw)) {
955
+ if (!isRecord$1(raw)) {
3335
956
  return {
3336
957
  file: null,
3337
958
  errors: [
@@ -3690,7 +1311,7 @@ function suggestionForCode(code) {
3690
1311
  return `See ${CONTRIBUTOR_GUIDE} for schema and validation rules`;
3691
1312
  return ADD_MENU_VALIDATION_SUGGESTIONS[code] ?? `See ${CONTRIBUTOR_GUIDE} for schema and validation rules`;
3692
1313
  }
3693
- function mapSchemaError(error, catalogPath) {
1314
+ function mapSchemaError$1(error, catalogPath) {
3694
1315
  const code = error.code ?? "catalog-validation-error";
3695
1316
  return {
3696
1317
  type: "add-menu-catalog",
@@ -3764,7 +1385,7 @@ async function validateAddMenuCatalogFileAtPath(catalogPath, relPath, result) {
3764
1385
  return;
3765
1386
  }
3766
1387
  const validated = validateAddMenuCatalogFile(parsed, relPath);
3767
- result.errors.push(...validated.errors.map((error) => mapSchemaError(error, relPath)));
1388
+ result.errors.push(...validated.errors.map((error) => mapSchemaError$1(error, relPath)));
3768
1389
  if (!validated.file?.items.length) {
3769
1390
  return;
3770
1391
  }
@@ -3785,6 +1406,173 @@ async function validateAddMenuCatalog(context, result) {
3785
1406
  await validateAddMenuCatalogFileAtPath(catalogPath, relPath, result);
3786
1407
  }
3787
1408
  }
1409
+ function isRecord(value) {
1410
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1411
+ }
1412
+ function parseRequires(raw, filePath) {
1413
+ const errors = [];
1414
+ if (raw === void 0) {
1415
+ return { requires: [], errors };
1416
+ }
1417
+ if (!Array.isArray(raw)) {
1418
+ errors.push({
1419
+ path: `${filePath}.requires`,
1420
+ message: "requires must be an array",
1421
+ code: "settings-requires-type"
1422
+ });
1423
+ return { requires: [], errors };
1424
+ }
1425
+ const requires = [];
1426
+ raw.forEach((entry, index) => {
1427
+ if (!isRecord(entry)) {
1428
+ errors.push({
1429
+ path: `${filePath}.requires[${index}]`,
1430
+ message: "require entry must be an object",
1431
+ code: "settings-require-shape"
1432
+ });
1433
+ return;
1434
+ }
1435
+ if (typeof entry.plugin !== "string" || !entry.plugin.trim()) {
1436
+ errors.push({
1437
+ path: `${filePath}.requires[${index}].plugin`,
1438
+ message: "plugin is required",
1439
+ code: "settings-require-plugin"
1440
+ });
1441
+ return;
1442
+ }
1443
+ if (entry.status !== "configured") {
1444
+ errors.push({
1445
+ path: `${filePath}.requires[${index}].status`,
1446
+ message: 'status must be "configured"',
1447
+ code: "settings-require-status"
1448
+ });
1449
+ return;
1450
+ }
1451
+ requires.push({ plugin: entry.plugin.trim(), status: "configured" });
1452
+ });
1453
+ return { requires, errors };
1454
+ }
1455
+ function validateAiditorSettingsFile(raw, sourcePath) {
1456
+ const errors = [];
1457
+ if (!isRecord(raw)) {
1458
+ return {
1459
+ errors: [
1460
+ {
1461
+ path: sourcePath,
1462
+ message: "settings file must be a YAML object",
1463
+ code: "settings-root-type"
1464
+ }
1465
+ ]
1466
+ };
1467
+ }
1468
+ if (typeof raw.label !== "string" || !raw.label.trim()) {
1469
+ errors.push({
1470
+ path: `${sourcePath}.label`,
1471
+ message: "label is required",
1472
+ code: "settings-label"
1473
+ });
1474
+ }
1475
+ if (typeof raw.route !== "string" || !raw.route.trim()) {
1476
+ errors.push({
1477
+ path: `${sourcePath}.route`,
1478
+ message: "route is required",
1479
+ code: "settings-route"
1480
+ });
1481
+ } else if (!raw.route.startsWith("/")) {
1482
+ errors.push({
1483
+ path: `${sourcePath}.route`,
1484
+ message: "route must start with /",
1485
+ code: "settings-route-format"
1486
+ });
1487
+ }
1488
+ if (raw.pluginName !== void 0 && typeof raw.pluginName !== "string") {
1489
+ errors.push({
1490
+ path: `${sourcePath}.pluginName`,
1491
+ message: "pluginName must be a string",
1492
+ code: "settings-plugin-name-type"
1493
+ });
1494
+ }
1495
+ const { requires, errors: requireErrors } = parseRequires(raw.requires, sourcePath);
1496
+ errors.push(...requireErrors);
1497
+ if (errors.length > 0) {
1498
+ return { errors };
1499
+ }
1500
+ return {
1501
+ file: {
1502
+ label: raw.label.trim(),
1503
+ route: raw.route.trim(),
1504
+ ...typeof raw.pluginName === "string" && raw.pluginName.trim() ? { pluginName: raw.pluginName.trim() } : {},
1505
+ ...requires.length > 0 ? { requires } : {}
1506
+ },
1507
+ errors: []
1508
+ };
1509
+ }
1510
+ const AIDITOR_SETTINGS_TEMPLATE_REL_PATH = "agent-kit/aiditor/settings.template.yaml";
1511
+ function mapSchemaError(error, relPath) {
1512
+ return {
1513
+ type: "schema",
1514
+ message: error.message,
1515
+ location: error.path || relPath,
1516
+ code: error.code
1517
+ };
1518
+ }
1519
+ function validateSettingsTemplateAtPath(catalogPath, relPath, result, manifest) {
1520
+ let parsed;
1521
+ try {
1522
+ parsed = YAML.parse(fs.readFileSync(catalogPath, "utf-8"));
1523
+ } catch (err) {
1524
+ result.errors.push({
1525
+ type: "schema",
1526
+ message: `Invalid YAML in ${relPath}: ${err instanceof Error ? err.message : String(err)}`,
1527
+ location: relPath
1528
+ });
1529
+ return;
1530
+ }
1531
+ const validated = validateAiditorSettingsFile(parsed, relPath);
1532
+ result.errors.push(...validated.errors.map((error) => mapSchemaError(error, relPath)));
1533
+ if (!validated.file) {
1534
+ return;
1535
+ }
1536
+ const routeEntry = manifest.routes?.find((route) => route.path === validated.file.route);
1537
+ if (!routeEntry) {
1538
+ result.warnings.push({
1539
+ type: "schema",
1540
+ message: `settings route "${validated.file.route}" is not declared in plugin.yaml routes[]`,
1541
+ location: relPath,
1542
+ code: "settings-route-missing",
1543
+ suggestion: "Add a matching routes[] entry or fix the route in settings.template.yaml"
1544
+ });
1545
+ } else if (routeEntry.devOnly !== true) {
1546
+ result.warnings.push({
1547
+ type: "schema",
1548
+ message: `settings route "${validated.file.route}" should declare devOnly: true on routes[]`,
1549
+ location: "plugin.yaml routes",
1550
+ code: "settings-route-dev-only",
1551
+ suggestion: "Add devOnly: true when the settings page is dev-server tooling (see Design Log #171)"
1552
+ });
1553
+ }
1554
+ }
1555
+ async function validateAiditorSettings(context, result) {
1556
+ const templatePath = path.join(context.pluginPath, AIDITOR_SETTINGS_TEMPLATE_REL_PATH);
1557
+ if (!fs.existsSync(templatePath)) {
1558
+ return;
1559
+ }
1560
+ validateSettingsTemplateAtPath(
1561
+ templatePath,
1562
+ AIDITOR_SETTINGS_TEMPLATE_REL_PATH,
1563
+ result,
1564
+ context.manifest
1565
+ );
1566
+ if (!context.manifest.agentkit) {
1567
+ result.warnings.push({
1568
+ type: "schema",
1569
+ message: "Plugin ships agent-kit/aiditor/settings.template.yaml but has no agentkit handler",
1570
+ location: "plugin.yaml",
1571
+ code: "settings-missing-agentkit-handler",
1572
+ suggestion: "Declare agentkit in plugin.yaml and materialize to agent-kit/aiditor/settings/<plugin>.yaml on jay-stack agent-kit — see agent-kit/plugin/aiditor-settings-guide.md"
1573
+ });
1574
+ }
1575
+ }
3788
1576
  async function validatePlugin(options = {}) {
3789
1577
  const pluginPath = options.pluginPath || process.cwd();
3790
1578
  if (options.local) {
@@ -3852,6 +1640,7 @@ async function validatePluginPackage(pluginPath, options) {
3852
1640
  await validateDynamicContracts(context, result);
3853
1641
  }
3854
1642
  await validateAddMenuCatalog(context, result);
1643
+ await validateAiditorSettings(context, result);
3855
1644
  result.valid = result.errors.length === 0;
3856
1645
  return result;
3857
1646
  }
@@ -4161,6 +1950,13 @@ async function validateSchema(context, result) {
4161
1950
  if (route.css) {
4162
1951
  validateDocFile(route.css, `route "${route.path}" css`, context, result);
4163
1952
  }
1953
+ if (route.devOnly !== void 0 && typeof route.devOnly !== "boolean") {
1954
+ result.errors.push({
1955
+ type: "schema",
1956
+ message: `Route "${route.path}" devOnly must be a boolean`,
1957
+ location: "plugin.yaml"
1958
+ });
1959
+ }
4164
1960
  });
4165
1961
  }
4166
1962
  }
@@ -5230,7 +3026,7 @@ function resolveContractLinks(contract, contractPath) {
5230
3026
  return { ...contract, tags: resolveLinkedTags(contract.tags, contractDir) };
5231
3027
  }
5232
3028
  async function runPluginValidators(projectRoot, parsedFiles, errors, warnings) {
5233
- const scannedPlugins = await scanPlugins$1({ projectRoot, includeDevDeps: true });
3029
+ const scannedPlugins = await scanPlugins({ projectRoot, includeDevDeps: true });
5234
3030
  const loadedValidators = [];
5235
3031
  for (const [, plugin] of scannedPlugins) {
5236
3032
  if (!plugin.manifest.validators)
@@ -5354,12 +3150,18 @@ async function validateJayFiles(options = {}) {
5354
3150
  const resolvedConfig = getConfigWithDefaults(config);
5355
3151
  const projectRoot = options.projectRoot ?? process.cwd();
5356
3152
  const scanDir = options.path ? path.resolve(options.path) : path.resolve(resolvedConfig.devServer.pagesBase);
3153
+ const componentsDir = path.resolve(resolvedConfig.devServer.componentsBase);
5357
3154
  const errors = [];
5358
3155
  const warnings = [];
5359
3156
  const coverage = [];
5360
3157
  const parsedFiles = [];
5361
- const jayHtmlFiles = await findJayFiles(scanDir);
5362
- const contractFiles = await findContractFiles(scanDir);
3158
+ const pageJayHtmlFiles = await findJayFiles(scanDir);
3159
+ const componentJayHtmlFiles = await findJayFiles(componentsDir).catch(() => []);
3160
+ const jayHtmlFiles = [...pageJayHtmlFiles, ...componentJayHtmlFiles];
3161
+ const contractFiles = [
3162
+ ...await findContractFiles(scanDir),
3163
+ ...await findContractFiles(componentsDir).catch(() => [])
3164
+ ];
5363
3165
  if (options.verbose) {
5364
3166
  getLogger().info(chalk.gray(`Scanning directory: ${scanDir}`));
5365
3167
  getLogger().info(chalk.gray(`Found ${jayHtmlFiles.length} .jay-html files`));
@@ -5607,6 +3409,14 @@ function printJayValidationResult(result, options) {
5607
3409
  } else {
5608
3410
  logger.important(chalk.red(`Validation failed — ${result.errors.length} error(s).`));
5609
3411
  }
3412
+ const totalIssues = result.errors.length + result.warnings.length + result.coverage.length;
3413
+ if (totalIssues > 0) {
3414
+ logger.important(
3415
+ chalk.gray(
3416
+ "\nSee: agent-kit/designer/validation-guide.md for how to interpret and suppress warnings."
3417
+ )
3418
+ );
3419
+ }
5610
3420
  }
5611
3421
  async function runValidate(scanPath, options) {
5612
3422
  const result = await validateJayFiles({
@@ -5819,7 +3629,7 @@ async function copyDirRecursive(src, dest) {
5819
3629
  }
5820
3630
  }
5821
3631
  async function mergePluginAgentKitGuides(projectRoot, mode) {
5822
- const plugins = await scanPlugins$1({ projectRoot, includeDevDeps: true });
3632
+ const plugins = await scanPlugins({ projectRoot, includeDevDeps: true });
5823
3633
  const agentKitDir = path$1.join(projectRoot, "agent-kit");
5824
3634
  const roles = mode && ALL_ROLES.includes(mode) ? [mode] : ALL_ROLES;
5825
3635
  const copiedPerRole = /* @__PURE__ */ new Map();
@@ -6170,19 +3980,17 @@ function createDefaultPrompt(pluginName) {
6170
3980
  }
6171
3981
  };
6172
3982
  }
6173
- async function runSetup(pluginFilter, options, projectRoot, initializeServices) {
3983
+ async function runSetup(pluginFilter, options, projectRoot) {
6174
3984
  let viteServer;
6175
3985
  try {
6176
3986
  const logger = getLogger();
6177
- const path2 = await import("node:path");
6178
3987
  const jayConfig = loadConfig();
6179
- const configDir = path2.resolve(projectRoot, jayConfig.devServer?.configBase || "./config");
3988
+ const configDir = path$1.resolve(projectRoot, jayConfig.devServer?.configBase || "./config");
6180
3989
  logger.important(chalk.bold("\nšŸ”§ Setting up plugins...\n"));
6181
3990
  if (options.verbose) {
6182
3991
  logger.info("Starting Vite for TypeScript support...");
6183
3992
  }
6184
3993
  viteServer = await createViteForCli({ projectRoot });
6185
- const { discoverPluginsWithSetup, executePluginSetup } = await import("@jay-framework/stack-server-runtime");
6186
3994
  const pluginsWithSetup = await discoverPluginsWithSetup({
6187
3995
  projectRoot,
6188
3996
  verbose: options.verbose,
@@ -6203,12 +4011,9 @@ async function runSetup(pluginFilter, options, projectRoot, initializeServices)
6203
4011
  `Found ${pluginsWithSetup.length} plugin(s) with setup: ${pluginsWithSetup.map((p) => p.name).join(", ")}`
6204
4012
  );
6205
4013
  }
6206
- const { initErrors } = await initializeServices(projectRoot, viteServer);
6207
- if (initErrors.size > 0 && options.verbose) {
6208
- for (const [name, err] of initErrors) {
6209
- logger.info(chalk.yellow(`āš ļø ${name} init error: ${err.message}`));
6210
- }
6211
- }
4014
+ const allPluginsWithInit = sortPluginsByDependencies(
4015
+ await discoverPluginsWithInit({ projectRoot, verbose: options.verbose })
4016
+ );
6212
4017
  const interactive = options.interactive === true;
6213
4018
  let answersMap;
6214
4019
  if (options.answers) {
@@ -6230,14 +4035,13 @@ async function runSetup(pluginFilter, options, projectRoot, initializeServices)
6230
4035
  force: options.force ?? false,
6231
4036
  interactive,
6232
4037
  prompt,
6233
- initError: initErrors.get(plugin.name),
4038
+ initError: void 0,
6234
4039
  viteServer,
6235
4040
  verbose: options.verbose
6236
4041
  });
6237
4042
  switch (result.status) {
6238
4043
  case "configured":
6239
4044
  configured++;
6240
- logger.important(chalk.green(" āœ… Services verified"));
6241
4045
  if (result.configCreated?.length) {
6242
4046
  for (const cfg of result.configCreated) {
6243
4047
  logger.important(chalk.green(` āœ… Created ${cfg}`));
@@ -6246,6 +4050,7 @@ async function runSetup(pluginFilter, options, projectRoot, initializeServices)
6246
4050
  if (result.message) {
6247
4051
  logger.important(chalk.gray(` ${result.message}`));
6248
4052
  }
4053
+ await initPlugin(plugin.name, allPluginsWithInit, viteServer, logger);
6249
4054
  break;
6250
4055
  case "needs-config":
6251
4056
  needsConfig++;
@@ -6304,6 +4109,7 @@ async function runSetup(pluginFilter, options, projectRoot, initializeServices)
6304
4109
  }
6305
4110
  logger.important("");
6306
4111
  }
4112
+ await runProjectInit(projectRoot, viteServer);
6307
4113
  const parts = [];
6308
4114
  if (configured > 0)
6309
4115
  parts.push(`${configured} configured`);
@@ -6327,6 +4133,36 @@ async function runSetup(pluginFilter, options, projectRoot, initializeServices)
6327
4133
  }
6328
4134
  }
6329
4135
  }
4136
+ async function initPlugin(pluginName, allPluginsWithInit, viteServer, logger) {
4137
+ const pluginInit = allPluginsWithInit.filter((p) => p.name === pluginName);
4138
+ if (pluginInit.length === 0)
4139
+ return;
4140
+ const initErrors = await executePluginServerInits(pluginInit, viteServer, false, true);
4141
+ if (initErrors.size > 0) {
4142
+ for (const [, err] of initErrors) {
4143
+ logger.important(chalk.yellow(` āš ļø Init after setup: ${err.message}`));
4144
+ }
4145
+ } else {
4146
+ logger.important(chalk.green(` āœ… Services initialized`));
4147
+ }
4148
+ }
4149
+ async function runProjectInit(projectRoot, viteServer) {
4150
+ try {
4151
+ const initPathTs = path$1.join(projectRoot, "src", "init.ts");
4152
+ const initPathJs = path$1.join(projectRoot, "src", "init.js");
4153
+ let initModule;
4154
+ if (fsSync.existsSync(initPathTs) && viteServer) {
4155
+ initModule = await viteServer.ssrLoadModule(initPathTs);
4156
+ } else if (fsSync.existsSync(initPathJs)) {
4157
+ initModule = await import(initPathJs);
4158
+ }
4159
+ if (initModule?.init?._serverInit) {
4160
+ await initModule.init._serverInit();
4161
+ }
4162
+ await runInitCallbacks();
4163
+ } catch {
4164
+ }
4165
+ }
6330
4166
  async function runCommand(commandRef, rawArgs, options, projectRoot, initializeServices) {
6331
4167
  let viteServer;
6332
4168
  try {
@@ -6556,7 +4392,7 @@ program.command("validate-plugin").description("Validate a Jay Stack plugin pack
6556
4392
  }
6557
4393
  });
6558
4394
  program.command("setup [plugin]").description("Run plugin setup: config templates, credential validation, reference data").option("--force", "Force re-run (overwrite config templates and regenerate references)").option("--interactive", "Prompt for input via terminal (for humans)").option("--answers <file>", "Read answers from YAML file (for agents)").option("-v, --verbose", "Show detailed output").action(async (plugin, options) => {
6559
- await runSetup(plugin, options, process.cwd(), initializeServicesForCli);
4395
+ await runSetup(plugin, options, process.cwd());
6560
4396
  });
6561
4397
  program.command("agent-kit").description("Prepare agent kit: materialize contracts, generate references, write docs").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(
6562
4398
  "-m, --mode <role>",
@@ -6582,11 +4418,7 @@ if (!process.argv.slice(2).length) {
6582
4418
  program.outputHelp();
6583
4419
  }
6584
4420
  export {
6585
- createEditorHandlers,
6586
4421
  getConfigWithDefaults,
6587
- getRegisteredVendors,
6588
- getVendor,
6589
- hasVendor,
6590
4422
  listContracts2 as listContracts,
6591
4423
  loadConfig,
6592
4424
  materializeContracts2 as materializeContracts,