@jay-framework/jay-stack-cli 0.22.2 → 0.23.1

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,25 +2,24 @@
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 { parseJayFile, JAY_IMPORT_RESOLVER, generateElementDefinitionFile, parseContract, generateElementFile, generateServerElementFile, htmlElementTagNameMap, loadLinkedContract, getLinkedContractDir } from "@jay-framework/compiler-jay-html";
11
+ import { scanPlugins, listContracts, materializeContracts, SetupNeedsAnswerError, discoverPluginsWithSetup, sortPluginsByDependencies, discoverPluginsWithInit, 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";
22
21
  import { fileURLToPath } from "node:url";
23
- import { input, confirm, select } from "@inquirer/prompts";
22
+ import { select, confirm, input } from "@inquirer/prompts";
24
23
  const DEFAULT_CONFIG = {
25
24
  devServer: {
26
25
  portRange: [3e3, 3100],
@@ -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,2316 +41,41 @@ 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
- }
1997
- } catch (error) {
1998
- getLogger().warn(`Failed to parse page config ${pageConfigPath}:`, error);
1999
- }
2000
- }
2001
- return {
2002
- name: pageName,
2003
- url: pageUrl,
2004
- filePath: pageFilePath,
2005
- contract,
2006
- usedComponents
2007
- };
2008
- }
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
- });
2020
- return {
2021
- name: projectName,
2022
- localPath: projectRootPath,
2023
- pages,
2024
- components,
2025
- plugins
2026
- };
2027
- }
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) {
2068
- 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
2084
- };
2085
- getLogger().info(`🧩 Published component: ${fullPath}`);
2086
- return [
2087
- {
2088
- success: true,
2089
- filePath: fullPath,
2090
- contractPath
2091
- },
2092
- createdJayHtml
2093
- ];
2094
- } 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
- ];
2103
- }
2104
- }
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
44
  }
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
- };
45
+ };
46
+ } catch (error) {
47
+ getLogger().warn(`Failed to parse .jay YAML config file, using defaults: ${error}`);
48
+ return DEFAULT_CONFIG;
49
+ }
50
+ }
51
+ function getConfigWithDefaults(config) {
52
+ return {
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
2317
59
  }
2318
60
  };
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
- };
61
+ }
62
+ function updateConfig(updates) {
63
+ const configPath = path.resolve(".jay");
64
+ try {
65
+ const existingConfig = loadConfig();
66
+ const updatedConfig = {
67
+ ...existingConfig,
68
+ ...updates,
69
+ devServer: {
70
+ ...existingConfig.devServer,
71
+ ...updates.devServer
2332
72
  }
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
- };
73
+ };
74
+ const yamlContent = YAML.stringify(updatedConfig, { indent: 2 });
75
+ fs.writeFileSync(configPath, yamlContent);
76
+ } catch (error) {
77
+ getLogger().warn(`Failed to update .jay config file: ${error}`);
78
+ }
2358
79
  }
2359
80
  async function generatePageDefinitionFiles(routes, tsConfigPath, projectRoot) {
2360
81
  for (const route of routes) {
@@ -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."));
@@ -2843,17 +464,13 @@ function collectLocalInterfaces(sourceFile) {
2843
464
  function collectContractImportedTypes(sourceFile) {
2844
465
  const contractTypes = /* @__PURE__ */ new Set();
2845
466
  for (const statement of sourceFile.statements) {
2846
- if (!u.isImportDeclaration(statement))
2847
- continue;
467
+ if (!u.isImportDeclaration(statement)) continue;
2848
468
  const moduleSpecifier = statement.moduleSpecifier;
2849
- if (!u.isStringLiteral(moduleSpecifier))
2850
- continue;
469
+ if (!u.isStringLiteral(moduleSpecifier)) continue;
2851
470
  const modulePath = moduleSpecifier.text;
2852
- if (!modulePath.includes(".jay-contract"))
2853
- continue;
471
+ if (!modulePath.includes(".jay-contract")) continue;
2854
472
  const importClause = statement.importClause;
2855
- if (!importClause)
2856
- continue;
473
+ if (!importClause) continue;
2857
474
  const namedBindings = importClause.namedBindings;
2858
475
  if (namedBindings && u.isNamedImports(namedBindings)) {
2859
476
  for (const element of namedBindings.elements) {
@@ -2889,8 +506,7 @@ function visitNode(node, result) {
2889
506
  }
2890
507
  if (node.arguments.length > 0) {
2891
508
  const arg = node.arguments[0];
2892
- if (u.isIdentifier(arg))
2893
- ;
509
+ if (u.isIdentifier(arg)) ;
2894
510
  }
2895
511
  }
2896
512
  }
@@ -2912,10 +528,8 @@ function extractTypeNames(typeNode) {
2912
528
  return [];
2913
529
  }
2914
530
  function checkPropsConsistency(propsTypeName, localInterfaces, contractImportedTypes, contract, contractName, contractPath, sourcePath, errors, warnings) {
2915
- if (FRAMEWORK_PROP_TYPES.has(propsTypeName))
2916
- return;
2917
- if (contractImportedTypes.has(propsTypeName))
2918
- return;
531
+ if (FRAMEWORK_PROP_TYPES.has(propsTypeName)) return;
532
+ if (contractImportedTypes.has(propsTypeName)) return;
2919
533
  const prefix = `[${contractName}]`;
2920
534
  const iface = localInterfaces.get(propsTypeName);
2921
535
  if (!iface) {
@@ -2930,8 +544,7 @@ function checkPropsConsistency(propsTypeName, localInterfaces, contractImportedT
2930
544
  return;
2931
545
  }
2932
546
  const ownProperties = iface.properties;
2933
- if (ownProperties.length === 0)
2934
- return;
547
+ if (ownProperties.length === 0) return;
2935
548
  if (!contract.props || contract.props.length === 0) {
2936
549
  errors.push({
2937
550
  type: "contract-invalid",
@@ -2975,10 +588,8 @@ function checkParamsConsistency(paramsTypeNames, localInterfaces, contractImport
2975
588
  suggestion: `Add a params section to the contract (e.g., params: { slug: string })`
2976
589
  });
2977
590
  for (const typeName of paramsTypeNames) {
2978
- if (FRAMEWORK_PROP_TYPES.has(typeName))
2979
- continue;
2980
- if (contractImportedTypes.has(typeName))
2981
- continue;
591
+ if (FRAMEWORK_PROP_TYPES.has(typeName)) continue;
592
+ if (contractImportedTypes.has(typeName)) continue;
2982
593
  const iface = localInterfaces.get(typeName);
2983
594
  if (iface) {
2984
595
  const ownProps = iface.properties;
@@ -2991,13 +602,10 @@ function checkParamsConsistency(paramsTypeNames, localInterfaces, contractImport
2991
602
  return;
2992
603
  }
2993
604
  for (const typeName of paramsTypeNames) {
2994
- if (FRAMEWORK_PROP_TYPES.has(typeName))
2995
- continue;
2996
- if (contractImportedTypes.has(typeName))
2997
- continue;
605
+ if (FRAMEWORK_PROP_TYPES.has(typeName)) continue;
606
+ if (contractImportedTypes.has(typeName)) continue;
2998
607
  const iface = localInterfaces.get(typeName);
2999
- if (!iface)
3000
- continue;
608
+ if (!iface) continue;
3001
609
  const ownProperties = iface.properties;
3002
610
  const contractParamNames = new Set(contract.params.map((p) => p.name));
3003
611
  for (const prop of ownProperties) {
@@ -3029,7 +637,7 @@ const FOLDER_PATH_MAX_SEGMENTS = 32;
3029
637
  const BLOCKED_TAGS = /<\s*(script|iframe|object|embed)\b[^>]*>[\s\S]*?<\/\s*\1\s*>|<\s*(script|iframe|object|embed)\b[^>]*\/?>/gi;
3030
638
  const EVENT_HANDLER_ATTR = /\s+on[a-z]+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi;
3031
639
  const JAVASCRIPT_URL = /\b(href|src|xlink:href)\s*=\s*("|')\s*javascript:/gi;
3032
- function isRecord(value) {
640
+ function isRecord$1(value) {
3033
641
  return typeof value === "object" && value !== null && !Array.isArray(value);
3034
642
  }
3035
643
  function byteLengthUtf8(value) {
@@ -3089,17 +697,14 @@ function requiredString(obj, field, itemPath, errors, code) {
3089
697
  }
3090
698
  function optionalString(obj, field) {
3091
699
  const value = obj[field];
3092
- if (value === void 0)
3093
- return void 0;
3094
- if (typeof value !== "string")
3095
- return void 0;
700
+ if (value === void 0) return void 0;
701
+ if (typeof value !== "string") return void 0;
3096
702
  const trimmed = value.trim();
3097
703
  return trimmed.length > 0 ? trimmed : void 0;
3098
704
  }
3099
705
  function validateInteraction(raw, itemPath, errors) {
3100
- if (raw === void 0)
3101
- return void 0;
3102
- if (!isRecord(raw)) {
706
+ if (raw === void 0) return void 0;
707
+ if (!isRecord$1(raw)) {
3103
708
  errors.push({
3104
709
  path: itemPath,
3105
710
  message: "interaction must be an object",
@@ -3122,9 +727,8 @@ function validateInteraction(raw, itemPath, errors) {
3122
727
  };
3123
728
  }
3124
729
  function validatePresentation(raw, itemPath, errors) {
3125
- if (raw === void 0)
3126
- return void 0;
3127
- if (!isRecord(raw)) {
730
+ if (raw === void 0) return void 0;
731
+ if (!isRecord$1(raw)) {
3128
732
  errors.push({
3129
733
  path: itemPath,
3130
734
  message: "presentation must be an object",
@@ -3143,14 +747,12 @@ function validatePresentation(raw, itemPath, errors) {
3143
747
  }
3144
748
  if (type === "image") {
3145
749
  const src = requiredString(raw, "src", itemPath, errors, "presentation-missing-src");
3146
- if (!src)
3147
- return void 0;
750
+ if (!src) return void 0;
3148
751
  return { type: "image", src };
3149
752
  }
3150
753
  if (type === "gif") {
3151
754
  const src = requiredString(raw, "src", itemPath, errors, "presentation-missing-src");
3152
- if (!src)
3153
- return void 0;
755
+ if (!src) return void 0;
3154
756
  return {
3155
757
  type: "gif",
3156
758
  src,
@@ -3195,9 +797,8 @@ function validatePresentation(raw, itemPath, errors) {
3195
797
  }
3196
798
  const BROWSE_SIZES = /* @__PURE__ */ new Set(["large", "medium", "small"]);
3197
799
  function validateBrowse(raw, itemPath, errors) {
3198
- if (raw === void 0)
3199
- return void 0;
3200
- if (!isRecord(raw)) {
800
+ if (raw === void 0) return void 0;
801
+ if (!isRecord$1(raw)) {
3201
802
  errors.push({
3202
803
  path: itemPath,
3203
804
  message: "browse must be an object",
@@ -3220,8 +821,7 @@ function validateBrowse(raw, itemPath, errors) {
3220
821
  return { size: sizeRaw };
3221
822
  }
3222
823
  function validateFolderPath(raw, itemPath, errors) {
3223
- if (raw === void 0)
3224
- return void 0;
824
+ if (raw === void 0) return void 0;
3225
825
  if (!Array.isArray(raw)) {
3226
826
  errors.push({
3227
827
  path: `${itemPath}.folderPath`,
@@ -3280,7 +880,7 @@ function validateFolderPath(raw, itemPath, errors) {
3280
880
  }
3281
881
  function validateAddMenuItem(raw, itemPath) {
3282
882
  const errors = [];
3283
- if (!isRecord(raw)) {
883
+ if (!isRecord$1(raw)) {
3284
884
  return {
3285
885
  item: null,
3286
886
  errors: [
@@ -3331,7 +931,7 @@ function validateAddMenuItem(raw, itemPath) {
3331
931
  }
3332
932
  function validateAddMenuCatalogFile(raw, sourcePath) {
3333
933
  const errors = [];
3334
- if (!isRecord(raw)) {
934
+ if (!isRecord$1(raw)) {
3335
935
  return {
3336
936
  file: null,
3337
937
  errors: [
@@ -3359,8 +959,7 @@ function validateAddMenuCatalogFile(raw, sourcePath) {
3359
959
  raw.items.forEach((entry, index) => {
3360
960
  const result = validateAddMenuItem(entry, `${sourcePath}.items[${index}]`);
3361
961
  errors.push(...result.errors);
3362
- if (result.item)
3363
- items.push(result.item);
962
+ if (result.item) items.push(result.item);
3364
963
  });
3365
964
  if (items.length === 0 && errors.length > 0) {
3366
965
  return { file: null, errors };
@@ -3368,11 +967,9 @@ function validateAddMenuCatalogFile(raw, sourcePath) {
3368
967
  return { file: { items }, errors };
3369
968
  }
3370
969
  function normalizeAddMenuPresentation(item) {
3371
- if (item.presentation)
3372
- return item.presentation;
970
+ if (item.presentation) return item.presentation;
3373
971
  const thumbnail = item.thumbnail?.trim();
3374
- if (!thumbnail)
3375
- return void 0;
972
+ if (!thumbnail) return void 0;
3376
973
  if (/\.gif$/i.test(thumbnail)) {
3377
974
  return { type: "gif", src: thumbnail };
3378
975
  }
@@ -3383,15 +980,12 @@ function normalizeAddMenuBrowseSize(item) {
3383
980
  }
3384
981
  function hasSingleRootDiv(html) {
3385
982
  const trimmed = html.trim();
3386
- if (!trimmed.startsWith("<div"))
3387
- return false;
983
+ if (!trimmed.startsWith("<div")) return false;
3388
984
  const openMatch = trimmed.match(/^<div\b[^>]*>/i);
3389
- if (!openMatch)
3390
- return false;
985
+ if (!openMatch) return false;
3391
986
  const afterOpen = trimmed.slice(openMatch[0].length);
3392
987
  const closeIdx = afterOpen.lastIndexOf("</div>");
3393
- if (closeIdx < 0)
3394
- return false;
988
+ if (closeIdx < 0) return false;
3395
989
  const tail = afterOpen.slice(closeIdx + "</div>".length).trim();
3396
990
  return tail.length === 0;
3397
991
  }
@@ -3467,8 +1061,7 @@ function lintHtmlFragment(item, sourcePath) {
3467
1061
  }
3468
1062
  function lintGifPoster(item, sourcePath) {
3469
1063
  const presentation = normalizeAddMenuPresentation(item);
3470
- if (presentation?.type !== "gif" || presentation.poster?.trim())
3471
- return [];
1064
+ if (presentation?.type !== "gif" || presentation.poster?.trim()) return [];
3472
1065
  return [
3473
1066
  catalogWarning(
3474
1067
  "gif-missing-poster",
@@ -3479,10 +1072,8 @@ function lintGifPoster(item, sourcePath) {
3479
1072
  ];
3480
1073
  }
3481
1074
  function lintBrowseLargeWithoutPresentation(item, sourcePath) {
3482
- if (normalizeAddMenuBrowseSize(item) !== "large")
3483
- return [];
3484
- if (normalizeAddMenuPresentation(item))
3485
- return [];
1075
+ if (normalizeAddMenuBrowseSize(item) !== "large") return [];
1076
+ if (normalizeAddMenuPresentation(item)) return [];
3486
1077
  return [
3487
1078
  catalogWarning(
3488
1079
  "browse-large-without-presentation",
@@ -3555,8 +1146,7 @@ function resolveModulePath$1(basePath) {
3555
1146
  return void 0;
3556
1147
  }
3557
1148
  function collectTypeScriptFiles(dir, depth = 0) {
3558
- if (depth > 4)
3559
- return [];
1149
+ if (depth > 4) return [];
3560
1150
  const entries = fs.readdirSync(dir, { withFileTypes: true });
3561
1151
  const files = [];
3562
1152
  for (const entry of entries) {
@@ -3575,8 +1165,7 @@ function resolveHandlerSourceFile(pluginPath, handlerRef, isNpmPackage) {
3575
1165
  }
3576
1166
  const searchRoots = isNpmPackage ? [path.join(pluginPath, "lib"), path.join(pluginPath, "dist")] : [pluginPath];
3577
1167
  for (const root of searchRoots) {
3578
- if (!fs.existsSync(root))
3579
- continue;
1168
+ if (!fs.existsSync(root)) continue;
3580
1169
  for (const file of collectTypeScriptFiles(root)) {
3581
1170
  const content = fs.readFileSync(file, "utf-8");
3582
1171
  const definesHandler = new RegExp(
@@ -3596,8 +1185,7 @@ function resolveHandlerSourceFile(pluginPath, handlerRef, isNpmPackage) {
3596
1185
  if (reExportMatch) {
3597
1186
  const importSpec = reExportMatch[1].replace(/\.js$/, "");
3598
1187
  const resolved = resolveModulePath$1(path.resolve(path.dirname(file), importSpec));
3599
- if (resolved)
3600
- return resolved;
1188
+ if (resolved) return resolved;
3601
1189
  }
3602
1190
  }
3603
1191
  }
@@ -3607,8 +1195,7 @@ function extractBalancedBlock(source, openBraceIndex) {
3607
1195
  let depth = 0;
3608
1196
  for (let index = openBraceIndex; index < source.length; index++) {
3609
1197
  const char = source[index];
3610
- if (char === "{")
3611
- depth++;
1198
+ if (char === "{") depth++;
3612
1199
  else if (char === "}") {
3613
1200
  depth--;
3614
1201
  if (depth === 0) {
@@ -3628,8 +1215,7 @@ function findFunctionBodyOpenBrace(source, searchFrom) {
3628
1215
  if (parenMatch?.index !== void 0) {
3629
1216
  candidates.push(searchFrom + parenMatch.index + parenMatch[0].length - 1);
3630
1217
  }
3631
- if (candidates.length === 0)
3632
- return -1;
1218
+ if (candidates.length === 0) return -1;
3633
1219
  return Math.min(...candidates);
3634
1220
  }
3635
1221
  function extractFunctionBody(source, functionName) {
@@ -3646,11 +1232,9 @@ function extractFunctionBody(source, functionName) {
3646
1232
  ];
3647
1233
  for (const pattern of patterns) {
3648
1234
  const match = pattern.exec(source);
3649
- if (!match)
3650
- continue;
1235
+ if (!match) continue;
3651
1236
  const braceIndex = findFunctionBodyOpenBrace(source, match.index);
3652
- if (braceIndex === -1)
3653
- continue;
1237
+ if (braceIndex === -1) continue;
3654
1238
  return extractBalancedBlock(source, braceIndex);
3655
1239
  }
3656
1240
  return null;
@@ -3663,11 +1247,9 @@ function extractDefaultExportFunctionBody(source) {
3663
1247
  ];
3664
1248
  for (const pattern of patterns) {
3665
1249
  const match = pattern.exec(source);
3666
- if (!match)
3667
- continue;
1250
+ if (!match) continue;
3668
1251
  const braceIndex = findFunctionBodyOpenBrace(source, match.index);
3669
- if (braceIndex === -1)
3670
- continue;
1252
+ if (braceIndex === -1) continue;
3671
1253
  return extractBalancedBlock(source, braceIndex);
3672
1254
  }
3673
1255
  return null;
@@ -3677,8 +1259,7 @@ function handlerBodyWritesAddMenuCatalog(body) {
3677
1259
  }
3678
1260
  function resolveSetupHandlerFunctionBody(pluginPath, handlerRef, isNpmPackage) {
3679
1261
  const sourceFile = resolveHandlerSourceFile(pluginPath, handlerRef, isNpmPackage);
3680
- if (!sourceFile)
3681
- return null;
1262
+ if (!sourceFile) return null;
3682
1263
  const source = fs.readFileSync(sourceFile, "utf-8");
3683
1264
  if (isRelativeHandlerRef(handlerRef)) {
3684
1265
  return extractDefaultExportFunctionBody(source) ?? extractFunctionBody(source, "setup") ?? extractFunctionBody(source, handlerRef);
@@ -3686,11 +1267,10 @@ function resolveSetupHandlerFunctionBody(pluginPath, handlerRef, isNpmPackage) {
3686
1267
  return extractFunctionBody(source, handlerRef);
3687
1268
  }
3688
1269
  function suggestionForCode(code) {
3689
- if (!code)
3690
- return `See ${CONTRIBUTOR_GUIDE} for schema and validation rules`;
1270
+ if (!code) return `See ${CONTRIBUTOR_GUIDE} for schema and validation rules`;
3691
1271
  return ADD_MENU_VALIDATION_SUGGESTIONS[code] ?? `See ${CONTRIBUTOR_GUIDE} for schema and validation rules`;
3692
1272
  }
3693
- function mapSchemaError(error, catalogPath) {
1273
+ function mapSchemaError$1(error, catalogPath) {
3694
1274
  const code = error.code ?? "catalog-validation-error";
3695
1275
  return {
3696
1276
  type: "add-menu-catalog",
@@ -3717,8 +1297,7 @@ function pluginShipsAddMenuCatalog(context) {
3717
1297
  );
3718
1298
  }
3719
1299
  function validateAddMenuAgentKitHandler(context, result) {
3720
- if (!pluginShipsAddMenuCatalog(context))
3721
- return;
1300
+ if (!pluginShipsAddMenuCatalog(context)) return;
3722
1301
  const agentKitHandler = context.manifest.agentkit;
3723
1302
  if (!agentKitHandler) {
3724
1303
  result.warnings.push({
@@ -3730,15 +1309,13 @@ function validateAddMenuAgentKitHandler(context, result) {
3730
1309
  });
3731
1310
  }
3732
1311
  const setupHandler = typeof context.manifest.setup === "string" ? context.manifest.setup : void 0;
3733
- if (!setupHandler)
3734
- return;
1312
+ if (!setupHandler) return;
3735
1313
  const setupBody = resolveSetupHandlerFunctionBody(
3736
1314
  context.pluginPath,
3737
1315
  setupHandler,
3738
1316
  context.isNpmPackage
3739
1317
  );
3740
- if (!setupBody || !handlerBodyWritesAddMenuCatalog(setupBody))
3741
- return;
1318
+ if (!setupBody || !handlerBodyWritesAddMenuCatalog(setupBody)) return;
3742
1319
  result.warnings.push({
3743
1320
  type: "add-menu-catalog",
3744
1321
  code: "add-menu-legacy-setup-handler",
@@ -3764,7 +1341,7 @@ async function validateAddMenuCatalogFileAtPath(catalogPath, relPath, result) {
3764
1341
  return;
3765
1342
  }
3766
1343
  const validated = validateAddMenuCatalogFile(parsed, relPath);
3767
- result.errors.push(...validated.errors.map((error) => mapSchemaError(error, relPath)));
1344
+ result.errors.push(...validated.errors.map((error) => mapSchemaError$1(error, relPath)));
3768
1345
  if (!validated.file?.items.length) {
3769
1346
  return;
3770
1347
  }
@@ -3780,11 +1357,177 @@ async function validateAddMenuCatalog(context, result) {
3780
1357
  validateAddMenuAgentKitHandler(context, result);
3781
1358
  for (const relPath of ADD_MENU_CATALOG_REL_PATHS) {
3782
1359
  const catalogPath = path.join(context.pluginPath, relPath);
3783
- if (!fs.existsSync(catalogPath))
3784
- continue;
1360
+ if (!fs.existsSync(catalogPath)) continue;
3785
1361
  await validateAddMenuCatalogFileAtPath(catalogPath, relPath, result);
3786
1362
  }
3787
1363
  }
1364
+ function isRecord(value) {
1365
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1366
+ }
1367
+ function parseRequires(raw, filePath) {
1368
+ const errors = [];
1369
+ if (raw === void 0) {
1370
+ return { requires: [], errors };
1371
+ }
1372
+ if (!Array.isArray(raw)) {
1373
+ errors.push({
1374
+ path: `${filePath}.requires`,
1375
+ message: "requires must be an array",
1376
+ code: "settings-requires-type"
1377
+ });
1378
+ return { requires: [], errors };
1379
+ }
1380
+ const requires = [];
1381
+ raw.forEach((entry, index) => {
1382
+ if (!isRecord(entry)) {
1383
+ errors.push({
1384
+ path: `${filePath}.requires[${index}]`,
1385
+ message: "require entry must be an object",
1386
+ code: "settings-require-shape"
1387
+ });
1388
+ return;
1389
+ }
1390
+ if (typeof entry.plugin !== "string" || !entry.plugin.trim()) {
1391
+ errors.push({
1392
+ path: `${filePath}.requires[${index}].plugin`,
1393
+ message: "plugin is required",
1394
+ code: "settings-require-plugin"
1395
+ });
1396
+ return;
1397
+ }
1398
+ if (entry.status !== "configured") {
1399
+ errors.push({
1400
+ path: `${filePath}.requires[${index}].status`,
1401
+ message: 'status must be "configured"',
1402
+ code: "settings-require-status"
1403
+ });
1404
+ return;
1405
+ }
1406
+ requires.push({ plugin: entry.plugin.trim(), status: "configured" });
1407
+ });
1408
+ return { requires, errors };
1409
+ }
1410
+ function validateAiditorSettingsFile(raw, sourcePath) {
1411
+ const errors = [];
1412
+ if (!isRecord(raw)) {
1413
+ return {
1414
+ errors: [
1415
+ {
1416
+ path: sourcePath,
1417
+ message: "settings file must be a YAML object",
1418
+ code: "settings-root-type"
1419
+ }
1420
+ ]
1421
+ };
1422
+ }
1423
+ if (typeof raw.label !== "string" || !raw.label.trim()) {
1424
+ errors.push({
1425
+ path: `${sourcePath}.label`,
1426
+ message: "label is required",
1427
+ code: "settings-label"
1428
+ });
1429
+ }
1430
+ if (typeof raw.route !== "string" || !raw.route.trim()) {
1431
+ errors.push({
1432
+ path: `${sourcePath}.route`,
1433
+ message: "route is required",
1434
+ code: "settings-route"
1435
+ });
1436
+ } else if (!raw.route.startsWith("/")) {
1437
+ errors.push({
1438
+ path: `${sourcePath}.route`,
1439
+ message: "route must start with /",
1440
+ code: "settings-route-format"
1441
+ });
1442
+ }
1443
+ if (raw.pluginName !== void 0 && typeof raw.pluginName !== "string") {
1444
+ errors.push({
1445
+ path: `${sourcePath}.pluginName`,
1446
+ message: "pluginName must be a string",
1447
+ code: "settings-plugin-name-type"
1448
+ });
1449
+ }
1450
+ const { requires, errors: requireErrors } = parseRequires(raw.requires, sourcePath);
1451
+ errors.push(...requireErrors);
1452
+ if (errors.length > 0) {
1453
+ return { errors };
1454
+ }
1455
+ return {
1456
+ file: {
1457
+ label: raw.label.trim(),
1458
+ route: raw.route.trim(),
1459
+ ...typeof raw.pluginName === "string" && raw.pluginName.trim() ? { pluginName: raw.pluginName.trim() } : {},
1460
+ ...requires.length > 0 ? { requires } : {}
1461
+ },
1462
+ errors: []
1463
+ };
1464
+ }
1465
+ const AIDITOR_SETTINGS_TEMPLATE_REL_PATH = "agent-kit/aiditor/settings.template.yaml";
1466
+ function mapSchemaError(error, relPath) {
1467
+ return {
1468
+ type: "schema",
1469
+ message: error.message,
1470
+ location: error.path || relPath,
1471
+ code: error.code
1472
+ };
1473
+ }
1474
+ function validateSettingsTemplateAtPath(catalogPath, relPath, result, manifest) {
1475
+ let parsed;
1476
+ try {
1477
+ parsed = YAML.parse(fs.readFileSync(catalogPath, "utf-8"));
1478
+ } catch (err) {
1479
+ result.errors.push({
1480
+ type: "schema",
1481
+ message: `Invalid YAML in ${relPath}: ${err instanceof Error ? err.message : String(err)}`,
1482
+ location: relPath
1483
+ });
1484
+ return;
1485
+ }
1486
+ const validated = validateAiditorSettingsFile(parsed, relPath);
1487
+ result.errors.push(...validated.errors.map((error) => mapSchemaError(error, relPath)));
1488
+ if (!validated.file) {
1489
+ return;
1490
+ }
1491
+ const routeEntry = manifest.routes?.find((route) => route.path === validated.file.route);
1492
+ if (!routeEntry) {
1493
+ result.warnings.push({
1494
+ type: "schema",
1495
+ message: `settings route "${validated.file.route}" is not declared in plugin.yaml routes[]`,
1496
+ location: relPath,
1497
+ code: "settings-route-missing",
1498
+ suggestion: "Add a matching routes[] entry or fix the route in settings.template.yaml"
1499
+ });
1500
+ } else if (routeEntry.devOnly !== true) {
1501
+ result.warnings.push({
1502
+ type: "schema",
1503
+ message: `settings route "${validated.file.route}" should declare devOnly: true on routes[]`,
1504
+ location: "plugin.yaml routes",
1505
+ code: "settings-route-dev-only",
1506
+ suggestion: "Add devOnly: true when the settings page is dev-server tooling (see Design Log #171)"
1507
+ });
1508
+ }
1509
+ }
1510
+ async function validateAiditorSettings(context, result) {
1511
+ const templatePath = path.join(context.pluginPath, AIDITOR_SETTINGS_TEMPLATE_REL_PATH);
1512
+ if (!fs.existsSync(templatePath)) {
1513
+ return;
1514
+ }
1515
+ validateSettingsTemplateAtPath(
1516
+ templatePath,
1517
+ AIDITOR_SETTINGS_TEMPLATE_REL_PATH,
1518
+ result,
1519
+ context.manifest
1520
+ );
1521
+ if (!context.manifest.agentkit) {
1522
+ result.warnings.push({
1523
+ type: "schema",
1524
+ message: "Plugin ships agent-kit/aiditor/settings.template.yaml but has no agentkit handler",
1525
+ location: "plugin.yaml",
1526
+ code: "settings-missing-agentkit-handler",
1527
+ 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"
1528
+ });
1529
+ }
1530
+ }
3788
1531
  async function validatePlugin(options = {}) {
3789
1532
  const pluginPath = options.pluginPath || process.cwd();
3790
1533
  if (options.local) {
@@ -3852,6 +1595,7 @@ async function validatePluginPackage(pluginPath, options) {
3852
1595
  await validateDynamicContracts(context, result);
3853
1596
  }
3854
1597
  await validateAddMenuCatalog(context, result);
1598
+ await validateAiditorSettings(context, result);
3855
1599
  result.valid = result.errors.length === 0;
3856
1600
  return result;
3857
1601
  }
@@ -4161,6 +1905,13 @@ async function validateSchema(context, result) {
4161
1905
  if (route.css) {
4162
1906
  validateDocFile(route.css, `route "${route.path}" css`, context, result);
4163
1907
  }
1908
+ if (route.devOnly !== void 0 && typeof route.devOnly !== "boolean") {
1909
+ result.errors.push({
1910
+ type: "schema",
1911
+ message: `Route "${route.path}" devOnly must be a boolean`,
1912
+ location: "plugin.yaml"
1913
+ });
1914
+ }
4164
1915
  });
4165
1916
  }
4166
1917
  }
@@ -4230,16 +1981,14 @@ async function validateSchema(context, result) {
4230
1981
  }
4231
1982
  function checkExportExists(exportName, context) {
4232
1983
  const packageJsonPath = path.join(context.pluginPath, "package.json");
4233
- if (!fs.existsSync(packageJsonPath))
4234
- return true;
1984
+ if (!fs.existsSync(packageJsonPath)) return true;
4235
1985
  let mainPath;
4236
1986
  try {
4237
1987
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
4238
1988
  if (packageJson.exports?.["."]) {
4239
1989
  const entry = packageJson.exports["."];
4240
1990
  const entryPath = typeof entry === "string" ? entry : entry.default || entry.import;
4241
- if (entryPath)
4242
- mainPath = path.join(context.pluginPath, entryPath);
1991
+ if (entryPath) mainPath = path.join(context.pluginPath, entryPath);
4243
1992
  }
4244
1993
  if (!mainPath && packageJson.main) {
4245
1994
  mainPath = path.join(context.pluginPath, packageJson.main);
@@ -4247,8 +1996,7 @@ function checkExportExists(exportName, context) {
4247
1996
  } catch {
4248
1997
  return true;
4249
1998
  }
4250
- if (!mainPath || !fs.existsSync(mainPath))
4251
- return true;
1999
+ if (!mainPath || !fs.existsSync(mainPath)) return true;
4252
2000
  try {
4253
2001
  const content = fs.readFileSync(mainPath, "utf-8");
4254
2002
  const patterns = [
@@ -4308,8 +2056,7 @@ function resolveContractFile(contractSpec, context) {
4308
2056
  const resolvedPath = typeof exportValue === "string" ? exportValue : exportValue.default || exportValue.import || exportValue.require;
4309
2057
  if (resolvedPath) {
4310
2058
  const fullPath = path.join(context.pluginPath, resolvedPath);
4311
- if (fs.existsSync(fullPath))
4312
- return fullPath;
2059
+ if (fs.existsSync(fullPath)) return fullPath;
4313
2060
  }
4314
2061
  }
4315
2062
  }
@@ -4318,8 +2065,7 @@ function resolveContractFile(contractSpec, context) {
4318
2065
  }
4319
2066
  for (const dir of ["dist", "lib", ""]) {
4320
2067
  const candidate = path.join(context.pluginPath, dir, contractSpec);
4321
- if (fs.existsSync(candidate))
4322
- return candidate;
2068
+ if (fs.existsSync(candidate)) return candidate;
4323
2069
  }
4324
2070
  return void 0;
4325
2071
  } else {
@@ -4407,8 +2153,7 @@ function hasExportModifier(node) {
4407
2153
  function resolveModulePath(basePath) {
4408
2154
  for (const ext of ["", ".ts", ".js", "/index.ts", "/index.js"]) {
4409
2155
  const candidate = basePath + ext;
4410
- if (fs.existsSync(candidate))
4411
- return candidate;
2156
+ if (fs.existsSync(candidate)) return candidate;
4412
2157
  }
4413
2158
  return void 0;
4414
2159
  }
@@ -4418,10 +2163,8 @@ function resolveComponentSourcePath(componentName, context) {
4418
2163
  const entryFile = resolveModulePath(entryBase);
4419
2164
  const libEntryFile = !entryFile ? resolveModulePath(path.join(context.pluginPath, "lib", modulePath)) : void 0;
4420
2165
  const sourceEntry = entryFile || libEntryFile;
4421
- if (!sourceEntry)
4422
- return void 0;
4423
- if (!sourceEntry.endsWith(".ts"))
4424
- return void 0;
2166
+ if (!sourceEntry) return void 0;
2167
+ if (!sourceEntry.endsWith(".ts")) return void 0;
4425
2168
  let sourceCode;
4426
2169
  try {
4427
2170
  sourceCode = fs.readFileSync(sourceEntry, "utf-8");
@@ -4437,12 +2180,9 @@ function resolveComponentSourcePath(componentName, context) {
4437
2180
  );
4438
2181
  const starReexportModules = [];
4439
2182
  for (const statement of sourceFile.statements) {
4440
- if (!u.isExportDeclaration(statement))
4441
- continue;
4442
- if (!statement.moduleSpecifier)
4443
- continue;
4444
- if (!u.isStringLiteral(statement.moduleSpecifier))
4445
- continue;
2183
+ if (!u.isExportDeclaration(statement)) continue;
2184
+ if (!statement.moduleSpecifier) continue;
2185
+ if (!u.isStringLiteral(statement.moduleSpecifier)) continue;
4446
2186
  const moduleSpec = statement.moduleSpecifier.text;
4447
2187
  const exportClause = statement.exportClause;
4448
2188
  if (!exportClause) {
@@ -4460,12 +2200,10 @@ function resolveComponentSourcePath(componentName, context) {
4460
2200
  }
4461
2201
  }
4462
2202
  for (const moduleSpec of starReexportModules) {
4463
- if (!moduleSpec.startsWith("."))
4464
- continue;
2203
+ if (!moduleSpec.startsWith(".")) continue;
4465
2204
  const resolvedBase = path.resolve(path.dirname(sourceEntry), moduleSpec);
4466
2205
  const resolvedPath = resolveModulePath(resolvedBase);
4467
- if (!resolvedPath || !resolvedPath.endsWith(".ts"))
4468
- continue;
2206
+ if (!resolvedPath || !resolvedPath.endsWith(".ts")) continue;
4469
2207
  try {
4470
2208
  const modSource = fs.readFileSync(resolvedPath, "utf-8");
4471
2209
  const modFile = u.createSourceFile(
@@ -4498,16 +2236,12 @@ function resolveContractPath(contract, context) {
4498
2236
  }
4499
2237
  async function checkComponentContractConsistency(contract, context, result) {
4500
2238
  const componentName = contract.component;
4501
- if (!componentName)
4502
- return;
2239
+ if (!componentName) return;
4503
2240
  const sourcePath = resolveComponentSourcePath(componentName, context);
4504
- if (!sourcePath)
4505
- return;
4506
- if (!sourcePath.endsWith(".ts"))
4507
- return;
2241
+ if (!sourcePath) return;
2242
+ if (!sourcePath.endsWith(".ts")) return;
4508
2243
  const contractPath = resolveContractPath(contract, context);
4509
- if (!contractPath)
4510
- return;
2244
+ if (!contractPath) return;
4511
2245
  let contractContent;
4512
2246
  try {
4513
2247
  contractContent = await fs.promises.readFile(contractPath, "utf-8");
@@ -4515,8 +2249,7 @@ async function checkComponentContractConsistency(contract, context, result) {
4515
2249
  return;
4516
2250
  }
4517
2251
  const parsed = parseContract(contractContent, path.basename(contractPath));
4518
- if (parsed.validations.length > 0)
4519
- return;
2252
+ if (parsed.validations.length > 0) return;
4520
2253
  let sourceCode;
4521
2254
  try {
4522
2255
  sourceCode = await fs.promises.readFile(sourcePath, "utf-8");
@@ -4635,8 +2368,7 @@ async function validatePackageJson(context, result) {
4635
2368
  }
4636
2369
  async function validateDynamicContracts(context, result) {
4637
2370
  const { dynamic_contracts } = context.manifest;
4638
- if (!dynamic_contracts)
4639
- return;
2371
+ if (!dynamic_contracts) return;
4640
2372
  const dynamicConfigs = Array.isArray(dynamic_contracts) ? dynamic_contracts : [dynamic_contracts];
4641
2373
  for (const config of dynamicConfigs) {
4642
2374
  const prefix = config.prefix || "(unknown)";
@@ -4715,8 +2447,7 @@ function extractExpressions(text) {
4715
2447
  function extractTagPath(expr) {
4716
2448
  let cleaned = expr.replace(/^!/, "").trim();
4717
2449
  cleaned = cleaned.split(/\s*[!=]==?\s*/)[0].trim();
4718
- if (cleaned === "." || cleaned === "")
4719
- return null;
2450
+ if (cleaned === "." || cleaned === "") return null;
4720
2451
  if (/^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*$/.test(cleaned)) {
4721
2452
  return cleaned;
4722
2453
  }
@@ -4815,8 +2546,7 @@ function collectUsedTags(jayHtml) {
4815
2546
  const ifVal = element.getAttribute?.("if");
4816
2547
  if (ifVal) {
4817
2548
  const ifPath = extractTagPath(ifVal);
4818
- if (ifPath)
4819
- resolvePath(ifPath, scopes);
2549
+ if (ifPath) resolvePath(ifPath, scopes);
4820
2550
  }
4821
2551
  const refVal = element.getAttribute?.("ref");
4822
2552
  if (refVal) {
@@ -4824,12 +2554,10 @@ function collectUsedTags(jayHtml) {
4824
2554
  }
4825
2555
  const attrs = element.attributes ?? {};
4826
2556
  for (const [name, value] of Object.entries(attrs)) {
4827
- if (SKIP_ATTRS.has(name))
4828
- continue;
2557
+ if (SKIP_ATTRS.has(name)) continue;
4829
2558
  for (const expr of extractExpressions(value)) {
4830
2559
  const p = extractTagPath(expr);
4831
- if (p)
4832
- resolvePath(p, scopes);
2560
+ if (p) resolvePath(p, scopes);
4833
2561
  }
4834
2562
  }
4835
2563
  for (const child of element.childNodes ?? []) {
@@ -4837,8 +2565,7 @@ function collectUsedTags(jayHtml) {
4837
2565
  const text = child.rawText ?? child.text ?? "";
4838
2566
  for (const expr of extractExpressions(text)) {
4839
2567
  const p = extractTagPath(expr);
4840
- if (p)
4841
- resolvePath(p, childScopes);
2568
+ if (p) resolvePath(p, childScopes);
4842
2569
  }
4843
2570
  } else if (child.nodeType === 1) {
4844
2571
  walkElement(child, childScopes);
@@ -4851,14 +2578,12 @@ function collectUsedTags(jayHtml) {
4851
2578
  function analyzeTagCoverage(jayHtml, file) {
4852
2579
  const imports = jayHtml.headlessImports;
4853
2580
  const withContracts = imports.filter((imp) => imp.contract);
4854
- if (withContracts.length === 0)
4855
- return null;
2581
+ if (withContracts.length === 0) return null;
4856
2582
  const usedTagsMap = collectUsedTags(jayHtml);
4857
2583
  const contracts = [];
4858
2584
  for (let i = 0; i < imports.length; i++) {
4859
2585
  const imp = imports[i];
4860
- if (!imp.contract)
4861
- continue;
2586
+ if (!imp.contract) continue;
4862
2587
  const allTags = flattenContractTags(imp.contract.tags);
4863
2588
  const usedSet = usedTagsMap.get(i) ?? /* @__PURE__ */ new Set();
4864
2589
  const expanded = new Set(usedSet);
@@ -4887,12 +2612,9 @@ function resolveContractTag(contract, tagPath) {
4887
2612
  let tags = contract.tags;
4888
2613
  for (let i = 0; i < segments.length; i++) {
4889
2614
  const tag = tags.find((t) => t.tag === segments[i]);
4890
- if (!tag)
4891
- return void 0;
4892
- if (i === segments.length - 1)
4893
- return tag;
4894
- if (!tag.tags)
4895
- return void 0;
2615
+ if (!tag) return void 0;
2616
+ if (i === segments.length - 1) return tag;
2617
+ if (!tag.tags) return void 0;
4896
2618
  tags = tag.tags;
4897
2619
  }
4898
2620
  return void 0;
@@ -4924,17 +2646,13 @@ function checkRefElementTypes(jayHtml, file) {
4924
2646
  importIndex = scope.importIndex;
4925
2647
  tagPath = scope.prefix ? `${scope.prefix}.${refPath}` : refPath;
4926
2648
  }
4927
- if (importIndex === void 0)
4928
- return;
2649
+ if (importIndex === void 0) return;
4929
2650
  const imp = imports[importIndex];
4930
- if (!imp.contract)
4931
- return;
2651
+ if (!imp.contract) return;
4932
2652
  const contractTag = resolveContractTag(imp.contract, tagPath);
4933
- if (!contractTag || !contractTag.elementType)
4934
- return;
2653
+ if (!contractTag || !contractTag.elementType) return;
4935
2654
  const contractTypes = contractTag.elementType;
4936
- if (contractTypes.includes("HTMLElement"))
4937
- return;
2655
+ if (contractTypes.includes("HTMLElement")) return;
4938
2656
  if (!contractTypes.includes(ref.actualType)) {
4939
2657
  const label = imp.key ? `${imp.key}.${tagPath}` : tagPath;
4940
2658
  warnings.push(
@@ -5044,8 +2762,7 @@ function checkRouteParams(parsedFile, filePath, pagesBase) {
5044
2762
  collectParams(imp.contract.params);
5045
2763
  }
5046
2764
  }
5047
- if (requiredParams.size === 0)
5048
- return [];
2765
+ if (requiredParams.size === 0) return [];
5049
2766
  const routeParams = extractRouteParams(filePath, pagesBase);
5050
2767
  const headlessProps = extractHeadlessPropsParamNames(parsedFile);
5051
2768
  const availableParams = /* @__PURE__ */ new Set([...routeParams, ...headlessProps]);
@@ -5061,11 +2778,9 @@ function checkRouteParams(parsedFile, filePath, pagesBase) {
5061
2778
  }
5062
2779
  function checkRouteToContractParams(parsedFile, filePath, pagesBase) {
5063
2780
  const routeParams = extractRouteParams(filePath, pagesBase);
5064
- if (routeParams.size === 0)
5065
- return [];
2781
+ if (routeParams.size === 0) return [];
5066
2782
  const hasAnyContract = !!parsedFile.contract || parsedFile.headlessImports.some((imp) => !!imp.contract);
5067
- if (!hasAnyContract)
5068
- return [];
2783
+ if (!hasAnyContract) return [];
5069
2784
  const declaredParams = /* @__PURE__ */ new Set();
5070
2785
  if (parsedFile.contract?.params) {
5071
2786
  for (const p of parsedFile.contract.params) {
@@ -5117,17 +2832,14 @@ function resolveBindingPhase(bindingPath, jayHtml) {
5117
2832
  const keyedImport = jayHtml.headlessImports.find((i) => i.key === root && i.contract);
5118
2833
  if (keyedImport?.contract) {
5119
2834
  const tagPath = segments.slice(1).join(".");
5120
- if (!tagPath)
5121
- return void 0;
2835
+ if (!tagPath) return void 0;
5122
2836
  const tag = resolveContractTag(keyedImport.contract, tagPath);
5123
- if (!tag)
5124
- return void 0;
2837
+ if (!tag) return void 0;
5125
2838
  return tag.phase || "slow";
5126
2839
  }
5127
2840
  if (jayHtml.contract) {
5128
2841
  const tag = resolveContractTag(jayHtml.contract, bindingPath);
5129
- if (!tag)
5130
- return void 0;
2842
+ if (!tag) return void 0;
5131
2843
  return tag.phase || "slow";
5132
2844
  }
5133
2845
  return void 0;
@@ -5178,15 +2890,12 @@ function checkHeadlessInstanceProps(jayHtml, file) {
5178
2890
  }
5179
2891
  for (const contractProp of contract.props) {
5180
2892
  const attrValue = lowerAttrs[contractProp.name.toLowerCase()];
5181
- if (!attrValue)
5182
- continue;
2893
+ if (!attrValue) continue;
5183
2894
  const bindingMatch = attrValue.match(/^\{(.+)\}$/);
5184
- if (!bindingMatch)
5185
- continue;
2895
+ if (!bindingMatch) continue;
5186
2896
  const bindingPath = bindingMatch[1];
5187
2897
  const sourcePhase = resolveBindingPhase(bindingPath, jayHtml);
5188
- if (!sourcePhase)
5189
- continue;
2898
+ if (!sourcePhase) continue;
5190
2899
  const propPhase = contractProp.phase ?? "slow";
5191
2900
  const sourceOrder = PHASE_ORDER[sourcePhase] ?? 0;
5192
2901
  const propOrder = PHASE_ORDER[propPhase] ?? 0;
@@ -5224,17 +2933,15 @@ function resolveLinkedTags(tags, contractDir) {
5224
2933
  });
5225
2934
  }
5226
2935
  function resolveContractLinks(contract, contractPath) {
5227
- if (!contractPath)
5228
- return contract;
2936
+ if (!contractPath) return contract;
5229
2937
  const contractDir = path.dirname(contractPath);
5230
2938
  return { ...contract, tags: resolveLinkedTags(contract.tags, contractDir) };
5231
2939
  }
5232
2940
  async function runPluginValidators(projectRoot, parsedFiles, errors, warnings) {
5233
- const scannedPlugins = await scanPlugins$1({ projectRoot, includeDevDeps: true });
2941
+ const scannedPlugins = await scanPlugins({ projectRoot, includeDevDeps: true });
5234
2942
  const loadedValidators = [];
5235
2943
  for (const [, plugin] of scannedPlugins) {
5236
- if (!plugin.manifest.validators)
5237
- continue;
2944
+ if (!plugin.manifest.validators) continue;
5238
2945
  for (const validatorDef of plugin.manifest.validators) {
5239
2946
  const source = `${plugin.name}/${validatorDef.name}`;
5240
2947
  let validatorFn;
@@ -5354,12 +3061,18 @@ async function validateJayFiles(options = {}) {
5354
3061
  const resolvedConfig = getConfigWithDefaults(config);
5355
3062
  const projectRoot = options.projectRoot ?? process.cwd();
5356
3063
  const scanDir = options.path ? path.resolve(options.path) : path.resolve(resolvedConfig.devServer.pagesBase);
3064
+ const componentsDir = path.resolve(resolvedConfig.devServer.componentsBase);
5357
3065
  const errors = [];
5358
3066
  const warnings = [];
5359
3067
  const coverage = [];
5360
3068
  const parsedFiles = [];
5361
- const jayHtmlFiles = await findJayFiles(scanDir);
5362
- const contractFiles = await findContractFiles(scanDir);
3069
+ const pageJayHtmlFiles = await findJayFiles(scanDir);
3070
+ const componentJayHtmlFiles = await findJayFiles(componentsDir).catch(() => []);
3071
+ const jayHtmlFiles = [...pageJayHtmlFiles, ...componentJayHtmlFiles];
3072
+ const contractFiles = [
3073
+ ...await findContractFiles(scanDir),
3074
+ ...await findContractFiles(componentsDir).catch(() => [])
3075
+ ];
5363
3076
  if (options.verbose) {
5364
3077
  getLogger().info(chalk.gray(`Scanning directory: ${scanDir}`));
5365
3078
  getLogger().info(chalk.gray(`Found ${jayHtmlFiles.length} .jay-html files`));
@@ -5607,6 +3320,14 @@ function printJayValidationResult(result, options) {
5607
3320
  } else {
5608
3321
  logger.important(chalk.red(`Validation failed — ${result.errors.length} error(s).`));
5609
3322
  }
3323
+ const totalIssues = result.errors.length + result.warnings.length + result.coverage.length;
3324
+ if (totalIssues > 0) {
3325
+ logger.important(
3326
+ chalk.gray(
3327
+ "\nSee: agent-kit/designer/validation-guide.md for how to interpret and suppress warnings."
3328
+ )
3329
+ );
3330
+ }
5610
3331
  }
5611
3332
  async function runValidate(scanPath, options) {
5612
3333
  const result = await validateJayFiles({
@@ -5799,8 +3520,7 @@ async function ensureAgentKitDocs(projectRoot, _force, mode) {
5799
3520
  const sharedDirs = ["contracts"];
5800
3521
  for (const dir of sharedDirs) {
5801
3522
  const srcDir = path$1.join(templateDir, dir);
5802
- if (!fsSync.existsSync(srcDir))
5803
- continue;
3523
+ if (!fsSync.existsSync(srcDir)) continue;
5804
3524
  await copyDirRecursive(srcDir, path$1.join(agentKitDir, dir));
5805
3525
  getLogger().info(chalk.gray(` Created agent-kit/${dir}/`));
5806
3526
  }
@@ -5819,14 +3539,13 @@ async function copyDirRecursive(src, dest) {
5819
3539
  }
5820
3540
  }
5821
3541
  async function mergePluginAgentKitGuides(projectRoot, mode) {
5822
- const plugins = await scanPlugins$1({ projectRoot, includeDevDeps: true });
3542
+ const plugins = await scanPlugins({ projectRoot, includeDevDeps: true });
5823
3543
  const agentKitDir = path$1.join(projectRoot, "agent-kit");
5824
3544
  const roles = mode && ALL_ROLES.includes(mode) ? [mode] : ALL_ROLES;
5825
3545
  const copiedPerRole = /* @__PURE__ */ new Map();
5826
3546
  for (const [, plugin] of plugins) {
5827
3547
  const pluginAgentKitDir = path$1.join(plugin.pluginPath, "agent-kit");
5828
- if (!fsSync.existsSync(pluginAgentKitDir))
5829
- continue;
3548
+ if (!fsSync.existsSync(pluginAgentKitDir)) continue;
5830
3549
  for (const role of roles) {
5831
3550
  const roleSourceDir = path$1.join(pluginAgentKitDir, role);
5832
3551
  let files;
@@ -5837,8 +3556,7 @@ async function mergePluginAgentKitGuides(projectRoot, mode) {
5837
3556
  } catch {
5838
3557
  continue;
5839
3558
  }
5840
- if (files.length === 0)
5841
- continue;
3559
+ if (files.length === 0) continue;
5842
3560
  const roleOutputDir = path$1.join(agentKitDir, role);
5843
3561
  await fs$1.mkdir(roleOutputDir, { recursive: true });
5844
3562
  for (const filename of files) {
@@ -5861,8 +3579,7 @@ async function mergePluginAgentKitGuides(projectRoot, mode) {
5861
3579
  }
5862
3580
  } catch {
5863
3581
  }
5864
- if (!copiedPerRole.has(role))
5865
- copiedPerRole.set(role, []);
3582
+ if (!copiedPerRole.has(role)) copiedPerRole.set(role, []);
5866
3583
  copiedPerRole.get(role).push({ filename, pluginName: plugin.name, description });
5867
3584
  getLogger().info(
5868
3585
  chalk.gray(
@@ -5874,8 +3591,7 @@ async function mergePluginAgentKitGuides(projectRoot, mode) {
5874
3591
  }
5875
3592
  for (const [role, entries] of copiedPerRole) {
5876
3593
  const instructionsPath = path$1.join(agentKitDir, role, "INSTRUCTIONS.md");
5877
- if (!fsSync.existsSync(instructionsPath))
5878
- continue;
3594
+ if (!fsSync.existsSync(instructionsPath)) continue;
5879
3595
  const lines = [
5880
3596
  "",
5881
3597
  "## Plugin-Contributed Guides",
@@ -5897,8 +3613,7 @@ async function generatePluginAgentKit(projectRoot, options, initErrors, viteServ
5897
3613
  verbose: options.verbose,
5898
3614
  pluginFilter: options.plugin
5899
3615
  });
5900
- if (plugins.length === 0)
5901
- return;
3616
+ if (plugins.length === 0) return;
5902
3617
  const logger = getLogger();
5903
3618
  logger.important("");
5904
3619
  logger.important(chalk.bold("Generating plugin agent-kit data..."));
@@ -6127,20 +3842,17 @@ function createAnswersFilePrompt(answers, pluginName) {
6127
3842
  return {
6128
3843
  async input(options) {
6129
3844
  const value = answers[options.key];
6130
- if (value !== void 0)
6131
- return value;
3845
+ if (value !== void 0) return value;
6132
3846
  throw new SetupNeedsAnswerError(pluginName, options.key, "input", options.message);
6133
3847
  },
6134
3848
  async confirm(options) {
6135
3849
  const value = answers[options.key];
6136
- if (value !== void 0)
6137
- return value === "true" || value === "yes";
3850
+ if (value !== void 0) return value === "true" || value === "yes";
6138
3851
  throw new SetupNeedsAnswerError(pluginName, options.key, "confirm", options.message);
6139
3852
  },
6140
3853
  async select(options) {
6141
3854
  const value = answers[options.key];
6142
- if (value !== void 0)
6143
- return value;
3855
+ if (value !== void 0) return value;
6144
3856
  throw new SetupNeedsAnswerError(
6145
3857
  pluginName,
6146
3858
  options.key,
@@ -6170,19 +3882,17 @@ function createDefaultPrompt(pluginName) {
6170
3882
  }
6171
3883
  };
6172
3884
  }
6173
- async function runSetup(pluginFilter, options, projectRoot, initializeServices) {
3885
+ async function runSetup(pluginFilter, options, projectRoot) {
6174
3886
  let viteServer;
6175
3887
  try {
6176
3888
  const logger = getLogger();
6177
- const path2 = await import("node:path");
6178
3889
  const jayConfig = loadConfig();
6179
- const configDir = path2.resolve(projectRoot, jayConfig.devServer?.configBase || "./config");
3890
+ const configDir = path$1.resolve(projectRoot, jayConfig.devServer?.configBase || "./config");
6180
3891
  logger.important(chalk.bold("\n🔧 Setting up plugins...\n"));
6181
3892
  if (options.verbose) {
6182
3893
  logger.info("Starting Vite for TypeScript support...");
6183
3894
  }
6184
3895
  viteServer = await createViteForCli({ projectRoot });
6185
- const { discoverPluginsWithSetup, executePluginSetup } = await import("@jay-framework/stack-server-runtime");
6186
3896
  const pluginsWithSetup = await discoverPluginsWithSetup({
6187
3897
  projectRoot,
6188
3898
  verbose: options.verbose,
@@ -6203,12 +3913,9 @@ async function runSetup(pluginFilter, options, projectRoot, initializeServices)
6203
3913
  `Found ${pluginsWithSetup.length} plugin(s) with setup: ${pluginsWithSetup.map((p) => p.name).join(", ")}`
6204
3914
  );
6205
3915
  }
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
- }
3916
+ const allPluginsWithInit = sortPluginsByDependencies(
3917
+ await discoverPluginsWithInit({ projectRoot, verbose: options.verbose })
3918
+ );
6212
3919
  const interactive = options.interactive === true;
6213
3920
  let answersMap;
6214
3921
  if (options.answers) {
@@ -6230,14 +3937,13 @@ async function runSetup(pluginFilter, options, projectRoot, initializeServices)
6230
3937
  force: options.force ?? false,
6231
3938
  interactive,
6232
3939
  prompt,
6233
- initError: initErrors.get(plugin.name),
3940
+ initError: void 0,
6234
3941
  viteServer,
6235
3942
  verbose: options.verbose
6236
3943
  });
6237
3944
  switch (result.status) {
6238
3945
  case "configured":
6239
3946
  configured++;
6240
- logger.important(chalk.green(" ✅ Services verified"));
6241
3947
  if (result.configCreated?.length) {
6242
3948
  for (const cfg of result.configCreated) {
6243
3949
  logger.important(chalk.green(` ✅ Created ${cfg}`));
@@ -6246,6 +3952,7 @@ async function runSetup(pluginFilter, options, projectRoot, initializeServices)
6246
3952
  if (result.message) {
6247
3953
  logger.important(chalk.gray(` ${result.message}`));
6248
3954
  }
3955
+ await initPlugin(plugin.name, allPluginsWithInit, viteServer, logger);
6249
3956
  break;
6250
3957
  case "needs-config":
6251
3958
  needsConfig++;
@@ -6304,13 +4011,11 @@ async function runSetup(pluginFilter, options, projectRoot, initializeServices)
6304
4011
  }
6305
4012
  logger.important("");
6306
4013
  }
4014
+ await runProjectInit(projectRoot, viteServer);
6307
4015
  const parts = [];
6308
- if (configured > 0)
6309
- parts.push(`${configured} configured`);
6310
- if (needsConfig > 0)
6311
- parts.push(`${needsConfig} needs config`);
6312
- if (errors > 0)
6313
- parts.push(`${errors} error(s)`);
4016
+ if (configured > 0) parts.push(`${configured} configured`);
4017
+ if (needsConfig > 0) parts.push(`${needsConfig} needs config`);
4018
+ if (errors > 0) parts.push(`${errors} error(s)`);
6314
4019
  logger.important(`Setup complete: ${parts.join(", ")}`);
6315
4020
  if (errors > 0) {
6316
4021
  process.exit(1);
@@ -6327,6 +4032,35 @@ async function runSetup(pluginFilter, options, projectRoot, initializeServices)
6327
4032
  }
6328
4033
  }
6329
4034
  }
4035
+ async function initPlugin(pluginName, allPluginsWithInit, viteServer, logger) {
4036
+ const pluginInit = allPluginsWithInit.filter((p) => p.name === pluginName);
4037
+ if (pluginInit.length === 0) return;
4038
+ const initErrors = await executePluginServerInits(pluginInit, viteServer, false, true);
4039
+ if (initErrors.size > 0) {
4040
+ for (const [, err] of initErrors) {
4041
+ logger.important(chalk.yellow(` ⚠️ Init after setup: ${err.message}`));
4042
+ }
4043
+ } else {
4044
+ logger.important(chalk.green(` ✅ Services initialized`));
4045
+ }
4046
+ }
4047
+ async function runProjectInit(projectRoot, viteServer) {
4048
+ try {
4049
+ const initPathTs = path$1.join(projectRoot, "src", "init.ts");
4050
+ const initPathJs = path$1.join(projectRoot, "src", "init.js");
4051
+ let initModule;
4052
+ if (fsSync.existsSync(initPathTs) && viteServer) {
4053
+ initModule = await viteServer.ssrLoadModule(initPathTs);
4054
+ } else if (fsSync.existsSync(initPathJs)) {
4055
+ initModule = await import(initPathJs);
4056
+ }
4057
+ if (initModule?.init?._serverInit) {
4058
+ await initModule.init._serverInit();
4059
+ }
4060
+ await runInitCallbacks();
4061
+ } catch {
4062
+ }
4063
+ }
6330
4064
  async function runCommand(commandRef, rawArgs, options, projectRoot, initializeServices) {
6331
4065
  let viteServer;
6332
4066
  try {
@@ -6440,8 +4174,7 @@ function printCommandList(commands) {
6440
4174
  logger.important("\nAvailable plugin commands:\n");
6441
4175
  const byPlugin = /* @__PURE__ */ new Map();
6442
4176
  for (const cmd of commands) {
6443
- if (!byPlugin.has(cmd.pluginName))
6444
- byPlugin.set(cmd.pluginName, []);
4177
+ if (!byPlugin.has(cmd.pluginName)) byPlugin.set(cmd.pluginName, []);
6445
4178
  byPlugin.get(cmd.pluginName).push(cmd);
6446
4179
  }
6447
4180
  for (const [pluginName, cmds] of byPlugin) {
@@ -6497,8 +4230,7 @@ program.command("build").description("Build production artifacts").option("-p, -
6497
4230
  await runBuild(options.path, options);
6498
4231
  } catch (error) {
6499
4232
  getLogger().error(chalk.red("Build failed:") + " " + error.message);
6500
- if (error.stack)
6501
- getLogger().error(error.stack);
4233
+ if (error.stack) getLogger().error(error.stack);
6502
4234
  process.exit(1);
6503
4235
  }
6504
4236
  });
@@ -6507,8 +4239,7 @@ program.command("serve").description("Start production server").option("-p, --pa
6507
4239
  await runServe(options.path, options);
6508
4240
  } catch (error) {
6509
4241
  getLogger().error(chalk.red("Server failed:") + " " + error.message);
6510
- if (error.stack)
6511
- getLogger().error(error.stack);
4242
+ if (error.stack) getLogger().error(error.stack);
6512
4243
  process.exit(1);
6513
4244
  }
6514
4245
  });
@@ -6517,8 +4248,7 @@ program.command("rebuild").description("Rebuild instances by contract, route, or
6517
4248
  await runRebuild(options.path, options);
6518
4249
  } catch (error) {
6519
4250
  getLogger().error(chalk.red("Rebuild failed:") + " " + error.message);
6520
- if (error.stack)
6521
- getLogger().error(error.stack);
4251
+ if (error.stack) getLogger().error(error.stack);
6522
4252
  process.exit(1);
6523
4253
  }
6524
4254
  });
@@ -6556,7 +4286,7 @@ program.command("validate-plugin").description("Validate a Jay Stack plugin pack
6556
4286
  }
6557
4287
  });
6558
4288
  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);
4289
+ await runSetup(plugin, options, process.cwd());
6560
4290
  });
6561
4291
  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
4292
  "-m, --mode <role>",
@@ -6582,11 +4312,7 @@ if (!process.argv.slice(2).length) {
6582
4312
  program.outputHelp();
6583
4313
  }
6584
4314
  export {
6585
- createEditorHandlers,
6586
4315
  getConfigWithDefaults,
6587
- getRegisteredVendors,
6588
- getVendor,
6589
- hasVendor,
6590
4316
  listContracts2 as listContracts,
6591
4317
  loadConfig,
6592
4318
  materializeContracts2 as materializeContracts,