@jay-framework/jay-stack-cli 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -0
- package/agent-kit-template/INSTRUCTIONS.md +120 -0
- package/agent-kit-template/cli-commands.md +229 -0
- package/agent-kit-template/contracts-and-plugins.md +293 -0
- package/agent-kit-template/jay-html-syntax.md +312 -0
- package/agent-kit-template/project-structure.md +242 -0
- package/agent-kit-template/routing.md +112 -0
- package/dist/index.d.ts +82 -2
- package/dist/index.js +2620 -629
- package/lib/vendors/README.md +510 -0
- package/lib/vendors/figma/README.md +396 -0
- package/package.json +13 -8
- package/test/vendors/figma/fixtures/README.md +164 -0
package/dist/index.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import express from "express";
|
|
3
|
-
import { mkDevServer } from "@jay-framework/dev-server";
|
|
3
|
+
import { mkDevServer, createViteForCli } from "@jay-framework/dev-server";
|
|
4
4
|
import { createEditorServer } from "@jay-framework/editor-server";
|
|
5
5
|
import getPort from "get-port";
|
|
6
6
|
import path from "path";
|
|
7
7
|
import fs, { promises } from "fs";
|
|
8
8
|
import YAML from "yaml";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
9
|
+
import { getLogger, createDevLogger, setDevLogger } from "@jay-framework/logger";
|
|
10
|
+
import { parseJayFile, JAY_IMPORT_RESOLVER, generateElementDefinitionFile, ContractTagType, parseContract, generateElementFile } from "@jay-framework/compiler-jay-html";
|
|
11
|
+
import { JAY_CONTRACT_EXTENSION, JAY_EXTENSION, resolvePluginManifest, LOCAL_PLUGIN_PATH, JayAtomicType, JayEnumType, loadPluginManifest, RuntimeMode, GenerateTarget } from "@jay-framework/compiler-shared";
|
|
12
|
+
import { listContracts, materializeContracts } from "@jay-framework/stack-server-runtime";
|
|
13
|
+
import { listContracts as listContracts2, materializeContracts as materializeContracts2 } from "@jay-framework/stack-server-runtime";
|
|
13
14
|
import { Command } from "commander";
|
|
14
15
|
import chalk from "chalk";
|
|
15
16
|
import { glob } from "glob";
|
|
@@ -44,7 +45,7 @@ function loadConfig() {
|
|
|
44
45
|
}
|
|
45
46
|
};
|
|
46
47
|
} catch (error) {
|
|
47
|
-
|
|
48
|
+
getLogger().warn(`Failed to parse .jay YAML config file, using defaults: ${error}`);
|
|
48
49
|
return DEFAULT_CONFIG;
|
|
49
50
|
}
|
|
50
51
|
}
|
|
@@ -82,12 +83,1473 @@ function updateConfig(updates) {
|
|
|
82
83
|
const yamlContent = YAML.stringify(updatedConfig, { indent: 2 });
|
|
83
84
|
fs.writeFileSync(configPath, yamlContent);
|
|
84
85
|
} catch (error) {
|
|
85
|
-
|
|
86
|
+
getLogger().warn(`Failed to update .jay config file: ${error}`);
|
|
86
87
|
}
|
|
87
88
|
}
|
|
89
|
+
function rgbToHex(color, opacity) {
|
|
90
|
+
const r = Math.round(color.r * 255);
|
|
91
|
+
const g = Math.round(color.g * 255);
|
|
92
|
+
const b = Math.round(color.b * 255);
|
|
93
|
+
if (opacity !== void 0 && opacity < 1) {
|
|
94
|
+
const alphaHex = Math.round(opacity * 255).toString(16).padStart(2, "0");
|
|
95
|
+
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}${alphaHex}`;
|
|
96
|
+
} else {
|
|
97
|
+
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function getPositionType(node) {
|
|
101
|
+
if (node.layoutPositioning === "ABSOLUTE") {
|
|
102
|
+
return "absolute";
|
|
103
|
+
}
|
|
104
|
+
if (node.parentOverflowDirection && node.parentOverflowDirection !== "NONE") {
|
|
105
|
+
if (node.parentNumberOfFixedChildren && node.parentChildIndex !== void 0) {
|
|
106
|
+
if (node.parentChildIndex >= 0 && node.parentChildIndex < node.parentNumberOfFixedChildren) {
|
|
107
|
+
return "sticky";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (node.scrollBehavior === "FIXED" && node.parentLayoutMode === "NONE") {
|
|
112
|
+
return "fixed";
|
|
113
|
+
}
|
|
114
|
+
if (node.parentType === "SECTION") {
|
|
115
|
+
return "absolute";
|
|
116
|
+
}
|
|
117
|
+
if (node.parentLayoutMode === "NONE") {
|
|
118
|
+
return "absolute";
|
|
119
|
+
}
|
|
120
|
+
if (node.parentLayoutMode && (node.parentLayoutMode === "HORIZONTAL" || node.parentLayoutMode === "VERTICAL")) {
|
|
121
|
+
return "relative";
|
|
122
|
+
}
|
|
123
|
+
return "static";
|
|
124
|
+
}
|
|
125
|
+
function getPositionStyle(node) {
|
|
126
|
+
if (node.type === "COMPONENT") {
|
|
127
|
+
return "";
|
|
128
|
+
}
|
|
129
|
+
const positionType = getPositionType(node);
|
|
130
|
+
if (positionType === "static") {
|
|
131
|
+
return "";
|
|
132
|
+
}
|
|
133
|
+
if (positionType === "absolute" || positionType === "fixed") {
|
|
134
|
+
const top = node.y !== void 0 ? node.y : 0;
|
|
135
|
+
const left = node.x !== void 0 ? node.x : 0;
|
|
136
|
+
return `position: ${positionType};top: ${top}px;left: ${left}px;`;
|
|
137
|
+
}
|
|
138
|
+
if (positionType === "sticky") {
|
|
139
|
+
return `position: ${positionType};top: 0;z-index: 10;`;
|
|
140
|
+
}
|
|
141
|
+
return `position: ${positionType};`;
|
|
142
|
+
}
|
|
143
|
+
function getAutoLayoutChildSizeStyles(node) {
|
|
144
|
+
if (!node.parentLayoutMode || node.parentLayoutMode === "NONE") {
|
|
145
|
+
const width2 = node.width !== void 0 ? node.width : 0;
|
|
146
|
+
const height2 = node.height !== void 0 ? node.height : 0;
|
|
147
|
+
return `width: ${width2}px;height: ${height2}px;`;
|
|
148
|
+
}
|
|
149
|
+
let styles = "";
|
|
150
|
+
if (!node.layoutGrow && !node.layoutAlign) {
|
|
151
|
+
const width2 = node.width !== void 0 ? node.width : 0;
|
|
152
|
+
const height2 = node.height !== void 0 ? node.height : 0;
|
|
153
|
+
return `width: ${width2}px;height: ${height2}px;`;
|
|
154
|
+
}
|
|
155
|
+
const isHorizontalLayout = node.parentLayoutMode === "HORIZONTAL";
|
|
156
|
+
const width = node.width !== void 0 ? node.width : 0;
|
|
157
|
+
const height = node.height !== void 0 ? node.height : 0;
|
|
158
|
+
if (node.layoutSizingHorizontal) {
|
|
159
|
+
switch (node.layoutSizingHorizontal) {
|
|
160
|
+
case "FIXED":
|
|
161
|
+
styles += `width: ${width}px;`;
|
|
162
|
+
break;
|
|
163
|
+
case "HUG":
|
|
164
|
+
styles += "width: fit-content;";
|
|
165
|
+
break;
|
|
166
|
+
case "FILL":
|
|
167
|
+
if (node.type === "TEXT") {
|
|
168
|
+
styles += "width: auto;";
|
|
169
|
+
} else if (isHorizontalLayout) {
|
|
170
|
+
styles += "flex-grow: 1;";
|
|
171
|
+
} else {
|
|
172
|
+
styles += "width: 100%;";
|
|
173
|
+
}
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
if (isHorizontalLayout && node.layoutGrow && node.layoutGrow > 0) {
|
|
178
|
+
styles += `flex-grow: ${node.layoutGrow};width: 0;`;
|
|
179
|
+
} else if (!isHorizontalLayout && node.layoutAlign === "STRETCH") {
|
|
180
|
+
styles += "width: 100%;";
|
|
181
|
+
} else {
|
|
182
|
+
styles += `width: ${width}px;`;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (node.layoutSizingVertical) {
|
|
186
|
+
switch (node.layoutSizingVertical) {
|
|
187
|
+
case "FIXED":
|
|
188
|
+
styles += `height: ${height}px;`;
|
|
189
|
+
break;
|
|
190
|
+
case "HUG":
|
|
191
|
+
styles += "height: fit-content;";
|
|
192
|
+
break;
|
|
193
|
+
case "FILL":
|
|
194
|
+
if (!isHorizontalLayout) {
|
|
195
|
+
styles += "flex-grow: 1;";
|
|
196
|
+
} else {
|
|
197
|
+
styles += "height: 100%;";
|
|
198
|
+
}
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
if (!isHorizontalLayout && node.layoutGrow && node.layoutGrow > 0) {
|
|
203
|
+
styles += `flex-grow: ${node.layoutGrow};height: 0;`;
|
|
204
|
+
} else if (isHorizontalLayout && node.layoutAlign === "STRETCH") {
|
|
205
|
+
styles += "height: 100%;";
|
|
206
|
+
} else {
|
|
207
|
+
styles += `height: ${height}px;`;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (node.layoutAlign) {
|
|
211
|
+
switch (node.layoutAlign) {
|
|
212
|
+
case "MIN":
|
|
213
|
+
styles += "align-self: flex-start;";
|
|
214
|
+
break;
|
|
215
|
+
case "CENTER":
|
|
216
|
+
styles += "align-self: center;";
|
|
217
|
+
break;
|
|
218
|
+
case "MAX":
|
|
219
|
+
styles += "align-self: flex-end;";
|
|
220
|
+
break;
|
|
221
|
+
case "STRETCH":
|
|
222
|
+
styles += "align-self: stretch;";
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return styles;
|
|
227
|
+
}
|
|
228
|
+
function getNodeSizeStyles(node) {
|
|
229
|
+
if (node.parentType === "SECTION") {
|
|
230
|
+
const height = node.height !== void 0 ? node.height : 0;
|
|
231
|
+
return `width: 100%;height: ${height}px;`;
|
|
232
|
+
}
|
|
233
|
+
return getAutoLayoutChildSizeStyles(node);
|
|
234
|
+
}
|
|
235
|
+
function getCommonStyles(node) {
|
|
236
|
+
let styles = "";
|
|
237
|
+
const transformStyles = [];
|
|
238
|
+
if (node.opacity !== void 0 && node.opacity < 1) {
|
|
239
|
+
styles += `opacity: ${node.opacity};`;
|
|
240
|
+
}
|
|
241
|
+
if (node.rotation !== void 0 && node.rotation !== 0) {
|
|
242
|
+
transformStyles.push(`rotate(${node.rotation}deg)`);
|
|
243
|
+
}
|
|
244
|
+
if (node.effects && Array.isArray(node.effects) && node.effects.length > 0) {
|
|
245
|
+
const visibleEffects = node.effects.filter((e) => e.visible !== false).reverse();
|
|
246
|
+
const filterFunctions = [];
|
|
247
|
+
const boxShadows = [];
|
|
248
|
+
for (const effect of visibleEffects) {
|
|
249
|
+
switch (effect.type) {
|
|
250
|
+
case "DROP_SHADOW":
|
|
251
|
+
case "INNER_SHADOW": {
|
|
252
|
+
if (effect.color && effect.offset && effect.radius !== void 0) {
|
|
253
|
+
const { offset, radius, color, spread } = effect;
|
|
254
|
+
const shadowColor = `rgba(${Math.round(color.r * 255)}, ${Math.round(color.g * 255)}, ${Math.round(color.b * 255)}, ${color.a ?? 1})`;
|
|
255
|
+
const inset = effect.type === "INNER_SHADOW" ? "inset " : "";
|
|
256
|
+
boxShadows.push(
|
|
257
|
+
`${inset}${offset.x}px ${offset.y}px ${radius}px ${spread ?? 0}px ${shadowColor}`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
case "LAYER_BLUR":
|
|
263
|
+
if (effect.radius !== void 0) {
|
|
264
|
+
filterFunctions.push(`blur(${effect.radius}px)`);
|
|
265
|
+
}
|
|
266
|
+
break;
|
|
267
|
+
case "BACKGROUND_BLUR":
|
|
268
|
+
if (effect.radius !== void 0) {
|
|
269
|
+
styles += `backdrop-filter: blur(${effect.radius}px);`;
|
|
270
|
+
styles += `-webkit-backdrop-filter: blur(${effect.radius}px);`;
|
|
271
|
+
}
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (boxShadows.length > 0) {
|
|
276
|
+
styles += `box-shadow: ${boxShadows.join(", ")};`;
|
|
277
|
+
}
|
|
278
|
+
if (filterFunctions.length > 0) {
|
|
279
|
+
styles += `filter: ${filterFunctions.join(" ")};`;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (transformStyles.length > 0) {
|
|
283
|
+
styles += `transform: ${transformStyles.join(" ")};`;
|
|
284
|
+
const width = node.width !== void 0 ? node.width : 0;
|
|
285
|
+
const height = node.height !== void 0 ? node.height : 0;
|
|
286
|
+
styles += `transform-origin: ${width / 2}px ${height / 2}px;`;
|
|
287
|
+
}
|
|
288
|
+
return styles;
|
|
289
|
+
}
|
|
290
|
+
function getBorderRadius(node) {
|
|
291
|
+
if (typeof node.cornerRadius === "number") {
|
|
292
|
+
return `border-radius: ${node.cornerRadius}px;`;
|
|
293
|
+
} else if (node.cornerRadius === "MIXED" && node.topLeftRadius !== void 0) {
|
|
294
|
+
return `border-radius: ${node.topLeftRadius}px ${node.topRightRadius}px ${node.bottomRightRadius}px ${node.bottomLeftRadius}px;`;
|
|
295
|
+
}
|
|
296
|
+
return "border-radius: 0px;";
|
|
297
|
+
}
|
|
298
|
+
function getAutoLayoutStyles(node) {
|
|
299
|
+
if (node.layoutMode === "NONE" || !node.layoutMode) {
|
|
300
|
+
return "";
|
|
301
|
+
}
|
|
302
|
+
let flexStyles = "display: flex;";
|
|
303
|
+
if (node.layoutMode === "HORIZONTAL") {
|
|
304
|
+
flexStyles += "flex-direction: row;";
|
|
305
|
+
} else if (node.layoutMode === "VERTICAL") {
|
|
306
|
+
flexStyles += "flex-direction: column;";
|
|
307
|
+
}
|
|
308
|
+
if (node.primaryAxisAlignItems) {
|
|
309
|
+
switch (node.primaryAxisAlignItems) {
|
|
310
|
+
case "MIN":
|
|
311
|
+
flexStyles += "justify-content: flex-start;";
|
|
312
|
+
break;
|
|
313
|
+
case "CENTER":
|
|
314
|
+
flexStyles += "justify-content: center;";
|
|
315
|
+
break;
|
|
316
|
+
case "MAX":
|
|
317
|
+
flexStyles += "justify-content: flex-end;";
|
|
318
|
+
break;
|
|
319
|
+
case "SPACE_BETWEEN":
|
|
320
|
+
flexStyles += "justify-content: space-between;";
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (node.counterAxisAlignItems) {
|
|
325
|
+
switch (node.counterAxisAlignItems) {
|
|
326
|
+
case "MIN":
|
|
327
|
+
flexStyles += "align-items: flex-start;";
|
|
328
|
+
break;
|
|
329
|
+
case "CENTER":
|
|
330
|
+
flexStyles += "align-items: center;";
|
|
331
|
+
break;
|
|
332
|
+
case "MAX":
|
|
333
|
+
flexStyles += "align-items: flex-end;";
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (typeof node.itemSpacing === "number") {
|
|
338
|
+
flexStyles += `gap: ${node.itemSpacing}px;`;
|
|
339
|
+
}
|
|
340
|
+
if (typeof node.paddingLeft === "number")
|
|
341
|
+
flexStyles += `padding-left: ${node.paddingLeft}px;`;
|
|
342
|
+
if (typeof node.paddingRight === "number")
|
|
343
|
+
flexStyles += `padding-right: ${node.paddingRight}px;`;
|
|
344
|
+
if (typeof node.paddingTop === "number")
|
|
345
|
+
flexStyles += `padding-top: ${node.paddingTop}px;`;
|
|
346
|
+
if (typeof node.paddingBottom === "number")
|
|
347
|
+
flexStyles += `padding-bottom: ${node.paddingBottom}px;`;
|
|
348
|
+
return flexStyles;
|
|
349
|
+
}
|
|
350
|
+
function getOverflowStyles(node) {
|
|
351
|
+
let overflowStyles = "";
|
|
352
|
+
const shouldClip = node.clipsContent;
|
|
353
|
+
const overflowDirection = node.overflowDirection || "NONE";
|
|
354
|
+
switch (overflowDirection) {
|
|
355
|
+
case "HORIZONTAL":
|
|
356
|
+
overflowStyles += shouldClip ? "overflow-x: auto; overflow-y: hidden;" : "overflow-x: auto; overflow-y: visible;";
|
|
357
|
+
break;
|
|
358
|
+
case "VERTICAL":
|
|
359
|
+
overflowStyles += shouldClip ? "overflow-x: hidden; overflow-y: auto;" : "overflow-x: visible; overflow-y: auto;";
|
|
360
|
+
break;
|
|
361
|
+
case "BOTH":
|
|
362
|
+
overflowStyles += "overflow: auto;";
|
|
363
|
+
break;
|
|
364
|
+
case "NONE":
|
|
365
|
+
default:
|
|
366
|
+
overflowStyles += shouldClip ? "overflow: hidden;" : "overflow: visible;";
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
if (overflowDirection !== "NONE") {
|
|
370
|
+
overflowStyles += "scrollbar-width: thin; scrollbar-color: rgba(0, 0, 0, 0.3) transparent;";
|
|
371
|
+
}
|
|
372
|
+
return overflowStyles;
|
|
373
|
+
}
|
|
374
|
+
function getBackgroundFillsStyle(node) {
|
|
375
|
+
if (!node.fills || !Array.isArray(node.fills) || node.fills.length === 0) {
|
|
376
|
+
return "background: transparent;";
|
|
377
|
+
}
|
|
378
|
+
const backgrounds = [];
|
|
379
|
+
const backgroundSizes = [];
|
|
380
|
+
const backgroundPositions = [];
|
|
381
|
+
const backgroundRepeats = [];
|
|
382
|
+
for (const fill of [...node.fills].reverse()) {
|
|
383
|
+
if (fill.visible === false)
|
|
384
|
+
continue;
|
|
385
|
+
if (fill.type === "SOLID" && fill.color) {
|
|
386
|
+
const { r, g, b } = fill.color;
|
|
387
|
+
const opacity = fill.opacity !== void 0 ? fill.opacity : 1;
|
|
388
|
+
backgrounds.push(
|
|
389
|
+
`linear-gradient(rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, ${opacity}), rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, ${opacity}))`
|
|
390
|
+
);
|
|
391
|
+
backgroundSizes.push("100% 100%");
|
|
392
|
+
backgroundPositions.push("center");
|
|
393
|
+
backgroundRepeats.push("no-repeat");
|
|
394
|
+
} else if (fill.type === "IMAGE") {
|
|
395
|
+
console.warn("Image fills are not yet supported in vendor conversion");
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (backgrounds.length === 0) {
|
|
399
|
+
return "background: transparent;";
|
|
400
|
+
}
|
|
401
|
+
let style = `background-image: ${backgrounds.join(", ")};`;
|
|
402
|
+
style += `background-size: ${backgroundSizes.join(", ")};`;
|
|
403
|
+
style += `background-position: ${backgroundPositions.join(", ")};`;
|
|
404
|
+
style += `background-repeat: ${backgroundRepeats.join(", ")};`;
|
|
405
|
+
return style;
|
|
406
|
+
}
|
|
407
|
+
function getStrokeStyles(node) {
|
|
408
|
+
if (!node.strokes || node.strokes.length === 0) {
|
|
409
|
+
return "";
|
|
410
|
+
}
|
|
411
|
+
const visibleStrokes = node.strokes.filter((s) => s.visible !== false);
|
|
412
|
+
if (visibleStrokes.length === 0) {
|
|
413
|
+
return "";
|
|
414
|
+
}
|
|
415
|
+
const stroke = visibleStrokes[0];
|
|
416
|
+
let cssProps = [];
|
|
417
|
+
if (stroke.type === "SOLID" && stroke.color) {
|
|
418
|
+
const r = Math.round(stroke.color.r * 255);
|
|
419
|
+
const g = Math.round(stroke.color.g * 255);
|
|
420
|
+
const b = Math.round(stroke.color.b * 255);
|
|
421
|
+
const a = stroke.opacity !== void 0 ? stroke.opacity : 1;
|
|
422
|
+
if (a < 1) {
|
|
423
|
+
cssProps.push(`border-color: rgba(${r}, ${g}, ${b}, ${a.toFixed(2)});`);
|
|
424
|
+
} else {
|
|
425
|
+
cssProps.push(`border-color: rgb(${r}, ${g}, ${b});`);
|
|
426
|
+
}
|
|
427
|
+
} else {
|
|
428
|
+
return "";
|
|
429
|
+
}
|
|
430
|
+
const top = typeof node.strokeTopWeight === "number" ? node.strokeTopWeight : typeof node.strokeWeight === "number" ? node.strokeWeight : 0;
|
|
431
|
+
const right = typeof node.strokeRightWeight === "number" ? node.strokeRightWeight : typeof node.strokeWeight === "number" ? node.strokeWeight : 0;
|
|
432
|
+
const bottom = typeof node.strokeBottomWeight === "number" ? node.strokeBottomWeight : typeof node.strokeWeight === "number" ? node.strokeWeight : 0;
|
|
433
|
+
const left = typeof node.strokeLeftWeight === "number" ? node.strokeLeftWeight : typeof node.strokeWeight === "number" ? node.strokeWeight : 0;
|
|
434
|
+
if (top !== 0 || right !== 0 || bottom !== 0 || left !== 0) {
|
|
435
|
+
if (top === right && right === bottom && bottom === left) {
|
|
436
|
+
cssProps.push(`border-width: ${top}px;`);
|
|
437
|
+
} else {
|
|
438
|
+
cssProps.push(`border-width: ${top}px ${right}px ${bottom}px ${left}px;`);
|
|
439
|
+
}
|
|
440
|
+
if (node.dashPattern && node.dashPattern.length > 0) {
|
|
441
|
+
cssProps.push("border-style: dashed;");
|
|
442
|
+
} else {
|
|
443
|
+
cssProps.push("border-style: solid;");
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return cssProps.join(" ");
|
|
447
|
+
}
|
|
448
|
+
function getFrameSizeStyles(node) {
|
|
449
|
+
const isTopLevel = node.parentType === "SECTION";
|
|
450
|
+
if (isTopLevel) {
|
|
451
|
+
const height2 = node.height !== void 0 ? node.height : 0;
|
|
452
|
+
return `width: 100%;height: ${height2}px;`;
|
|
453
|
+
}
|
|
454
|
+
if (node.layoutMode === "NONE" || !node.layoutMode) {
|
|
455
|
+
const width2 = node.width !== void 0 ? node.width : 0;
|
|
456
|
+
const height2 = node.height !== void 0 ? node.height : 0;
|
|
457
|
+
return `width: ${width2}px;height: ${height2}px;`;
|
|
458
|
+
}
|
|
459
|
+
let sizeStyles = "";
|
|
460
|
+
const width = node.width !== void 0 ? node.width : 0;
|
|
461
|
+
const height = node.height !== void 0 ? node.height : 0;
|
|
462
|
+
if (node.layoutSizingHorizontal) {
|
|
463
|
+
switch (node.layoutSizingHorizontal) {
|
|
464
|
+
case "FIXED":
|
|
465
|
+
sizeStyles += `width: ${width}px;`;
|
|
466
|
+
break;
|
|
467
|
+
case "HUG":
|
|
468
|
+
sizeStyles += "width: fit-content;";
|
|
469
|
+
break;
|
|
470
|
+
case "FILL":
|
|
471
|
+
sizeStyles += "width: 100%;";
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
} else {
|
|
475
|
+
sizeStyles += `width: ${width}px;`;
|
|
476
|
+
}
|
|
477
|
+
if (node.layoutSizingVertical) {
|
|
478
|
+
switch (node.layoutSizingVertical) {
|
|
479
|
+
case "FIXED":
|
|
480
|
+
sizeStyles += `height: ${height}px;`;
|
|
481
|
+
break;
|
|
482
|
+
case "HUG":
|
|
483
|
+
sizeStyles += "height: fit-content;";
|
|
484
|
+
break;
|
|
485
|
+
case "FILL":
|
|
486
|
+
sizeStyles += "height: 100%;";
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
} else {
|
|
490
|
+
sizeStyles += `height: ${height}px;`;
|
|
491
|
+
}
|
|
492
|
+
if (node.layoutWrap === "WRAP") {
|
|
493
|
+
sizeStyles += `max-width: ${width}px;`;
|
|
494
|
+
}
|
|
495
|
+
return sizeStyles;
|
|
496
|
+
}
|
|
497
|
+
function escapeHtmlContent(text) {
|
|
498
|
+
const map = {
|
|
499
|
+
"&": "&",
|
|
500
|
+
"<": "<",
|
|
501
|
+
">": ">",
|
|
502
|
+
'"': """,
|
|
503
|
+
"'": "'"
|
|
504
|
+
};
|
|
505
|
+
return text.replace(/[&<>"']/g, (char) => map[char]);
|
|
506
|
+
}
|
|
507
|
+
function convertTextNodeToHtml(node, indent, dynamicContent, refAttr, attributesHtml) {
|
|
508
|
+
const {
|
|
509
|
+
name,
|
|
510
|
+
id,
|
|
511
|
+
characters,
|
|
512
|
+
fontName,
|
|
513
|
+
fontSize,
|
|
514
|
+
fontWeight,
|
|
515
|
+
fills,
|
|
516
|
+
textAlignHorizontal,
|
|
517
|
+
textAlignVertical,
|
|
518
|
+
letterSpacing,
|
|
519
|
+
lineHeight,
|
|
520
|
+
textDecoration,
|
|
521
|
+
textCase,
|
|
522
|
+
textTruncation,
|
|
523
|
+
maxLines,
|
|
524
|
+
maxWidth,
|
|
525
|
+
textAutoResize,
|
|
526
|
+
hasMissingFont,
|
|
527
|
+
hyperlinks
|
|
528
|
+
} = node;
|
|
529
|
+
if (hasMissingFont || !characters) {
|
|
530
|
+
if (hasMissingFont) {
|
|
531
|
+
return `${indent}<!-- Text node "${name}" has missing fonts -->
|
|
532
|
+
`;
|
|
533
|
+
}
|
|
534
|
+
return "";
|
|
535
|
+
}
|
|
536
|
+
let fontFamilyStyle = "font-family: sans-serif;";
|
|
537
|
+
if (fontName && typeof fontName === "object" && fontName.family) {
|
|
538
|
+
fontFamilyStyle = `font-family: '${fontName.family}', sans-serif;`;
|
|
539
|
+
}
|
|
540
|
+
const fontSizeValue = typeof fontSize === "number" ? fontSize : 16;
|
|
541
|
+
const fontSizeStyle = `font-size: ${fontSizeValue}px;`;
|
|
542
|
+
const fontWeightValue = typeof fontWeight === "number" ? fontWeight : 400;
|
|
543
|
+
const fontWeightStyle = `font-weight: ${fontWeightValue};`;
|
|
544
|
+
let textColor = "#000000";
|
|
545
|
+
if (fills && Array.isArray(fills) && fills.length > 0 && fills[0].type === "SOLID" && fills[0].color) {
|
|
546
|
+
textColor = rgbToHex(fills[0].color);
|
|
547
|
+
}
|
|
548
|
+
const colorStyle = `color: ${textColor};`;
|
|
549
|
+
const textAlign = textAlignHorizontal ? textAlignHorizontal.toLowerCase() : "left";
|
|
550
|
+
const textAlignStyle = `text-align: ${textAlign};`;
|
|
551
|
+
let verticalAlignWrapperStyle = "";
|
|
552
|
+
if (textAlignVertical) {
|
|
553
|
+
verticalAlignWrapperStyle = "display: flex; flex-direction: column;";
|
|
554
|
+
switch (textAlignVertical) {
|
|
555
|
+
case "TOP":
|
|
556
|
+
verticalAlignWrapperStyle += "justify-content: flex-start;";
|
|
557
|
+
break;
|
|
558
|
+
case "CENTER":
|
|
559
|
+
verticalAlignWrapperStyle += "justify-content: center;";
|
|
560
|
+
break;
|
|
561
|
+
case "BOTTOM":
|
|
562
|
+
verticalAlignWrapperStyle += "justify-content: flex-end;";
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
let letterSpacingStyle = "";
|
|
567
|
+
if (letterSpacing && letterSpacing.value !== 0) {
|
|
568
|
+
const unit = letterSpacing.unit === "PIXELS" ? "px" : "%";
|
|
569
|
+
letterSpacingStyle = `letter-spacing: ${letterSpacing.value}${unit};`;
|
|
570
|
+
}
|
|
571
|
+
let lineHeightStyle = "";
|
|
572
|
+
if (lineHeight) {
|
|
573
|
+
if (lineHeight.unit === "AUTO") {
|
|
574
|
+
lineHeightStyle = "line-height: normal;";
|
|
575
|
+
} else {
|
|
576
|
+
const unit = lineHeight.unit === "PIXELS" ? "px" : "%";
|
|
577
|
+
lineHeightStyle = `line-height: ${lineHeight.value}${unit};`;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
let textDecorationStyle = "";
|
|
581
|
+
if (textDecoration === "UNDERLINE") {
|
|
582
|
+
textDecorationStyle = "text-decoration: underline;";
|
|
583
|
+
} else if (textDecoration === "STRIKETHROUGH") {
|
|
584
|
+
textDecorationStyle = "text-decoration: line-through;";
|
|
585
|
+
}
|
|
586
|
+
let textTransformStyle = "";
|
|
587
|
+
if (textCase && textCase !== "ORIGINAL") {
|
|
588
|
+
switch (textCase) {
|
|
589
|
+
case "UPPER":
|
|
590
|
+
textTransformStyle = "text-transform: uppercase;";
|
|
591
|
+
break;
|
|
592
|
+
case "LOWER":
|
|
593
|
+
textTransformStyle = "text-transform: lowercase;";
|
|
594
|
+
break;
|
|
595
|
+
case "TITLE":
|
|
596
|
+
textTransformStyle = "text-transform: capitalize;";
|
|
597
|
+
break;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
let truncationStyle = "";
|
|
601
|
+
if (textTruncation === "ENDING") {
|
|
602
|
+
if (maxLines && maxLines > 1) {
|
|
603
|
+
truncationStyle = `display: -webkit-box; -webkit-line-clamp: ${maxLines}; -webkit-box-orient: vertical; overflow: hidden;`;
|
|
604
|
+
} else {
|
|
605
|
+
truncationStyle = "white-space: nowrap; overflow: hidden; text-overflow: ellipsis;";
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
if (maxWidth && maxWidth > 0) {
|
|
609
|
+
truncationStyle += `max-width: ${maxWidth}px;`;
|
|
610
|
+
}
|
|
611
|
+
const positionStyle = getPositionStyle(node);
|
|
612
|
+
let sizeStyles = getNodeSizeStyles(node);
|
|
613
|
+
const commonStyles = getCommonStyles(node);
|
|
614
|
+
if (textAutoResize === "HEIGHT") {
|
|
615
|
+
sizeStyles = sizeStyles.replace(/height: [^;]+;/, "height: auto;");
|
|
616
|
+
}
|
|
617
|
+
const textStyles = `${fontFamilyStyle}${fontSizeStyle}${fontWeightStyle}${colorStyle}${textAlignStyle}${letterSpacingStyle}${lineHeightStyle}${textDecorationStyle}${textTransformStyle}${truncationStyle}`;
|
|
618
|
+
let htmlContent = "";
|
|
619
|
+
if (dynamicContent) {
|
|
620
|
+
htmlContent = dynamicContent;
|
|
621
|
+
} else if (hyperlinks && hyperlinks.length > 0) {
|
|
622
|
+
let lastEnd = 0;
|
|
623
|
+
for (const link of hyperlinks) {
|
|
624
|
+
if (link.start > lastEnd) {
|
|
625
|
+
const beforeText = characters.substring(lastEnd, link.start);
|
|
626
|
+
htmlContent += escapeHtmlContent(beforeText).replace(/\n/g, "<br>");
|
|
627
|
+
}
|
|
628
|
+
const linkText = characters.substring(link.start, link.end + 1);
|
|
629
|
+
htmlContent += `<a href="${escapeHtmlContent(link.url)}" style="color: inherit;">${escapeHtmlContent(linkText).replace(/\n/g, "<br>")}</a>`;
|
|
630
|
+
lastEnd = link.end + 1;
|
|
631
|
+
}
|
|
632
|
+
if (lastEnd < characters.length) {
|
|
633
|
+
const afterText = characters.substring(lastEnd);
|
|
634
|
+
htmlContent += escapeHtmlContent(afterText).replace(/\n/g, "<br>");
|
|
635
|
+
}
|
|
636
|
+
} else {
|
|
637
|
+
htmlContent = escapeHtmlContent(characters).replace(/\n/g, "<br>");
|
|
638
|
+
}
|
|
639
|
+
const childIndent = indent + " ";
|
|
640
|
+
const innerIndent = indent + " ";
|
|
641
|
+
const styleAttr = `${positionStyle}${sizeStyles}${commonStyles}${textStyles}`;
|
|
642
|
+
const refString = refAttr || "";
|
|
643
|
+
const attrsString = attributesHtml || "";
|
|
644
|
+
if (verticalAlignWrapperStyle) {
|
|
645
|
+
return `${indent}<div data-figma-id="${id}"${refString}${attrsString} style="${styleAttr}${verticalAlignWrapperStyle}">
|
|
646
|
+
${childIndent}<div style="${textStyles}">
|
|
647
|
+
${innerIndent}${htmlContent}
|
|
648
|
+
${childIndent}</div>
|
|
649
|
+
${indent}</div>
|
|
650
|
+
`;
|
|
651
|
+
} else {
|
|
652
|
+
return `${indent}<div data-figma-id="${id}"${refString}${attrsString} style="${styleAttr}">${htmlContent}</div>
|
|
653
|
+
`;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
function convertImageNodeToHtml(node, indent, srcBinding, altBinding, refAttr, staticImageUrl) {
|
|
657
|
+
const { name, id } = node;
|
|
658
|
+
const positionStyle = getPositionStyle(node);
|
|
659
|
+
const commonStyles = getCommonStyles(node);
|
|
660
|
+
const borderRadius = getBorderRadius(node);
|
|
661
|
+
const sizeStyles = getNodeSizeStyles(node);
|
|
662
|
+
const styles = `${positionStyle}${sizeStyles}${borderRadius}${commonStyles}`.trim();
|
|
663
|
+
let src = "";
|
|
664
|
+
if (srcBinding) {
|
|
665
|
+
src = srcBinding;
|
|
666
|
+
} else if (staticImageUrl) {
|
|
667
|
+
src = staticImageUrl;
|
|
668
|
+
} else {
|
|
669
|
+
src = "/placeholder-image.png";
|
|
670
|
+
console.warn(`Image node "${name}" (${id}) has no src binding or static image`);
|
|
671
|
+
}
|
|
672
|
+
const alt = altBinding || name;
|
|
673
|
+
const refAttribute = refAttr || "";
|
|
674
|
+
const styleAttribute = styles ? ` style="${styles}"` : "";
|
|
675
|
+
const dataAttribute = ` data-figma-id="${id}"`;
|
|
676
|
+
return `${indent}<img${dataAttribute}${refAttribute} src="${src}" alt="${alt}"${styleAttribute} />
|
|
677
|
+
`;
|
|
678
|
+
}
|
|
679
|
+
function extractStaticImageUrl(node) {
|
|
680
|
+
if (!node.fills || !Array.isArray(node.fills)) {
|
|
681
|
+
return void 0;
|
|
682
|
+
}
|
|
683
|
+
for (const fill of node.fills) {
|
|
684
|
+
if (fill.visible !== false && fill.type === "IMAGE") {
|
|
685
|
+
if (fill.imageUrl) {
|
|
686
|
+
return fill.imageUrl;
|
|
687
|
+
}
|
|
688
|
+
if (fill.imageHash) {
|
|
689
|
+
console.warn(
|
|
690
|
+
`Image fill with hash "${fill.imageHash}" found on node "${node.name}" (${node.id}) but no imageUrl in serialized data. Update plugin serialization to export and save images.`
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
return void 0;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return void 0;
|
|
697
|
+
}
|
|
698
|
+
function convertRectangleToHtml(node, indent) {
|
|
699
|
+
const { id } = node;
|
|
700
|
+
const positionStyle = getPositionStyle(node);
|
|
701
|
+
const sizeStyles = getNodeSizeStyles(node);
|
|
702
|
+
const commonStyles = getCommonStyles(node);
|
|
703
|
+
const backgroundStyle = getBackgroundFillsStyle(node);
|
|
704
|
+
const borderRadius = getBorderRadius(node);
|
|
705
|
+
const strokeStyles = getStrokeStyles(node);
|
|
706
|
+
const allStyles = `${positionStyle}${sizeStyles}${backgroundStyle}${strokeStyles}${borderRadius}${commonStyles}box-sizing: border-box;`;
|
|
707
|
+
return `${indent}<div data-figma-id="${id}" style="${allStyles}"></div>
|
|
708
|
+
`;
|
|
709
|
+
}
|
|
710
|
+
function convertEllipseToHtml(node, indent) {
|
|
711
|
+
const { id } = node;
|
|
712
|
+
const positionStyle = getPositionStyle(node);
|
|
713
|
+
const sizeStyles = getNodeSizeStyles(node);
|
|
714
|
+
const commonStyles = getCommonStyles(node);
|
|
715
|
+
const backgroundStyle = getBackgroundFillsStyle(node);
|
|
716
|
+
const strokeStyles = getStrokeStyles(node);
|
|
717
|
+
const borderRadius = "border-radius: 50%;";
|
|
718
|
+
const allStyles = `${positionStyle}${sizeStyles}${backgroundStyle}${strokeStyles}${borderRadius}${commonStyles}box-sizing: border-box;`;
|
|
719
|
+
return `${indent}<div data-figma-id="${id}" style="${allStyles}"></div>
|
|
720
|
+
`;
|
|
721
|
+
}
|
|
722
|
+
function convertVectorToHtml(node, indent) {
|
|
723
|
+
const { id, name, svgContent, svgExportFailed, width, height } = node;
|
|
724
|
+
const positionStyle = getPositionStyle(node);
|
|
725
|
+
const sizeStyles = getNodeSizeStyles(node);
|
|
726
|
+
const commonStyles = getCommonStyles(node);
|
|
727
|
+
let finalSvgContent;
|
|
728
|
+
if (svgExportFailed || !svgContent) {
|
|
729
|
+
finalSvgContent = `<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg"><rect width="${width}" height="${height}" fill="none" stroke="#ccc" stroke-width="1" stroke-dasharray="5,5"/><text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-size="10" fill="#999">Vector: ${name}</text></svg>`;
|
|
730
|
+
} else {
|
|
731
|
+
finalSvgContent = svgContent;
|
|
732
|
+
}
|
|
733
|
+
const allStyles = `${positionStyle}${sizeStyles}${commonStyles}box-sizing: border-box;`;
|
|
734
|
+
const childIndent = indent + " ";
|
|
735
|
+
return `${indent}<div data-figma-id="${id}" data-figma-type="vector" style="${allStyles}">
|
|
736
|
+
${childIndent}${finalSvgContent}
|
|
737
|
+
${indent}</div>
|
|
738
|
+
`;
|
|
739
|
+
}
|
|
740
|
+
function getComponentVariantValues(node, propertyBindings) {
|
|
741
|
+
const values = /* @__PURE__ */ new Map();
|
|
742
|
+
const filterPseudoVariants = (variantValues) => {
|
|
743
|
+
return variantValues.filter((value) => !value.includes(":"));
|
|
744
|
+
};
|
|
745
|
+
if (node.componentPropertyDefinitions) {
|
|
746
|
+
for (const binding of propertyBindings) {
|
|
747
|
+
const propDef = node.componentPropertyDefinitions[binding.property];
|
|
748
|
+
if (propDef && propDef.type === "VARIANT" && propDef.variantOptions) {
|
|
749
|
+
const filtered = filterPseudoVariants(propDef.variantOptions);
|
|
750
|
+
if (filtered.length > 0) {
|
|
751
|
+
values.set(binding.property, filtered);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
if (values.size === 0 && node.variants && node.variants.length > 0) {
|
|
757
|
+
const propertyValuesMap = /* @__PURE__ */ new Map();
|
|
758
|
+
for (const variant of node.variants) {
|
|
759
|
+
if (variant.variantProperties) {
|
|
760
|
+
for (const binding of propertyBindings) {
|
|
761
|
+
const propValue = variant.variantProperties[binding.property];
|
|
762
|
+
if (propValue && !propValue.includes(":")) {
|
|
763
|
+
if (!propertyValuesMap.has(binding.property)) {
|
|
764
|
+
propertyValuesMap.set(binding.property, /* @__PURE__ */ new Set());
|
|
765
|
+
}
|
|
766
|
+
propertyValuesMap.get(binding.property).add(propValue);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
for (const [prop, valueSet] of propertyValuesMap) {
|
|
772
|
+
if (valueSet.size > 0) {
|
|
773
|
+
values.set(prop, Array.from(valueSet));
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return values;
|
|
778
|
+
}
|
|
779
|
+
function isBooleanVariant(values, contractTag) {
|
|
780
|
+
if (contractTag.dataType !== "boolean") {
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
783
|
+
if (values.length !== 2) {
|
|
784
|
+
return false;
|
|
785
|
+
}
|
|
786
|
+
const sortedValues = [...values].sort();
|
|
787
|
+
return sortedValues[0] === "false" && sortedValues[1] === "true";
|
|
788
|
+
}
|
|
789
|
+
function generatePermutations(propertyValues, bindings) {
|
|
790
|
+
const properties = Array.from(propertyValues.entries());
|
|
791
|
+
if (properties.length === 0) {
|
|
792
|
+
return [];
|
|
793
|
+
}
|
|
794
|
+
const permutations = [];
|
|
795
|
+
function generate(index, current) {
|
|
796
|
+
if (index === properties.length) {
|
|
797
|
+
permutations.push([...current]);
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
const [propName, propValues] = properties[index];
|
|
801
|
+
const binding = bindings.find((b) => b.property === propName);
|
|
802
|
+
if (!binding)
|
|
803
|
+
return;
|
|
804
|
+
const isBoolean = isBooleanVariant(propValues, binding.contractTag);
|
|
805
|
+
for (const value of propValues) {
|
|
806
|
+
current.push({ property: propName, tagPath: binding.tagPath, value, isBoolean });
|
|
807
|
+
generate(index + 1, current);
|
|
808
|
+
current.pop();
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
generate(0, []);
|
|
812
|
+
return permutations;
|
|
813
|
+
}
|
|
814
|
+
function findComponentVariant(node, permutation) {
|
|
815
|
+
if (!node.variants || node.variants.length === 0) {
|
|
816
|
+
throw new Error(
|
|
817
|
+
`Node "${node.name}" has no variants array - cannot find variant component`
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
const targetProps = /* @__PURE__ */ new Map();
|
|
821
|
+
for (const { property, value } of permutation) {
|
|
822
|
+
targetProps.set(property, value);
|
|
823
|
+
}
|
|
824
|
+
const matchingVariant = node.variants.find((variant) => {
|
|
825
|
+
if (!variant.variantProperties) {
|
|
826
|
+
return false;
|
|
827
|
+
}
|
|
828
|
+
for (const [prop, value] of targetProps) {
|
|
829
|
+
if (variant.variantProperties[prop] !== value) {
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
for (const [prop, value] of Object.entries(variant.variantProperties)) {
|
|
834
|
+
if (targetProps.has(prop) && targetProps.get(prop) !== value) {
|
|
835
|
+
return false;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return true;
|
|
839
|
+
});
|
|
840
|
+
if (!matchingVariant) {
|
|
841
|
+
console.log(
|
|
842
|
+
`No matching variant found for "${node.name}" with properties:`,
|
|
843
|
+
Object.fromEntries(targetProps),
|
|
844
|
+
"\nAvailable variants:",
|
|
845
|
+
node.variants.map((v) => v.variantProperties),
|
|
846
|
+
"\nUsing first variant as fallback"
|
|
847
|
+
);
|
|
848
|
+
return node.variants[0] || node;
|
|
849
|
+
}
|
|
850
|
+
return matchingVariant;
|
|
851
|
+
}
|
|
852
|
+
function buildVariantCondition(permutation) {
|
|
853
|
+
const conditions = permutation.map(({ tagPath, value, isBoolean }) => {
|
|
854
|
+
if (isBoolean) {
|
|
855
|
+
if (value === "true") {
|
|
856
|
+
return tagPath;
|
|
857
|
+
} else {
|
|
858
|
+
return `!${tagPath}`;
|
|
859
|
+
}
|
|
860
|
+
} else {
|
|
861
|
+
return `${tagPath} == ${value}`;
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
return conditions.join(" && ");
|
|
865
|
+
}
|
|
866
|
+
function convertVariantNode(node, analysis, context, convertNodeToJayHtml2) {
|
|
867
|
+
const indent = " ".repeat(context.indentLevel);
|
|
868
|
+
const innerIndent = " ".repeat(context.indentLevel + 1);
|
|
869
|
+
const propertyValues = getComponentVariantValues(node, analysis.propertyBindings);
|
|
870
|
+
const permutations = generatePermutations(propertyValues, analysis.propertyBindings);
|
|
871
|
+
if (permutations.length === 0) {
|
|
872
|
+
throw new Error(
|
|
873
|
+
`No permutations generated for variant node "${node.name}" - check property definitions`
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
let variantHtml = "";
|
|
877
|
+
for (const permutation of permutations) {
|
|
878
|
+
const conditions = buildVariantCondition(permutation);
|
|
879
|
+
const variantNode = findComponentVariant(node, permutation);
|
|
880
|
+
variantHtml += `${innerIndent}<div if="${conditions}">
|
|
881
|
+
`;
|
|
882
|
+
const variantContext = {
|
|
883
|
+
...context,
|
|
884
|
+
indentLevel: context.indentLevel + 2
|
|
885
|
+
// +2 because we're inside wrapper and if div
|
|
886
|
+
};
|
|
887
|
+
if (variantNode.children && variantNode.children.length > 0) {
|
|
888
|
+
for (const child of variantNode.children) {
|
|
889
|
+
variantHtml += convertNodeToJayHtml2(child, variantContext);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
variantHtml += `${innerIndent}</div>
|
|
893
|
+
`;
|
|
894
|
+
}
|
|
895
|
+
const positionStyle = getPositionStyle(node);
|
|
896
|
+
const frameSizeStyles = getFrameSizeStyles(node);
|
|
897
|
+
const backgroundStyle = getBackgroundFillsStyle(node);
|
|
898
|
+
const borderRadius = getBorderRadius(node);
|
|
899
|
+
const strokeStyles = getStrokeStyles(node);
|
|
900
|
+
const flexStyles = getAutoLayoutStyles(node);
|
|
901
|
+
const overflowStyles = getOverflowStyles(node);
|
|
902
|
+
const commonStyles = getCommonStyles(node);
|
|
903
|
+
const wrapperStyleAttr = `${positionStyle}${frameSizeStyles}${backgroundStyle}${strokeStyles}${borderRadius}${overflowStyles}${commonStyles}${flexStyles}box-sizing: border-box;`;
|
|
904
|
+
let refAttr = "";
|
|
905
|
+
if (analysis.refPath) {
|
|
906
|
+
refAttr = ` ref="${analysis.refPath}"`;
|
|
907
|
+
} else if (analysis.dualPath) {
|
|
908
|
+
refAttr = ` ref="${analysis.dualPath}"`;
|
|
909
|
+
} else if (analysis.interactiveVariantPath) {
|
|
910
|
+
refAttr = ` ref="${analysis.interactiveVariantPath}"`;
|
|
911
|
+
}
|
|
912
|
+
return `${indent}<div id="${node.id}" data-figma-id="${node.id}" data-figma-type="variant-container"${refAttr} style="${wrapperStyleAttr}">
|
|
913
|
+
` + variantHtml + `${indent}</div>
|
|
914
|
+
`;
|
|
915
|
+
}
|
|
916
|
+
function convertRepeaterNode(node, analysis, context, convertNodeToJayHtml2) {
|
|
917
|
+
const { repeaterPath, trackByKey } = analysis;
|
|
918
|
+
const indent = " ".repeat(context.indentLevel);
|
|
919
|
+
const innerIndent = " ".repeat(context.indentLevel + 1);
|
|
920
|
+
if (node.type !== "FRAME") {
|
|
921
|
+
throw new Error(`Repeater node "${node.name}" must be a FRAME (got: ${node.type})`);
|
|
922
|
+
}
|
|
923
|
+
if (!node.layoutMode || node.layoutMode === "NONE") {
|
|
924
|
+
throw new Error(
|
|
925
|
+
`Repeater node "${node.name}" must have auto-layout (HORIZONTAL or VERTICAL)`
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
const positionStyle = getPositionStyle(node);
|
|
929
|
+
const frameSizeStyles = getFrameSizeStyles(node);
|
|
930
|
+
const backgroundStyle = getBackgroundFillsStyle(node);
|
|
931
|
+
const borderRadius = getBorderRadius(node);
|
|
932
|
+
const strokeStyles = getStrokeStyles(node);
|
|
933
|
+
const flexStyles = getAutoLayoutStyles(node);
|
|
934
|
+
const overflowStyles = getOverflowStyles(node);
|
|
935
|
+
const commonStyles = getCommonStyles(node);
|
|
936
|
+
const outerStyleAttr = `${positionStyle}${frameSizeStyles}${backgroundStyle}${strokeStyles}${borderRadius}${overflowStyles}${commonStyles}${flexStyles}box-sizing: border-box;`;
|
|
937
|
+
let innerDivSizeStyles = "";
|
|
938
|
+
if (node.layoutWrap === "WRAP") {
|
|
939
|
+
innerDivSizeStyles = "width: fit-content; height: fit-content;";
|
|
940
|
+
} else if (node.layoutMode === "HORIZONTAL") {
|
|
941
|
+
innerDivSizeStyles = "height: 100%;";
|
|
942
|
+
} else if (node.layoutMode === "VERTICAL") {
|
|
943
|
+
innerDivSizeStyles = "width: 100%;";
|
|
944
|
+
}
|
|
945
|
+
let html = `${indent}<div id="${node.id}" data-figma-id="${node.id}" data-figma-type="frame-repeater" style="${outerStyleAttr}">
|
|
946
|
+
`;
|
|
947
|
+
html += `${innerIndent}<div style="position: relative; ${innerDivSizeStyles}" forEach="${repeaterPath}" trackBy="${trackByKey}">
|
|
948
|
+
`;
|
|
949
|
+
const newContext = {
|
|
950
|
+
...context,
|
|
951
|
+
repeaterPathStack: [...context.repeaterPathStack, repeaterPath.split(".")],
|
|
952
|
+
indentLevel: context.indentLevel + 2
|
|
953
|
+
// +2 because we're inside both divs
|
|
954
|
+
};
|
|
955
|
+
if (node.children && node.children.length > 0) {
|
|
956
|
+
html += convertNodeToJayHtml2(node.children[0], newContext);
|
|
957
|
+
} else {
|
|
958
|
+
throw new Error(
|
|
959
|
+
`Repeater node "${node.name}" has no children - repeater template is required`
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
html += `${innerIndent}</div>
|
|
963
|
+
`;
|
|
964
|
+
html += `${indent}</div>
|
|
965
|
+
`;
|
|
966
|
+
return html;
|
|
967
|
+
}
|
|
968
|
+
function convertGroupNode(node, analysis, context, convertNodeToJayHtml2) {
|
|
969
|
+
const indent = " ".repeat(context.indentLevel);
|
|
970
|
+
const positionStyle = getPositionStyle(node);
|
|
971
|
+
const sizeStyles = getNodeSizeStyles(node);
|
|
972
|
+
const commonStyles = getCommonStyles(node);
|
|
973
|
+
const styleAttr = `${positionStyle}${sizeStyles}${commonStyles}box-sizing: border-box;`;
|
|
974
|
+
let refAttr = "";
|
|
975
|
+
if (analysis.refPath) {
|
|
976
|
+
refAttr = ` ref="${analysis.refPath}"`;
|
|
977
|
+
} else if (analysis.dualPath) {
|
|
978
|
+
refAttr = ` ref="${analysis.dualPath}"`;
|
|
979
|
+
}
|
|
980
|
+
let htmlAttrs = `id="${node.id}" data-figma-id="${node.id}" data-figma-type="group"${refAttr} style="${styleAttr}"`;
|
|
981
|
+
for (const [attr, tagPath] of analysis.attributes) {
|
|
982
|
+
htmlAttrs += ` ${attr}="{${tagPath}}"`;
|
|
983
|
+
}
|
|
984
|
+
let html = `${indent}<div ${htmlAttrs}>
|
|
985
|
+
`;
|
|
986
|
+
const childContext = {
|
|
987
|
+
...context,
|
|
988
|
+
indentLevel: context.indentLevel + 1
|
|
989
|
+
};
|
|
990
|
+
if (node.children && node.children.length > 0) {
|
|
991
|
+
for (const child of node.children) {
|
|
992
|
+
html += convertNodeToJayHtml2(child, childContext);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
html += `${indent}</div>
|
|
996
|
+
`;
|
|
997
|
+
return html;
|
|
998
|
+
}
|
|
999
|
+
function findContractTag(tags, tagPath) {
|
|
1000
|
+
if (tagPath.length === 0) {
|
|
1001
|
+
return void 0;
|
|
1002
|
+
}
|
|
1003
|
+
const tag = tags.find((t) => t.tag === tagPath[0]);
|
|
1004
|
+
if (!tag) {
|
|
1005
|
+
return void 0;
|
|
1006
|
+
}
|
|
1007
|
+
if (tagPath.length === 1) {
|
|
1008
|
+
return tag;
|
|
1009
|
+
}
|
|
1010
|
+
if (!tag.tags || tag.tags.length === 0) {
|
|
1011
|
+
return void 0;
|
|
1012
|
+
}
|
|
1013
|
+
return findContractTag(tag.tags, tagPath.slice(1));
|
|
1014
|
+
}
|
|
1015
|
+
function findPlugin(plugins, pluginName) {
|
|
1016
|
+
return plugins.find((p) => p.name === pluginName);
|
|
1017
|
+
}
|
|
1018
|
+
function findPluginContract(plugin, componentName) {
|
|
1019
|
+
const contract = plugin.contracts.find((c) => c.name === componentName);
|
|
1020
|
+
return contract ? { tags: contract.tags } : void 0;
|
|
1021
|
+
}
|
|
1022
|
+
function findPageContract(projectPage) {
|
|
1023
|
+
return projectPage.contract ? { tags: projectPage.contract.tags } : void 0;
|
|
1024
|
+
}
|
|
1025
|
+
function isDataTag(contractTag) {
|
|
1026
|
+
if (Array.isArray(contractTag.type)) {
|
|
1027
|
+
return contractTag.type.includes("data");
|
|
1028
|
+
}
|
|
1029
|
+
return contractTag.type === "data";
|
|
1030
|
+
}
|
|
1031
|
+
function isInteractiveTag(contractTag) {
|
|
1032
|
+
if (Array.isArray(contractTag.type)) {
|
|
1033
|
+
return contractTag.type.includes("interactive");
|
|
1034
|
+
}
|
|
1035
|
+
return contractTag.type === "interactive";
|
|
1036
|
+
}
|
|
1037
|
+
function isDualTag(contractTag) {
|
|
1038
|
+
if (Array.isArray(contractTag.type)) {
|
|
1039
|
+
return contractTag.type.includes("data") && contractTag.type.includes("interactive");
|
|
1040
|
+
}
|
|
1041
|
+
return false;
|
|
1042
|
+
}
|
|
1043
|
+
function isRepeaterTag(contractTag) {
|
|
1044
|
+
return contractTag.type === "subContract" && contractTag.repeated === true;
|
|
1045
|
+
}
|
|
1046
|
+
function applyRepeaterContext(path2, repeaterStack) {
|
|
1047
|
+
for (const repeaterPath of repeaterStack) {
|
|
1048
|
+
const prefix = repeaterPath.join(".") + ".";
|
|
1049
|
+
if (path2.startsWith(prefix)) {
|
|
1050
|
+
path2 = path2.substring(prefix.length);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
return path2;
|
|
1054
|
+
}
|
|
1055
|
+
function resolveBinding(binding, context) {
|
|
1056
|
+
let contract;
|
|
1057
|
+
let key;
|
|
1058
|
+
let tagPathWithoutKey;
|
|
1059
|
+
if (binding.pageContractPath.pluginName && binding.pageContractPath.componentName) {
|
|
1060
|
+
const plugin = findPlugin(context.plugins, binding.pageContractPath.pluginName);
|
|
1061
|
+
if (!plugin) {
|
|
1062
|
+
throw new Error(`Plugin not found: ${binding.pageContractPath.pluginName}`);
|
|
1063
|
+
}
|
|
1064
|
+
contract = findPluginContract(plugin, binding.pageContractPath.componentName);
|
|
1065
|
+
if (!contract) {
|
|
1066
|
+
throw new Error(
|
|
1067
|
+
`Contract not found in plugin ${binding.pageContractPath.pluginName}: ${binding.pageContractPath.componentName}`
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
const usedComponent = context.projectPage.usedComponents?.find(
|
|
1071
|
+
(c) => c.componentName === binding.pageContractPath.componentName
|
|
1072
|
+
);
|
|
1073
|
+
if (!usedComponent) {
|
|
1074
|
+
throw new Error(
|
|
1075
|
+
`Used component not found in page: ${binding.pageContractPath.componentName}`
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
key = usedComponent.key;
|
|
1079
|
+
tagPathWithoutKey = binding.tagPath.slice(1);
|
|
1080
|
+
} else {
|
|
1081
|
+
contract = findPageContract(context.projectPage);
|
|
1082
|
+
if (!contract) {
|
|
1083
|
+
throw new Error(`Page contract not found for page ${context.projectPage.url}`);
|
|
1084
|
+
}
|
|
1085
|
+
tagPathWithoutKey = binding.tagPath;
|
|
1086
|
+
}
|
|
1087
|
+
const contractTag = findContractTag(contract.tags, tagPathWithoutKey);
|
|
1088
|
+
if (!contractTag) {
|
|
1089
|
+
throw new Error(`Contract tag not found: ${tagPathWithoutKey.join(".")} in contract`);
|
|
1090
|
+
}
|
|
1091
|
+
let fullPath;
|
|
1092
|
+
if (key) {
|
|
1093
|
+
fullPath = [key, ...tagPathWithoutKey].join(".");
|
|
1094
|
+
} else {
|
|
1095
|
+
fullPath = binding.tagPath.join(".");
|
|
1096
|
+
}
|
|
1097
|
+
fullPath = applyRepeaterContext(fullPath, context.repeaterPathStack);
|
|
1098
|
+
return { fullPath, contractTag };
|
|
1099
|
+
}
|
|
1100
|
+
function getBindingsData(node) {
|
|
1101
|
+
const bindingsDataRaw = node.pluginData?.["jay-layer-bindings"];
|
|
1102
|
+
if (bindingsDataRaw) {
|
|
1103
|
+
try {
|
|
1104
|
+
return JSON.parse(bindingsDataRaw);
|
|
1105
|
+
} catch (error) {
|
|
1106
|
+
console.warn(`Failed to parse bindings data for node ${node.name}:`, error);
|
|
1107
|
+
return [];
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
return [];
|
|
1111
|
+
}
|
|
1112
|
+
function analyzeBindings(bindings, context) {
|
|
1113
|
+
const analysis = {
|
|
1114
|
+
type: "none",
|
|
1115
|
+
attributes: /* @__PURE__ */ new Map(),
|
|
1116
|
+
propertyBindings: [],
|
|
1117
|
+
isRepeater: false
|
|
1118
|
+
};
|
|
1119
|
+
if (bindings.length === 0) {
|
|
1120
|
+
return analysis;
|
|
1121
|
+
}
|
|
1122
|
+
const resolved = bindings.map((b) => ({
|
|
1123
|
+
binding: b,
|
|
1124
|
+
...resolveBinding(b, context)
|
|
1125
|
+
})).filter((r) => r.fullPath && r.contractTag);
|
|
1126
|
+
if (resolved.length === 0) {
|
|
1127
|
+
return analysis;
|
|
1128
|
+
}
|
|
1129
|
+
const repeaterBinding = resolved.find((r) => isRepeaterTag(r.contractTag));
|
|
1130
|
+
if (repeaterBinding) {
|
|
1131
|
+
analysis.type = "repeater";
|
|
1132
|
+
analysis.isRepeater = true;
|
|
1133
|
+
analysis.repeaterPath = repeaterBinding.fullPath;
|
|
1134
|
+
analysis.repeaterTag = repeaterBinding.contractTag;
|
|
1135
|
+
analysis.trackByKey = repeaterBinding.contractTag.trackBy || "id";
|
|
1136
|
+
return analysis;
|
|
1137
|
+
}
|
|
1138
|
+
const propertyBindings = resolved.filter((r) => r.binding.property);
|
|
1139
|
+
if (propertyBindings.length > 0) {
|
|
1140
|
+
if (propertyBindings.length !== resolved.length) {
|
|
1141
|
+
throw new Error(`Node has mixed property and non-property bindings - this is invalid`);
|
|
1142
|
+
}
|
|
1143
|
+
analysis.type = "property-variant";
|
|
1144
|
+
analysis.propertyBindings = propertyBindings.map((r) => ({
|
|
1145
|
+
property: r.binding.property,
|
|
1146
|
+
tagPath: r.fullPath,
|
|
1147
|
+
contractTag: r.contractTag
|
|
1148
|
+
}));
|
|
1149
|
+
for (const r of propertyBindings) {
|
|
1150
|
+
if (isInteractiveTag(r.contractTag)) {
|
|
1151
|
+
analysis.interactiveVariantPath = r.fullPath;
|
|
1152
|
+
break;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
return analysis;
|
|
1156
|
+
}
|
|
1157
|
+
const attributeBindings = resolved.filter((r) => r.binding.attribute);
|
|
1158
|
+
if (attributeBindings.length > 0) {
|
|
1159
|
+
analysis.type = "attribute";
|
|
1160
|
+
for (const r of attributeBindings) {
|
|
1161
|
+
analysis.attributes.set(r.binding.attribute, r.fullPath);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
const contentBindings = resolved.filter((r) => !r.binding.attribute && !r.binding.property);
|
|
1165
|
+
if (contentBindings.length > 0) {
|
|
1166
|
+
const binding = contentBindings[0];
|
|
1167
|
+
if (isDualTag(binding.contractTag)) {
|
|
1168
|
+
analysis.type = "dual";
|
|
1169
|
+
analysis.dualPath = binding.fullPath;
|
|
1170
|
+
} else if (isInteractiveTag(binding.contractTag)) {
|
|
1171
|
+
analysis.type = "interactive";
|
|
1172
|
+
analysis.refPath = binding.fullPath;
|
|
1173
|
+
} else if (isDataTag(binding.contractTag)) {
|
|
1174
|
+
if (analysis.type === "attribute") {
|
|
1175
|
+
analysis.dynamicContentPath = binding.fullPath;
|
|
1176
|
+
analysis.dynamicContentTag = binding.contractTag;
|
|
1177
|
+
} else {
|
|
1178
|
+
analysis.type = "dynamic-content";
|
|
1179
|
+
analysis.dynamicContentPath = binding.fullPath;
|
|
1180
|
+
analysis.dynamicContentTag = binding.contractTag;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
return analysis;
|
|
1185
|
+
}
|
|
1186
|
+
function validateBindings(analysis, node) {
|
|
1187
|
+
if (analysis.type === "property-variant" && analysis.attributes.size > 0) {
|
|
1188
|
+
throw new Error(
|
|
1189
|
+
`Node "${node.name}" has both property and attribute bindings - this is invalid`
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
if (analysis.type === "interactive" && analysis.attributes.size > 0) {
|
|
1193
|
+
throw new Error(
|
|
1194
|
+
`Node "${node.name}" has interactive binding with attributes - this is invalid`
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
function convertRegularNode(node, analysis, context) {
|
|
1199
|
+
const indent = " ".repeat(context.indentLevel);
|
|
1200
|
+
const { type, children, pluginData } = node;
|
|
1201
|
+
const semanticHtml = pluginData?.["semanticHtml"];
|
|
1202
|
+
if (type === "TEXT") {
|
|
1203
|
+
const dynamicContent = analysis.dynamicContentPath ? `{${analysis.dynamicContentPath}}` : "";
|
|
1204
|
+
const refAttr = analysis.refPath ? ` ref="${analysis.refPath}"` : "";
|
|
1205
|
+
const dualContent = analysis.dualPath ? `{${analysis.dualPath}}` : "";
|
|
1206
|
+
const dualRef = analysis.dualPath ? ` ref="${analysis.dualPath}"` : "";
|
|
1207
|
+
let attributesHtml = "";
|
|
1208
|
+
for (const [attr, tagPath] of analysis.attributes) {
|
|
1209
|
+
attributesHtml += ` ${attr}="{${tagPath}}"`;
|
|
1210
|
+
}
|
|
1211
|
+
return convertTextNodeToHtml(
|
|
1212
|
+
node,
|
|
1213
|
+
indent,
|
|
1214
|
+
dynamicContent || dualContent,
|
|
1215
|
+
refAttr || dualRef,
|
|
1216
|
+
attributesHtml
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
if (semanticHtml === "img") {
|
|
1220
|
+
let srcBinding;
|
|
1221
|
+
let altBinding;
|
|
1222
|
+
for (const [attr, tagPath] of analysis.attributes) {
|
|
1223
|
+
if (attr === "src") {
|
|
1224
|
+
srcBinding = `{${tagPath}}`;
|
|
1225
|
+
} else if (attr === "alt") {
|
|
1226
|
+
altBinding = `{${tagPath}}`;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
let staticImageUrl;
|
|
1230
|
+
if (!srcBinding) {
|
|
1231
|
+
staticImageUrl = extractStaticImageUrl(node);
|
|
1232
|
+
}
|
|
1233
|
+
const refAttr = analysis.refPath ? ` ref="${analysis.refPath}"` : analysis.dualPath ? ` ref="${analysis.dualPath}"` : "";
|
|
1234
|
+
return convertImageNodeToHtml(
|
|
1235
|
+
node,
|
|
1236
|
+
indent,
|
|
1237
|
+
srcBinding,
|
|
1238
|
+
altBinding,
|
|
1239
|
+
refAttr,
|
|
1240
|
+
staticImageUrl
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
const positionStyle = getPositionStyle(node);
|
|
1244
|
+
const sizeStyles = getNodeSizeStyles(node);
|
|
1245
|
+
const commonStyles = getCommonStyles(node);
|
|
1246
|
+
let styleAttr = "";
|
|
1247
|
+
if (type === "FRAME") {
|
|
1248
|
+
const backgroundStyle = getBackgroundFillsStyle(node);
|
|
1249
|
+
const borderRadius = getBorderRadius(node);
|
|
1250
|
+
const strokeStyles = getStrokeStyles(node);
|
|
1251
|
+
const flexStyles = getAutoLayoutStyles(node);
|
|
1252
|
+
const overflowStyles = getOverflowStyles(node);
|
|
1253
|
+
const frameSizeStyles = getFrameSizeStyles(node);
|
|
1254
|
+
styleAttr = `${positionStyle}${frameSizeStyles}${backgroundStyle}${strokeStyles}${borderRadius}${overflowStyles}${commonStyles}${flexStyles}box-sizing: border-box;`;
|
|
1255
|
+
} else {
|
|
1256
|
+
styleAttr = `${positionStyle}${sizeStyles}${commonStyles}`;
|
|
1257
|
+
}
|
|
1258
|
+
const tag = semanticHtml || "div";
|
|
1259
|
+
let htmlAttrs = `data-figma-id="${node.id}" data-figma-type="${type.toLowerCase()}" style="${styleAttr}"`;
|
|
1260
|
+
if (analysis.refPath) {
|
|
1261
|
+
htmlAttrs += ` ref="${analysis.refPath}"`;
|
|
1262
|
+
} else if (analysis.dualPath) {
|
|
1263
|
+
htmlAttrs += ` ref="${analysis.dualPath}"`;
|
|
1264
|
+
}
|
|
1265
|
+
for (const [attr, tagPath] of analysis.attributes) {
|
|
1266
|
+
htmlAttrs += ` ${attr}="{${tagPath}}"`;
|
|
1267
|
+
}
|
|
1268
|
+
if (type === "RECTANGLE") {
|
|
1269
|
+
return convertRectangleToHtml(node, indent);
|
|
1270
|
+
} else if (type === "ELLIPSE") {
|
|
1271
|
+
return convertEllipseToHtml(node, indent);
|
|
1272
|
+
} else if (type === "GROUP") {
|
|
1273
|
+
return convertGroupNode(node, analysis, context, convertNodeToJayHtml);
|
|
1274
|
+
} else if (type === "VECTOR" || type === "STAR" || type === "POLYGON" || type === "LINE" || type === "BOOLEAN_OPERATION") {
|
|
1275
|
+
return convertVectorToHtml(node, indent);
|
|
1276
|
+
} else if (children && children.length > 0) {
|
|
1277
|
+
let html = `${indent}<${tag} ${htmlAttrs}>
|
|
1278
|
+
`;
|
|
1279
|
+
html += `${indent} <!-- ${node.name} -->
|
|
1280
|
+
`;
|
|
1281
|
+
const childContext = {
|
|
1282
|
+
...context,
|
|
1283
|
+
indentLevel: context.indentLevel + 1
|
|
1284
|
+
};
|
|
1285
|
+
for (const child of children) {
|
|
1286
|
+
html += convertNodeToJayHtml(child, childContext);
|
|
1287
|
+
}
|
|
1288
|
+
html += `${indent}</${tag}>
|
|
1289
|
+
`;
|
|
1290
|
+
return html;
|
|
1291
|
+
} else {
|
|
1292
|
+
return `${indent}<!-- ${node.name} (${type}) -->
|
|
1293
|
+
`;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
function convertNodeToJayHtml(node, context) {
|
|
1297
|
+
const { name, type, children, pluginData } = node;
|
|
1298
|
+
const isJPage = pluginData?.["jpage"] === "true";
|
|
1299
|
+
const urlRoute = pluginData?.["urlRoute"];
|
|
1300
|
+
if (type === "TEXT" && node.fontName) {
|
|
1301
|
+
if (typeof node.fontName === "object" && node.fontName.family) {
|
|
1302
|
+
context.fontFamilies.add(node.fontName.family);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
const indent = " ".repeat(context.indentLevel);
|
|
1306
|
+
if (type === "SECTION" && isJPage) {
|
|
1307
|
+
let html = `${indent}<section data-figma-id="${node.id}" data-page-url="${urlRoute || ""}">
|
|
1308
|
+
`;
|
|
1309
|
+
html += `${indent} <!-- Jay Page: ${name} -->
|
|
1310
|
+
`;
|
|
1311
|
+
if (children && children.length > 0) {
|
|
1312
|
+
const childContext = {
|
|
1313
|
+
...context,
|
|
1314
|
+
indentLevel: context.indentLevel + 1
|
|
1315
|
+
};
|
|
1316
|
+
for (const child of children) {
|
|
1317
|
+
html += convertNodeToJayHtml(child, childContext);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
html += `${indent}</section>
|
|
1321
|
+
`;
|
|
1322
|
+
return html;
|
|
1323
|
+
}
|
|
1324
|
+
const bindings = getBindingsData(node);
|
|
1325
|
+
const analysis = analyzeBindings(bindings, context);
|
|
1326
|
+
validateBindings(analysis, node);
|
|
1327
|
+
if (analysis.isRepeater) {
|
|
1328
|
+
return convertRepeaterNode(node, analysis, context, convertNodeToJayHtml);
|
|
1329
|
+
}
|
|
1330
|
+
if (analysis.type === "property-variant") {
|
|
1331
|
+
return convertVariantNode(node, analysis, context, convertNodeToJayHtml);
|
|
1332
|
+
}
|
|
1333
|
+
return convertRegularNode(node, analysis, context);
|
|
1334
|
+
}
|
|
1335
|
+
function findContentFrame(section) {
|
|
1336
|
+
if (!section.children || section.children.length === 0) {
|
|
1337
|
+
return {
|
|
1338
|
+
frame: null,
|
|
1339
|
+
error: `Jay Page section "${section.name}" has no children`
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
const frameNodes = section.children.filter((child) => child.type === "FRAME");
|
|
1343
|
+
if (frameNodes.length === 0) {
|
|
1344
|
+
return {
|
|
1345
|
+
frame: null,
|
|
1346
|
+
error: `Jay Page section "${section.name}" has no FrameNode children. Found: ${section.children.map((c) => c.type).join(", ")}`
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
if (frameNodes.length > 1) {
|
|
1350
|
+
return {
|
|
1351
|
+
frame: frameNodes[0],
|
|
1352
|
+
warning: `Jay Page section "${section.name}" has ${frameNodes.length} FrameNodes, using the first one`
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
return { frame: frameNodes[0] };
|
|
1356
|
+
}
|
|
1357
|
+
const figmaVendor = {
|
|
1358
|
+
vendorId: "figma",
|
|
1359
|
+
async convertToBodyHtml(vendorDoc, pageUrl, projectPage, plugins) {
|
|
1360
|
+
console.log(`🎨 Converting Figma document for page: ${pageUrl}`);
|
|
1361
|
+
console.log(` Document type: ${vendorDoc.type}, name: ${vendorDoc.name}`);
|
|
1362
|
+
const isJPage = vendorDoc.pluginData?.["jpage"] === "true";
|
|
1363
|
+
if (!isJPage) {
|
|
1364
|
+
throw new Error(
|
|
1365
|
+
`Document "${vendorDoc.name}" is not marked as a Jay Page (missing jpage='true' in pluginData)`
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
const { frame, error, warning } = findContentFrame(vendorDoc);
|
|
1369
|
+
if (error) {
|
|
1370
|
+
throw new Error(`Cannot convert to Jay HTML: ${error}`);
|
|
1371
|
+
}
|
|
1372
|
+
if (warning) {
|
|
1373
|
+
console.warn(`⚠️ ${warning}`);
|
|
1374
|
+
}
|
|
1375
|
+
if (!frame) {
|
|
1376
|
+
throw new Error(`Cannot convert to Jay HTML: No content frame found`);
|
|
1377
|
+
}
|
|
1378
|
+
console.log(` Converting content frame: ${frame.name} (${frame.type})`);
|
|
1379
|
+
const fontFamilies = /* @__PURE__ */ new Set();
|
|
1380
|
+
const context = {
|
|
1381
|
+
repeaterPathStack: [],
|
|
1382
|
+
indentLevel: 1,
|
|
1383
|
+
// Start at 1 for body content
|
|
1384
|
+
fontFamilies,
|
|
1385
|
+
projectPage,
|
|
1386
|
+
plugins
|
|
1387
|
+
};
|
|
1388
|
+
const bodyHtml = convertNodeToJayHtml(frame, context);
|
|
1389
|
+
if (fontFamilies.size > 0) {
|
|
1390
|
+
console.log(
|
|
1391
|
+
` Found ${fontFamilies.size} font families: ${Array.from(fontFamilies).join(", ")}`
|
|
1392
|
+
);
|
|
1393
|
+
}
|
|
1394
|
+
return {
|
|
1395
|
+
bodyHtml,
|
|
1396
|
+
fontFamilies,
|
|
1397
|
+
// No contract data for now - Figma vendor doesn't generate contracts yet
|
|
1398
|
+
contractData: void 0
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
};
|
|
1402
|
+
const vendorRegistry = /* @__PURE__ */ new Map([
|
|
1403
|
+
[figmaVendor.vendorId, figmaVendor]
|
|
1404
|
+
// Add more vendors here as they are contributed
|
|
1405
|
+
]);
|
|
1406
|
+
function getVendor(vendorId) {
|
|
1407
|
+
return vendorRegistry.get(vendorId);
|
|
1408
|
+
}
|
|
1409
|
+
function hasVendor(vendorId) {
|
|
1410
|
+
return vendorRegistry.has(vendorId);
|
|
1411
|
+
}
|
|
1412
|
+
function getRegisteredVendors() {
|
|
1413
|
+
return Array.from(vendorRegistry.keys());
|
|
1414
|
+
}
|
|
1415
|
+
function escapeHtml(text) {
|
|
1416
|
+
if (!text)
|
|
1417
|
+
return "";
|
|
1418
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1419
|
+
}
|
|
1420
|
+
function generateGoogleFontsLinks(fontFamilies) {
|
|
1421
|
+
if (fontFamilies.size === 0) {
|
|
1422
|
+
return "";
|
|
1423
|
+
}
|
|
1424
|
+
const families = Array.from(fontFamilies);
|
|
1425
|
+
const googleFontsUrl = `https://fonts.googleapis.com/css2?${families.map((family) => {
|
|
1426
|
+
const encodedFamily = encodeURIComponent(family).replace(/%20/g, "+");
|
|
1427
|
+
return `family=${encodedFamily}:wght@100;200;300;400;500;600;700;800;900`;
|
|
1428
|
+
}).join("&")}&display=swap`;
|
|
1429
|
+
return ` <link rel="preconnect" href="https://fonts.googleapis.com">
|
|
1430
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
1431
|
+
<link href="${googleFontsUrl}" rel="stylesheet">`;
|
|
1432
|
+
}
|
|
1433
|
+
function generateHeadlessComponentScripts(components) {
|
|
1434
|
+
if (components.length === 0) {
|
|
1435
|
+
return "";
|
|
1436
|
+
}
|
|
1437
|
+
const scriptTags = components.map(
|
|
1438
|
+
(comp) => ` <script
|
|
1439
|
+
type="application/jay-headless"
|
|
1440
|
+
plugin="${comp.plugin}"
|
|
1441
|
+
contract="${comp.contract}"
|
|
1442
|
+
key="${comp.key}"
|
|
1443
|
+
><\/script>`
|
|
1444
|
+
);
|
|
1445
|
+
return "\n" + scriptTags.join("\n");
|
|
1446
|
+
}
|
|
1447
|
+
function generateJayDataScript(contractData) {
|
|
1448
|
+
if (contractData) {
|
|
1449
|
+
return ` <script type="application/jay-data">
|
|
1450
|
+
data:
|
|
1451
|
+
${contractData.tagsYaml}
|
|
1452
|
+
<\/script>`;
|
|
1453
|
+
}
|
|
1454
|
+
return ` <script type="application/jay-data">
|
|
1455
|
+
data:
|
|
1456
|
+
<\/script>`;
|
|
1457
|
+
}
|
|
1458
|
+
function buildJayHtml(options) {
|
|
1459
|
+
const {
|
|
1460
|
+
bodyHtml,
|
|
1461
|
+
fontFamilies,
|
|
1462
|
+
contractData,
|
|
1463
|
+
headlessComponents = [],
|
|
1464
|
+
title = "Page"
|
|
1465
|
+
} = options;
|
|
1466
|
+
const fontLinks = generateGoogleFontsLinks(fontFamilies);
|
|
1467
|
+
const headlessScripts = generateHeadlessComponentScripts(headlessComponents);
|
|
1468
|
+
const jayDataScript = generateJayDataScript(contractData);
|
|
1469
|
+
return `<!DOCTYPE html>
|
|
1470
|
+
<html lang="en">
|
|
1471
|
+
<head>
|
|
1472
|
+
<meta charset="UTF-8">
|
|
1473
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1474
|
+
${fontLinks}${headlessScripts}
|
|
1475
|
+
${jayDataScript}
|
|
1476
|
+
<title>${escapeHtml(title)}</title>
|
|
1477
|
+
<style>
|
|
1478
|
+
/* Basic reset */
|
|
1479
|
+
body { margin: 0; font-family: sans-serif; }
|
|
1480
|
+
a { color: inherit; text-decoration: none; }
|
|
1481
|
+
a:hover { text-decoration: underline; }
|
|
1482
|
+
div { box-sizing: border-box; }
|
|
1483
|
+
|
|
1484
|
+
/* Scrollbar styling for Webkit browsers */
|
|
1485
|
+
::-webkit-scrollbar {
|
|
1486
|
+
width: 8px;
|
|
1487
|
+
height: 8px;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
::-webkit-scrollbar-track {
|
|
1491
|
+
background: transparent;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
::-webkit-scrollbar-thumb {
|
|
1495
|
+
background: rgba(0, 0, 0, 0.3);
|
|
1496
|
+
border-radius: 4px;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
::-webkit-scrollbar-thumb:hover {
|
|
1500
|
+
background: rgba(0, 0, 0, 0.5);
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
/* Smooth scrolling */
|
|
1504
|
+
* {
|
|
1505
|
+
scroll-behavior: smooth;
|
|
1506
|
+
}
|
|
1507
|
+
</style>
|
|
1508
|
+
</head>
|
|
1509
|
+
<body>
|
|
1510
|
+
${bodyHtml}
|
|
1511
|
+
</body>
|
|
1512
|
+
</html>`;
|
|
1513
|
+
}
|
|
1514
|
+
async function buildJayHtmlFromVendorResult(conversionResult, pageDirectory, pageTitle) {
|
|
1515
|
+
const pageConfigPath = path.join(pageDirectory, "page.conf.yaml");
|
|
1516
|
+
const headlessComponents = [];
|
|
1517
|
+
if (fs.existsSync(pageConfigPath)) {
|
|
1518
|
+
try {
|
|
1519
|
+
const configContent = await fs.promises.readFile(pageConfigPath, "utf-8");
|
|
1520
|
+
const pageConfig = YAML.parse(configContent);
|
|
1521
|
+
if (pageConfig.used_components && Array.isArray(pageConfig.used_components)) {
|
|
1522
|
+
for (const comp of pageConfig.used_components) {
|
|
1523
|
+
if (comp.plugin && comp.contract && comp.key) {
|
|
1524
|
+
headlessComponents.push({
|
|
1525
|
+
plugin: comp.plugin,
|
|
1526
|
+
contract: comp.contract,
|
|
1527
|
+
key: comp.key
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
} catch (configError) {
|
|
1533
|
+
console.warn(`Failed to read page config ${pageConfigPath}:`, configError);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
const title = pageTitle || path.basename(pageDirectory);
|
|
1537
|
+
return buildJayHtml({
|
|
1538
|
+
bodyHtml: conversionResult.bodyHtml,
|
|
1539
|
+
fontFamilies: conversionResult.fontFamilies,
|
|
1540
|
+
contractData: conversionResult.contractData,
|
|
1541
|
+
headlessComponents,
|
|
1542
|
+
title
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
88
1545
|
const PAGE_FILENAME = `page${JAY_EXTENSION}`;
|
|
89
1546
|
const PAGE_CONTRACT_FILENAME = `page${JAY_CONTRACT_EXTENSION}`;
|
|
90
1547
|
const PAGE_CONFIG_FILENAME = "page.conf.yaml";
|
|
1548
|
+
function pageUrlToDirectoryPath(pageUrl, pagesBasePath) {
|
|
1549
|
+
const routePath = pageUrl === "/" ? "" : pageUrl;
|
|
1550
|
+
const fsPath = routePath.replace(/:([^/]+)/g, "[$1]");
|
|
1551
|
+
return path.join(pagesBasePath, fsPath);
|
|
1552
|
+
}
|
|
91
1553
|
function jayTypeToString(jayType) {
|
|
92
1554
|
if (!jayType)
|
|
93
1555
|
return void 0;
|
|
@@ -100,31 +1562,30 @@ function jayTypeToString(jayType) {
|
|
|
100
1562
|
}
|
|
101
1563
|
}
|
|
102
1564
|
function convertContractTagToProtocol(tag) {
|
|
103
|
-
const
|
|
1565
|
+
const typeArray = Array.isArray(tag.type) ? tag.type : [tag.type];
|
|
1566
|
+
const typeStrings = typeArray.map((t) => ContractTagType[t]);
|
|
1567
|
+
return {
|
|
104
1568
|
tag: tag.tag,
|
|
105
|
-
type:
|
|
1569
|
+
type: typeStrings.length === 1 ? typeStrings[0] : typeStrings,
|
|
1570
|
+
dataType: tag.dataType ? jayTypeToString(tag.dataType) : void 0,
|
|
1571
|
+
elementType: tag.elementType ? tag.elementType.join(" | ") : void 0,
|
|
1572
|
+
required: tag.required,
|
|
1573
|
+
repeated: tag.repeated,
|
|
1574
|
+
trackBy: tag.trackBy,
|
|
1575
|
+
async: tag.async,
|
|
1576
|
+
phase: tag.phase,
|
|
1577
|
+
link: tag.link,
|
|
1578
|
+
tags: tag.tags ? tag.tags.map(convertContractTagToProtocol) : void 0
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
function convertContractToProtocol(contract) {
|
|
1582
|
+
return {
|
|
1583
|
+
name: contract.name,
|
|
1584
|
+
tags: contract.tags.map(convertContractTagToProtocol)
|
|
106
1585
|
};
|
|
107
|
-
if (tag.dataType) {
|
|
108
|
-
protocolTag.dataType = jayTypeToString(tag.dataType);
|
|
109
|
-
}
|
|
110
|
-
if (tag.elementType) {
|
|
111
|
-
protocolTag.elementType = tag.elementType.join(" | ");
|
|
112
|
-
}
|
|
113
|
-
if (tag.required !== void 0) {
|
|
114
|
-
protocolTag.required = tag.required;
|
|
115
|
-
}
|
|
116
|
-
if (tag.repeated !== void 0) {
|
|
117
|
-
protocolTag.repeated = tag.repeated;
|
|
118
|
-
}
|
|
119
|
-
if (tag.link) {
|
|
120
|
-
protocolTag.link = tag.link;
|
|
121
|
-
}
|
|
122
|
-
if (tag.tags) {
|
|
123
|
-
protocolTag.tags = tag.tags.map(convertContractTagToProtocol);
|
|
124
|
-
}
|
|
125
|
-
return protocolTag;
|
|
126
1586
|
}
|
|
127
|
-
function isPageDirectory(
|
|
1587
|
+
async function isPageDirectory(dirPath) {
|
|
1588
|
+
const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
|
|
128
1589
|
const hasPageHtml = entries.some((e) => e.name === PAGE_FILENAME);
|
|
129
1590
|
const hasPageContract = entries.some((e) => e.name === PAGE_CONTRACT_FILENAME);
|
|
130
1591
|
const hasPageConfig = entries.some((e) => e.name === PAGE_CONFIG_FILENAME);
|
|
@@ -134,8 +1595,7 @@ function isPageDirectory(entries) {
|
|
|
134
1595
|
async function scanPageDirectories(pagesBasePath, onPageFound) {
|
|
135
1596
|
async function scanDirectory(dirPath, urlPath = "") {
|
|
136
1597
|
try {
|
|
137
|
-
const
|
|
138
|
-
const { isPage, hasPageHtml, hasPageContract, hasPageConfig } = isPageDirectory(entries);
|
|
1598
|
+
const { isPage, hasPageHtml, hasPageContract, hasPageConfig } = await isPageDirectory(dirPath);
|
|
139
1599
|
if (isPage) {
|
|
140
1600
|
const pageUrl = urlPath || "/";
|
|
141
1601
|
const pageName = dirPath === pagesBasePath ? "Home" : path.basename(dirPath);
|
|
@@ -148,6 +1608,7 @@ async function scanPageDirectories(pagesBasePath, onPageFound) {
|
|
|
148
1608
|
hasPageConfig
|
|
149
1609
|
});
|
|
150
1610
|
}
|
|
1611
|
+
const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
|
|
151
1612
|
for (const entry of entries) {
|
|
152
1613
|
const fullPath = path.join(dirPath, entry.name);
|
|
153
1614
|
if (entry.isDirectory()) {
|
|
@@ -158,246 +1619,124 @@ async function scanPageDirectories(pagesBasePath, onPageFound) {
|
|
|
158
1619
|
}
|
|
159
1620
|
}
|
|
160
1621
|
} catch (error) {
|
|
161
|
-
|
|
1622
|
+
getLogger().warn(`Failed to scan directory ${dirPath}:`, error);
|
|
162
1623
|
}
|
|
163
1624
|
}
|
|
164
1625
|
await scanDirectory(pagesBasePath);
|
|
165
1626
|
}
|
|
166
|
-
|
|
167
|
-
try {
|
|
168
|
-
const contractYaml = await fs.promises.readFile(contractFilePath, "utf-8");
|
|
169
|
-
const parsedContract = parseContract(contractYaml, contractFilePath);
|
|
170
|
-
if (parsedContract.validations.length > 0) {
|
|
171
|
-
console.warn(
|
|
172
|
-
`Contract validation errors in ${contractFilePath}:`,
|
|
173
|
-
parsedContract.validations
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
if (parsedContract.val) {
|
|
177
|
-
const resolvedTags = await resolveLinkedTags(
|
|
178
|
-
parsedContract.val.tags,
|
|
179
|
-
path.dirname(contractFilePath)
|
|
180
|
-
);
|
|
181
|
-
return {
|
|
182
|
-
name: parsedContract.val.name,
|
|
183
|
-
tags: resolvedTags
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
} catch (error) {
|
|
187
|
-
console.warn(`Failed to parse contract file ${contractFilePath}:`, error);
|
|
188
|
-
}
|
|
189
|
-
return null;
|
|
190
|
-
}
|
|
191
|
-
async function resolveLinkedTags(tags, baseDir) {
|
|
1627
|
+
function expandContractTags(tags, baseDir) {
|
|
192
1628
|
const resolvedTags = [];
|
|
193
1629
|
for (const tag of tags) {
|
|
194
1630
|
if (tag.link) {
|
|
195
1631
|
try {
|
|
196
|
-
const
|
|
197
|
-
const
|
|
198
|
-
|
|
1632
|
+
const linkWithExtension = tag.link.endsWith(JAY_CONTRACT_EXTENSION) ? tag.link : tag.link + JAY_CONTRACT_EXTENSION;
|
|
1633
|
+
const linkedPath = JAY_IMPORT_RESOLVER.resolveLink(baseDir, linkWithExtension);
|
|
1634
|
+
const loadResult = JAY_IMPORT_RESOLVER.loadContract(linkedPath);
|
|
1635
|
+
if (loadResult.val) {
|
|
1636
|
+
const expandedSubTags = expandContractTags(
|
|
1637
|
+
loadResult.val.tags,
|
|
1638
|
+
path.dirname(linkedPath)
|
|
1639
|
+
);
|
|
199
1640
|
const resolvedTag = {
|
|
200
1641
|
tag: tag.tag,
|
|
201
|
-
type: tag.type
|
|
202
|
-
|
|
203
|
-
|
|
1642
|
+
type: tag.type,
|
|
1643
|
+
// Keep the original enum type
|
|
1644
|
+
tags: expandedSubTags,
|
|
1645
|
+
required: tag.required,
|
|
1646
|
+
repeated: tag.repeated,
|
|
1647
|
+
trackBy: tag.trackBy,
|
|
1648
|
+
async: tag.async,
|
|
1649
|
+
phase: tag.phase,
|
|
1650
|
+
link: tag.link
|
|
204
1651
|
};
|
|
205
|
-
if (tag.required !== void 0) {
|
|
206
|
-
resolvedTag.required = tag.required;
|
|
207
|
-
}
|
|
208
|
-
if (tag.repeated !== void 0) {
|
|
209
|
-
resolvedTag.repeated = tag.repeated;
|
|
210
|
-
}
|
|
211
1652
|
resolvedTags.push(resolvedTag);
|
|
212
1653
|
} else {
|
|
213
|
-
|
|
214
|
-
resolvedTags.push(
|
|
1654
|
+
getLogger().warn(`Failed to load linked contract: ${tag.link} from ${baseDir}`);
|
|
1655
|
+
resolvedTags.push(tag);
|
|
215
1656
|
}
|
|
216
1657
|
} catch (error) {
|
|
217
|
-
|
|
218
|
-
resolvedTags.push(
|
|
1658
|
+
getLogger().warn(`Error resolving linked contract ${tag.link}:`, error);
|
|
1659
|
+
resolvedTags.push(tag);
|
|
219
1660
|
}
|
|
220
1661
|
} else if (tag.tags) {
|
|
221
|
-
const resolvedSubTags =
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
1662
|
+
const resolvedSubTags = expandContractTags(tag.tags, baseDir);
|
|
1663
|
+
const resolvedTag = {
|
|
1664
|
+
...tag,
|
|
1665
|
+
tags: resolvedSubTags
|
|
1666
|
+
};
|
|
1667
|
+
resolvedTags.push(resolvedTag);
|
|
225
1668
|
} else {
|
|
226
|
-
resolvedTags.push(
|
|
1669
|
+
resolvedTags.push(tag);
|
|
227
1670
|
}
|
|
228
1671
|
}
|
|
229
1672
|
return resolvedTags;
|
|
230
1673
|
}
|
|
231
|
-
function
|
|
1674
|
+
function loadAndExpandContract(contractFilePath) {
|
|
232
1675
|
try {
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
1676
|
+
const loadResult = JAY_IMPORT_RESOLVER.loadContract(contractFilePath);
|
|
1677
|
+
if (loadResult.validations.length > 0) {
|
|
1678
|
+
getLogger().warn(
|
|
1679
|
+
`Contract validation errors in ${contractFilePath}:`,
|
|
1680
|
+
loadResult.validations
|
|
1681
|
+
);
|
|
1682
|
+
}
|
|
1683
|
+
if (loadResult.val) {
|
|
1684
|
+
const resolvedTags = expandContractTags(
|
|
1685
|
+
loadResult.val.tags,
|
|
1686
|
+
path.dirname(contractFilePath)
|
|
1687
|
+
);
|
|
1688
|
+
return convertContractToProtocol({
|
|
1689
|
+
name: loadResult.val.name,
|
|
1690
|
+
tags: resolvedTags
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
237
1693
|
} catch (error) {
|
|
238
|
-
|
|
239
|
-
`Failed to resolve contract: ${appModule}/${contractFileName}`,
|
|
240
|
-
error instanceof Error ? error.message : error
|
|
241
|
-
);
|
|
242
|
-
return null;
|
|
1694
|
+
getLogger().warn(`Failed to parse contract file ${contractFilePath}:`, error);
|
|
243
1695
|
}
|
|
1696
|
+
return null;
|
|
244
1697
|
}
|
|
245
|
-
async function
|
|
246
|
-
const installedAppContracts = {};
|
|
247
|
-
const installedAppsPath = path.join(configBasePath, "installedApps");
|
|
1698
|
+
async function extractHeadlessComponentsFromJayHtml(jayHtmlContent, pageFilePath, projectRootPath) {
|
|
248
1699
|
try {
|
|
249
|
-
|
|
250
|
-
|
|
1700
|
+
const parsedJayHtml = await parseJayFile(
|
|
1701
|
+
jayHtmlContent,
|
|
1702
|
+
path.basename(pageFilePath),
|
|
1703
|
+
path.dirname(pageFilePath),
|
|
1704
|
+
{ relativePath: "" },
|
|
1705
|
+
// We don't need TypeScript config for headless extraction
|
|
1706
|
+
JAY_IMPORT_RESOLVER,
|
|
1707
|
+
projectRootPath
|
|
1708
|
+
);
|
|
1709
|
+
if (parsedJayHtml.validations.length > 0) {
|
|
1710
|
+
getLogger().warn(
|
|
1711
|
+
`Jay-HTML parsing warnings for ${pageFilePath}:`,
|
|
1712
|
+
parsedJayHtml.validations
|
|
1713
|
+
);
|
|
251
1714
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const appConfigPath = path.join(installedAppsPath, appDir.name, "app.conf.yaml");
|
|
256
|
-
try {
|
|
257
|
-
if (fs.existsSync(appConfigPath)) {
|
|
258
|
-
const configContent = await fs.promises.readFile(appConfigPath, "utf-8");
|
|
259
|
-
const appConfig = YAML.parse(configContent);
|
|
260
|
-
const appName = appConfig.name || appDir.name;
|
|
261
|
-
const appModule = appConfig.module || appDir.name;
|
|
262
|
-
const appContracts = {
|
|
263
|
-
appName,
|
|
264
|
-
module: appModule,
|
|
265
|
-
pages: [],
|
|
266
|
-
components: []
|
|
267
|
-
};
|
|
268
|
-
if (appConfig.pages && Array.isArray(appConfig.pages)) {
|
|
269
|
-
for (const page of appConfig.pages) {
|
|
270
|
-
if (page.headless_components && Array.isArray(page.headless_components)) {
|
|
271
|
-
for (const component of page.headless_components) {
|
|
272
|
-
if (component.contract) {
|
|
273
|
-
const contractPath = resolveAppContractPath(
|
|
274
|
-
appModule,
|
|
275
|
-
component.contract,
|
|
276
|
-
projectRootPath
|
|
277
|
-
);
|
|
278
|
-
if (contractPath) {
|
|
279
|
-
const contractSchema = await parseContractFile(contractPath);
|
|
280
|
-
if (contractSchema) {
|
|
281
|
-
appContracts.pages.push({
|
|
282
|
-
pageName: page.name,
|
|
283
|
-
contractSchema
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
if (appConfig.components && Array.isArray(appConfig.components)) {
|
|
293
|
-
for (const component of appConfig.components) {
|
|
294
|
-
if (component.headless_components && Array.isArray(component.headless_components)) {
|
|
295
|
-
for (const headlessComp of component.headless_components) {
|
|
296
|
-
if (headlessComp.contract) {
|
|
297
|
-
const contractPath = resolveAppContractPath(
|
|
298
|
-
appModule,
|
|
299
|
-
headlessComp.contract,
|
|
300
|
-
projectRootPath
|
|
301
|
-
);
|
|
302
|
-
if (contractPath) {
|
|
303
|
-
const contractSchema = await parseContractFile(contractPath);
|
|
304
|
-
if (contractSchema) {
|
|
305
|
-
appContracts.components.push({
|
|
306
|
-
componentName: component.name,
|
|
307
|
-
contractSchema
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
installedAppContracts[appName] = appContracts;
|
|
317
|
-
}
|
|
318
|
-
} catch (error) {
|
|
319
|
-
console.warn(`Failed to parse app config ${appConfigPath}:`, error);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
1715
|
+
if (!parsedJayHtml.val) {
|
|
1716
|
+
getLogger().warn(`Failed to parse jay-html file: ${pageFilePath}`);
|
|
1717
|
+
return [];
|
|
322
1718
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const headlessScripts = root.querySelectorAll('script[type="application/jay-headless"]');
|
|
331
|
-
const resolvedComponents = [];
|
|
332
|
-
for (const script of headlessScripts) {
|
|
333
|
-
const src = script.getAttribute("src") || "";
|
|
334
|
-
const name = script.getAttribute("name") || "";
|
|
335
|
-
const key = script.getAttribute("key") || "";
|
|
336
|
-
let resolved = false;
|
|
337
|
-
for (const app of installedApps) {
|
|
338
|
-
if (app.module !== src && app.name !== src) {
|
|
339
|
-
continue;
|
|
340
|
-
}
|
|
341
|
-
for (const appPage of app.pages) {
|
|
342
|
-
for (const headlessComp of appPage.headless_components) {
|
|
343
|
-
if (headlessComp.name === name && headlessComp.key === key) {
|
|
344
|
-
const appContracts = installedAppContracts[app.name];
|
|
345
|
-
if (appContracts) {
|
|
346
|
-
const matchingPageContract = appContracts.pages.find(
|
|
347
|
-
(pc) => pc.pageName === appPage.name
|
|
348
|
-
);
|
|
349
|
-
if (matchingPageContract) {
|
|
350
|
-
resolvedComponents.push({
|
|
351
|
-
appName: app.name,
|
|
352
|
-
componentName: appPage.name,
|
|
353
|
-
key
|
|
354
|
-
});
|
|
355
|
-
resolved = true;
|
|
356
|
-
break;
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
if (resolved)
|
|
362
|
-
break;
|
|
363
|
-
}
|
|
364
|
-
if (resolved)
|
|
365
|
-
break;
|
|
366
|
-
for (const appComponent of app.components) {
|
|
367
|
-
for (const headlessComp of appComponent.headless_components) {
|
|
368
|
-
if (headlessComp.name === name && headlessComp.key === key) {
|
|
369
|
-
const appContracts = installedAppContracts[app.name];
|
|
370
|
-
if (appContracts) {
|
|
371
|
-
const matchingComponentContract = appContracts.components.find(
|
|
372
|
-
(cc) => cc.componentName === appComponent.name
|
|
373
|
-
);
|
|
374
|
-
if (matchingComponentContract) {
|
|
375
|
-
resolvedComponents.push({
|
|
376
|
-
appName: app.name,
|
|
377
|
-
componentName: appComponent.name,
|
|
378
|
-
key
|
|
379
|
-
});
|
|
380
|
-
resolved = true;
|
|
381
|
-
break;
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
1719
|
+
const resolvedComponents = [];
|
|
1720
|
+
for (const headlessImport of parsedJayHtml.val.headlessImports) {
|
|
1721
|
+
if (headlessImport.codeLink) {
|
|
1722
|
+
let pluginName = headlessImport.codeLink.module;
|
|
1723
|
+
const nodeModulesMatch = pluginName.match(/node_modules\/([^/]+)/);
|
|
1724
|
+
if (nodeModulesMatch) {
|
|
1725
|
+
pluginName = nodeModulesMatch[1];
|
|
385
1726
|
}
|
|
386
|
-
|
|
387
|
-
|
|
1727
|
+
const componentName = headlessImport.contract?.name || "unknown";
|
|
1728
|
+
resolvedComponents.push({
|
|
1729
|
+
appName: pluginName,
|
|
1730
|
+
componentName,
|
|
1731
|
+
key: headlessImport.key
|
|
1732
|
+
});
|
|
388
1733
|
}
|
|
389
|
-
if (resolved)
|
|
390
|
-
break;
|
|
391
|
-
}
|
|
392
|
-
if (!resolved) {
|
|
393
|
-
resolvedComponents.push({
|
|
394
|
-
appName: src,
|
|
395
|
-
componentName: name,
|
|
396
|
-
key
|
|
397
|
-
});
|
|
398
1734
|
}
|
|
1735
|
+
return resolvedComponents;
|
|
1736
|
+
} catch (error) {
|
|
1737
|
+
getLogger().warn(`Failed to parse jay-html content for ${pageFilePath}:`, error);
|
|
1738
|
+
return [];
|
|
399
1739
|
}
|
|
400
|
-
return resolvedComponents;
|
|
401
1740
|
}
|
|
402
1741
|
async function scanProjectComponents(componentsBasePath) {
|
|
403
1742
|
const components = [];
|
|
@@ -420,43 +1759,10 @@ async function scanProjectComponents(componentsBasePath) {
|
|
|
420
1759
|
}
|
|
421
1760
|
}
|
|
422
1761
|
} catch (error) {
|
|
423
|
-
|
|
1762
|
+
getLogger().warn(`Failed to scan components directory ${componentsBasePath}:`, error);
|
|
424
1763
|
}
|
|
425
1764
|
return components;
|
|
426
1765
|
}
|
|
427
|
-
async function scanInstalledApps(configBasePath) {
|
|
428
|
-
const installedApps = [];
|
|
429
|
-
const installedAppsPath = path.join(configBasePath, "installedApps");
|
|
430
|
-
try {
|
|
431
|
-
if (!fs.existsSync(installedAppsPath)) {
|
|
432
|
-
return installedApps;
|
|
433
|
-
}
|
|
434
|
-
const appDirs = await fs.promises.readdir(installedAppsPath, { withFileTypes: true });
|
|
435
|
-
for (const appDir of appDirs) {
|
|
436
|
-
if (appDir.isDirectory()) {
|
|
437
|
-
const appConfigPath = path.join(installedAppsPath, appDir.name, "app.conf.yaml");
|
|
438
|
-
try {
|
|
439
|
-
if (fs.existsSync(appConfigPath)) {
|
|
440
|
-
const configContent = await fs.promises.readFile(appConfigPath, "utf-8");
|
|
441
|
-
const appConfig = YAML.parse(configContent);
|
|
442
|
-
installedApps.push({
|
|
443
|
-
name: appConfig.name || appDir.name,
|
|
444
|
-
module: appConfig.module || appDir.name,
|
|
445
|
-
pages: appConfig.pages || [],
|
|
446
|
-
components: appConfig.components || [],
|
|
447
|
-
config_map: appConfig.config_map || []
|
|
448
|
-
});
|
|
449
|
-
}
|
|
450
|
-
} catch (error) {
|
|
451
|
-
console.warn(`Failed to parse app config ${appConfigPath}:`, error);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
} catch (error) {
|
|
456
|
-
console.warn(`Failed to scan installed apps directory ${installedAppsPath}:`, error);
|
|
457
|
-
}
|
|
458
|
-
return installedApps;
|
|
459
|
-
}
|
|
460
1766
|
async function getProjectName(configBasePath) {
|
|
461
1767
|
const projectConfigPath = path.join(configBasePath, "project.conf.yaml");
|
|
462
1768
|
try {
|
|
@@ -466,265 +1772,256 @@ async function getProjectName(configBasePath) {
|
|
|
466
1772
|
return projectConfig.name || "Unnamed Project";
|
|
467
1773
|
}
|
|
468
1774
|
} catch (error) {
|
|
469
|
-
|
|
1775
|
+
getLogger().warn(`Failed to read project config ${projectConfigPath}:`, error);
|
|
470
1776
|
}
|
|
471
1777
|
return "Unnamed Project";
|
|
472
1778
|
}
|
|
473
|
-
async function
|
|
1779
|
+
async function scanLocalPluginNames(projectRoot) {
|
|
474
1780
|
const plugins = [];
|
|
475
|
-
const
|
|
476
|
-
if (fs.existsSync(
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const
|
|
1781
|
+
const localPluginsDir = path.join(projectRoot, LOCAL_PLUGIN_PATH);
|
|
1782
|
+
if (!fs.existsSync(localPluginsDir)) {
|
|
1783
|
+
return plugins;
|
|
1784
|
+
}
|
|
1785
|
+
try {
|
|
1786
|
+
const entries = await fs.promises.readdir(localPluginsDir, { withFileTypes: true });
|
|
1787
|
+
for (const entry of entries) {
|
|
1788
|
+
if (entry.isDirectory()) {
|
|
1789
|
+
const pluginDir = path.join(localPluginsDir, entry.name);
|
|
1790
|
+
const pluginYamlPath = path.join(pluginDir, "plugin.yaml");
|
|
484
1791
|
if (fs.existsSync(pluginYamlPath)) {
|
|
485
|
-
|
|
486
|
-
const yamlContent = await fs.promises.readFile(pluginYamlPath, "utf-8");
|
|
487
|
-
const manifest = YAML.parse(yamlContent);
|
|
488
|
-
plugins.push({
|
|
489
|
-
manifest,
|
|
490
|
-
location: {
|
|
491
|
-
type: "local",
|
|
492
|
-
path: pluginPath
|
|
493
|
-
}
|
|
494
|
-
});
|
|
495
|
-
} catch (error) {
|
|
496
|
-
console.warn(`Failed to parse plugin.yaml for ${dir.name}:`, error);
|
|
497
|
-
}
|
|
1792
|
+
plugins.push(entry.name);
|
|
498
1793
|
}
|
|
499
1794
|
}
|
|
500
|
-
} catch (error) {
|
|
501
|
-
console.warn(`Failed to scan local plugins directory ${localPluginsPath}:`, error);
|
|
502
1795
|
}
|
|
1796
|
+
} catch (error) {
|
|
1797
|
+
getLogger().warn(`Failed to scan local plugins directory ${localPluginsDir}:`, error);
|
|
503
1798
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
manifest: {
|
|
543
|
-
...manifest,
|
|
544
|
-
module: moduleName
|
|
545
|
-
},
|
|
546
|
-
location: {
|
|
547
|
-
type: "npm",
|
|
548
|
-
module: moduleName || manifest.name
|
|
549
|
-
}
|
|
550
|
-
});
|
|
551
|
-
} catch (error) {
|
|
552
|
-
console.warn(
|
|
553
|
-
`Failed to parse plugin.yaml for package ${pkgPath}:`,
|
|
554
|
-
error
|
|
555
|
-
);
|
|
556
|
-
}
|
|
1799
|
+
return plugins;
|
|
1800
|
+
}
|
|
1801
|
+
async function findPluginNamesFromPackageJson(projectRootPath) {
|
|
1802
|
+
const pluginNames = [];
|
|
1803
|
+
try {
|
|
1804
|
+
const packageJsonPath = path.join(projectRootPath, "package.json");
|
|
1805
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
1806
|
+
getLogger().warn("package.json not found");
|
|
1807
|
+
return pluginNames;
|
|
1808
|
+
}
|
|
1809
|
+
const packageJsonContent = await fs.promises.readFile(packageJsonPath, "utf-8");
|
|
1810
|
+
const packageJson = JSON.parse(packageJsonContent);
|
|
1811
|
+
const workspaceDependencies = /* @__PURE__ */ new Set();
|
|
1812
|
+
const regularDependencies = /* @__PURE__ */ new Set();
|
|
1813
|
+
for (const [depName, version] of Object.entries({
|
|
1814
|
+
...packageJson.dependencies
|
|
1815
|
+
})) {
|
|
1816
|
+
if (typeof version === "string" && version.startsWith("workspace:")) {
|
|
1817
|
+
workspaceDependencies.add(depName);
|
|
1818
|
+
} else {
|
|
1819
|
+
regularDependencies.add(depName);
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
const nodeModulesPath = path.join(projectRootPath, "node_modules");
|
|
1823
|
+
for (const depName of regularDependencies) {
|
|
1824
|
+
if (await checkPackageForPlugin(nodeModulesPath, depName)) {
|
|
1825
|
+
pluginNames.push(depName);
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
if (workspaceDependencies.size > 0) {
|
|
1829
|
+
const workspaceNodeModules = await findWorkspaceNodeModulesPath(
|
|
1830
|
+
projectRootPath,
|
|
1831
|
+
Array.from(workspaceDependencies)
|
|
1832
|
+
);
|
|
1833
|
+
if (workspaceNodeModules) {
|
|
1834
|
+
for (const depName of workspaceDependencies) {
|
|
1835
|
+
if (await checkPackageForPlugin(workspaceNodeModules, depName)) {
|
|
1836
|
+
pluginNames.push(depName);
|
|
557
1837
|
}
|
|
558
1838
|
}
|
|
559
1839
|
}
|
|
560
|
-
} catch (error) {
|
|
561
|
-
console.warn(`Failed to scan node_modules for plugins:`, error);
|
|
562
1840
|
}
|
|
1841
|
+
} catch (error) {
|
|
1842
|
+
getLogger().error("Error finding plugins from package.json:", error);
|
|
563
1843
|
}
|
|
564
|
-
return
|
|
1844
|
+
return pluginNames;
|
|
565
1845
|
}
|
|
566
|
-
async function
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
const parsedContract = await parseContractFile(contractPath);
|
|
585
|
-
if (parsedContract) {
|
|
586
|
-
contractSchema = parsedContract;
|
|
1846
|
+
async function checkPackageForPlugin(nodeModulesDir, packageName) {
|
|
1847
|
+
try {
|
|
1848
|
+
const packageDir = path.join(nodeModulesDir, packageName);
|
|
1849
|
+
const pluginYamlPath = path.join(packageDir, "plugin.yaml");
|
|
1850
|
+
return fs.existsSync(pluginYamlPath);
|
|
1851
|
+
} catch (error) {
|
|
1852
|
+
return false;
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
async function findWorkspaceNodeModulesPath(startPath, workspaceDeps) {
|
|
1856
|
+
let currentPath = startPath;
|
|
1857
|
+
while (currentPath !== path.dirname(currentPath)) {
|
|
1858
|
+
const nodeModulesPath = path.join(currentPath, "node_modules");
|
|
1859
|
+
if (fs.existsSync(nodeModulesPath)) {
|
|
1860
|
+
for (const depName of workspaceDeps) {
|
|
1861
|
+
const depPath = path.join(nodeModulesPath, depName);
|
|
1862
|
+
if (fs.existsSync(depPath)) {
|
|
1863
|
+
return nodeModulesPath;
|
|
587
1864
|
}
|
|
588
|
-
} catch (error) {
|
|
589
|
-
console.warn(`Failed to parse contract file ${contractPath}:`, error);
|
|
590
1865
|
}
|
|
591
1866
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
1867
|
+
currentPath = path.dirname(currentPath);
|
|
1868
|
+
}
|
|
1869
|
+
return null;
|
|
1870
|
+
}
|
|
1871
|
+
async function scanPlugins(projectRootPath) {
|
|
1872
|
+
const plugins = [];
|
|
1873
|
+
try {
|
|
1874
|
+
const [localPluginNames, dependencyPluginNames] = await Promise.all([
|
|
1875
|
+
scanLocalPluginNames(projectRootPath),
|
|
1876
|
+
findPluginNamesFromPackageJson(projectRootPath)
|
|
1877
|
+
]);
|
|
1878
|
+
const allPluginNames = [.../* @__PURE__ */ new Set([...localPluginNames, ...dependencyPluginNames])];
|
|
1879
|
+
getLogger().info(`Found ${allPluginNames.length} plugins: ${allPluginNames.join(", ")}`);
|
|
1880
|
+
for (const pluginName of allPluginNames) {
|
|
1881
|
+
const manifest = resolvePluginManifest(projectRootPath, pluginName);
|
|
1882
|
+
if (manifest.validations.length > 0) {
|
|
1883
|
+
getLogger().warn(
|
|
1884
|
+
`Failed to resolve plugin manifest for ${pluginName}:`,
|
|
1885
|
+
manifest.validations
|
|
599
1886
|
);
|
|
600
|
-
|
|
601
|
-
console.warn(`Failed to read page file ${pageFilePath}:`, error);
|
|
1887
|
+
continue;
|
|
602
1888
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
1889
|
+
if (!manifest.val) {
|
|
1890
|
+
getLogger().warn(
|
|
1891
|
+
`Failed to resolve plugin manifest for ${pluginName}:`,
|
|
1892
|
+
manifest.validations
|
|
1893
|
+
);
|
|
1894
|
+
continue;
|
|
1895
|
+
}
|
|
1896
|
+
const contracts = manifest.val.contracts;
|
|
1897
|
+
plugins.push({
|
|
1898
|
+
name: pluginName,
|
|
1899
|
+
contracts: contracts.map((contract) => {
|
|
1900
|
+
const resolveResult = JAY_IMPORT_RESOLVER.resolvePluginComponent(
|
|
1901
|
+
pluginName,
|
|
1902
|
+
contract.name,
|
|
1903
|
+
projectRootPath
|
|
1904
|
+
);
|
|
1905
|
+
if (resolveResult.validations.length > 0) {
|
|
1906
|
+
getLogger().warn(
|
|
1907
|
+
`Failed to resolve plugin component for ${pluginName}:${contract.name}:`,
|
|
1908
|
+
resolveResult.validations
|
|
1909
|
+
);
|
|
1910
|
+
return null;
|
|
1911
|
+
}
|
|
1912
|
+
if (!resolveResult.val) {
|
|
1913
|
+
getLogger().warn(
|
|
1914
|
+
`Failed to resolve plugin component for ${pluginName}:${contract.name}:`,
|
|
1915
|
+
resolveResult.validations
|
|
1916
|
+
);
|
|
1917
|
+
return null;
|
|
1918
|
+
}
|
|
1919
|
+
const expandedContract = loadAndExpandContract(resolveResult.val.contractPath);
|
|
1920
|
+
if (!expandedContract) {
|
|
1921
|
+
return null;
|
|
1922
|
+
}
|
|
1923
|
+
return expandedContract;
|
|
1924
|
+
})
|
|
1925
|
+
});
|
|
1926
|
+
}
|
|
1927
|
+
} catch (error) {
|
|
1928
|
+
getLogger().error("Error scanning plugins:", error);
|
|
1929
|
+
}
|
|
1930
|
+
return plugins;
|
|
1931
|
+
}
|
|
1932
|
+
async function loadProjectPage(pageContext, plugins) {
|
|
1933
|
+
const { dirPath, pageUrl, pageName, hasPageHtml, hasPageContract, hasPageConfig } = pageContext;
|
|
1934
|
+
const pageFilePath = path.join(dirPath, PAGE_FILENAME);
|
|
1935
|
+
const pageConfigPath = path.join(dirPath, PAGE_CONFIG_FILENAME);
|
|
1936
|
+
const contractPath = path.join(dirPath, PAGE_CONTRACT_FILENAME);
|
|
1937
|
+
const projectRootPath = process.cwd();
|
|
1938
|
+
let usedComponents = [];
|
|
1939
|
+
let contract;
|
|
1940
|
+
if (hasPageContract) {
|
|
1941
|
+
const parsedContract = loadAndExpandContract(contractPath);
|
|
1942
|
+
if (parsedContract) {
|
|
1943
|
+
contract = parsedContract;
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
if (hasPageHtml) {
|
|
1947
|
+
try {
|
|
1948
|
+
const jayHtmlContent = await fs.promises.readFile(pageFilePath, "utf-8");
|
|
1949
|
+
usedComponents = await extractHeadlessComponentsFromJayHtml(
|
|
1950
|
+
jayHtmlContent,
|
|
1951
|
+
pageFilePath,
|
|
1952
|
+
projectRootPath
|
|
1953
|
+
);
|
|
1954
|
+
} catch (error) {
|
|
1955
|
+
getLogger().warn(`Failed to read page file ${pageFilePath}:`, error);
|
|
1956
|
+
}
|
|
1957
|
+
} else if (hasPageConfig) {
|
|
1958
|
+
try {
|
|
1959
|
+
const configContent = await fs.promises.readFile(pageConfigPath, "utf-8");
|
|
1960
|
+
const pageConfig = YAML.parse(configContent);
|
|
1961
|
+
if (pageConfig.used_components && Array.isArray(pageConfig.used_components)) {
|
|
1962
|
+
for (const comp of pageConfig.used_components) {
|
|
1963
|
+
const key = comp.key || "";
|
|
1964
|
+
if (comp.plugin && comp.contract) {
|
|
1965
|
+
const plugin = plugins.find((p) => p.name === comp.plugin);
|
|
1966
|
+
if (plugin && plugin.contracts) {
|
|
1967
|
+
const contract2 = plugin.contracts.find((c) => c.name === comp.contract);
|
|
1968
|
+
if (contract2) {
|
|
1969
|
+
usedComponents.push({
|
|
1970
|
+
appName: comp.plugin,
|
|
1971
|
+
componentName: comp.contract,
|
|
1972
|
+
key
|
|
1973
|
+
});
|
|
639
1974
|
continue;
|
|
640
1975
|
}
|
|
641
|
-
for (const appPage of app.pages) {
|
|
642
|
-
for (const headlessComp of appPage.headless_components) {
|
|
643
|
-
if (headlessComp.name === name && headlessComp.key === key) {
|
|
644
|
-
const appContracts = installedAppContracts[app.name];
|
|
645
|
-
if (appContracts) {
|
|
646
|
-
const matchingPageContract = appContracts.pages.find(
|
|
647
|
-
(pc) => pc.pageName === appPage.name
|
|
648
|
-
);
|
|
649
|
-
if (matchingPageContract) {
|
|
650
|
-
usedComponents.push({
|
|
651
|
-
appName: app.name,
|
|
652
|
-
componentName: appPage.name,
|
|
653
|
-
key
|
|
654
|
-
});
|
|
655
|
-
resolved = true;
|
|
656
|
-
break;
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
if (resolved)
|
|
662
|
-
break;
|
|
663
|
-
}
|
|
664
|
-
if (resolved)
|
|
665
|
-
break;
|
|
666
|
-
for (const appComponent of app.components) {
|
|
667
|
-
for (const headlessComp of appComponent.headless_components) {
|
|
668
|
-
if (headlessComp.name === name && headlessComp.key === key) {
|
|
669
|
-
const appContracts = installedAppContracts[app.name];
|
|
670
|
-
if (appContracts) {
|
|
671
|
-
const matchingComponentContract = appContracts.components.find(
|
|
672
|
-
(cc) => cc.componentName === appComponent.name
|
|
673
|
-
);
|
|
674
|
-
if (matchingComponentContract) {
|
|
675
|
-
usedComponents.push({
|
|
676
|
-
appName: app.name,
|
|
677
|
-
componentName: appComponent.name,
|
|
678
|
-
key
|
|
679
|
-
});
|
|
680
|
-
resolved = true;
|
|
681
|
-
break;
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
if (resolved)
|
|
687
|
-
break;
|
|
688
|
-
}
|
|
689
|
-
if (resolved)
|
|
690
|
-
break;
|
|
691
|
-
}
|
|
692
|
-
if (!resolved) {
|
|
693
|
-
usedComponents.push({
|
|
694
|
-
appName: src,
|
|
695
|
-
componentName: name,
|
|
696
|
-
key
|
|
697
|
-
});
|
|
698
1976
|
}
|
|
1977
|
+
usedComponents.push({
|
|
1978
|
+
appName: comp.plugin,
|
|
1979
|
+
componentName: comp.contract,
|
|
1980
|
+
key
|
|
1981
|
+
});
|
|
1982
|
+
} else {
|
|
1983
|
+
getLogger().warn(
|
|
1984
|
+
`Invalid component definition in ${pageConfigPath}: Only plugin/contract syntax is supported for headless components. Found:`,
|
|
1985
|
+
comp
|
|
1986
|
+
);
|
|
699
1987
|
}
|
|
700
1988
|
}
|
|
701
|
-
} catch (error) {
|
|
702
|
-
console.warn(`Failed to parse page config ${pageConfigPath}:`, error);
|
|
703
1989
|
}
|
|
1990
|
+
} catch (error) {
|
|
1991
|
+
getLogger().warn(`Failed to parse page config ${pageConfigPath}:`, error);
|
|
704
1992
|
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
1993
|
+
}
|
|
1994
|
+
return {
|
|
1995
|
+
name: pageName,
|
|
1996
|
+
url: pageUrl,
|
|
1997
|
+
filePath: pageFilePath,
|
|
1998
|
+
contract,
|
|
1999
|
+
usedComponents
|
|
2000
|
+
};
|
|
2001
|
+
}
|
|
2002
|
+
async function scanProjectInfo(pagesBasePath, componentsBasePath, configBasePath, projectRootPath) {
|
|
2003
|
+
const [projectName, components, plugins] = await Promise.all([
|
|
2004
|
+
getProjectName(configBasePath),
|
|
2005
|
+
scanProjectComponents(componentsBasePath),
|
|
2006
|
+
scanPlugins(projectRootPath)
|
|
2007
|
+
]);
|
|
2008
|
+
const pages = [];
|
|
2009
|
+
await scanPageDirectories(pagesBasePath, async (context) => {
|
|
2010
|
+
const page = await loadProjectPage(context, plugins);
|
|
2011
|
+
pages.push(page);
|
|
712
2012
|
});
|
|
713
2013
|
return {
|
|
714
2014
|
name: projectName,
|
|
715
2015
|
localPath: projectRootPath,
|
|
716
2016
|
pages,
|
|
717
2017
|
components,
|
|
718
|
-
installedApps,
|
|
719
|
-
installedAppContracts,
|
|
720
2018
|
plugins
|
|
721
2019
|
};
|
|
722
2020
|
}
|
|
723
2021
|
async function handlePagePublish(resolvedConfig, page) {
|
|
724
2022
|
try {
|
|
725
2023
|
const pagesBasePath = path.resolve(resolvedConfig.devServer.pagesBase);
|
|
726
|
-
const
|
|
727
|
-
const dirname = path.join(pagesBasePath, routePath);
|
|
2024
|
+
const dirname = pageUrlToDirectoryPath(page.route, pagesBasePath);
|
|
728
2025
|
const fullPath = path.join(dirname, PAGE_FILENAME);
|
|
729
2026
|
await fs.promises.mkdir(dirname, { recursive: true });
|
|
730
2027
|
await fs.promises.writeFile(fullPath, page.jayHtml, "utf-8");
|
|
@@ -732,7 +2029,7 @@ async function handlePagePublish(resolvedConfig, page) {
|
|
|
732
2029
|
if (page.contract) {
|
|
733
2030
|
contractPath = path.join(dirname, `page${JAY_CONTRACT_EXTENSION}`);
|
|
734
2031
|
await fs.promises.writeFile(contractPath, page.contract, "utf-8");
|
|
735
|
-
|
|
2032
|
+
getLogger().info(`📄 Published page contract: ${contractPath}`);
|
|
736
2033
|
}
|
|
737
2034
|
const createdJayHtml = {
|
|
738
2035
|
jayHtml: page.jayHtml,
|
|
@@ -740,7 +2037,7 @@ async function handlePagePublish(resolvedConfig, page) {
|
|
|
740
2037
|
dirname,
|
|
741
2038
|
fullPath
|
|
742
2039
|
};
|
|
743
|
-
|
|
2040
|
+
getLogger().info(`📝 Published page: ${fullPath}`);
|
|
744
2041
|
return [
|
|
745
2042
|
{
|
|
746
2043
|
success: true,
|
|
@@ -750,7 +2047,7 @@ async function handlePagePublish(resolvedConfig, page) {
|
|
|
750
2047
|
createdJayHtml
|
|
751
2048
|
];
|
|
752
2049
|
} catch (error) {
|
|
753
|
-
|
|
2050
|
+
getLogger().error(`Failed to publish page ${page.route}:`, error);
|
|
754
2051
|
return [
|
|
755
2052
|
{
|
|
756
2053
|
success: false,
|
|
@@ -778,7 +2075,7 @@ async function handleComponentPublish(resolvedConfig, component) {
|
|
|
778
2075
|
dirname,
|
|
779
2076
|
fullPath
|
|
780
2077
|
};
|
|
781
|
-
|
|
2078
|
+
getLogger().info(`🧩 Published component: ${fullPath}`);
|
|
782
2079
|
return [
|
|
783
2080
|
{
|
|
784
2081
|
success: true,
|
|
@@ -788,7 +2085,7 @@ async function handleComponentPublish(resolvedConfig, component) {
|
|
|
788
2085
|
createdJayHtml
|
|
789
2086
|
];
|
|
790
2087
|
} catch (error) {
|
|
791
|
-
|
|
2088
|
+
getLogger().error(`Failed to publish component ${component.name}:`, error);
|
|
792
2089
|
return [
|
|
793
2090
|
{
|
|
794
2091
|
success: false,
|
|
@@ -798,6 +2095,22 @@ async function handleComponentPublish(resolvedConfig, component) {
|
|
|
798
2095
|
];
|
|
799
2096
|
}
|
|
800
2097
|
}
|
|
2098
|
+
async function loadPageContracts(dirPath, pageUrl, projectRootPath) {
|
|
2099
|
+
const { hasPageHtml, hasPageContract, hasPageConfig } = await isPageDirectory(dirPath);
|
|
2100
|
+
const plugins = await scanPlugins(projectRootPath);
|
|
2101
|
+
const pageInfo = await loadProjectPage(
|
|
2102
|
+
{
|
|
2103
|
+
dirPath,
|
|
2104
|
+
pageUrl,
|
|
2105
|
+
pageName: path.basename(dirPath),
|
|
2106
|
+
hasPageHtml,
|
|
2107
|
+
hasPageContract,
|
|
2108
|
+
hasPageConfig
|
|
2109
|
+
},
|
|
2110
|
+
plugins
|
|
2111
|
+
);
|
|
2112
|
+
return { projectPage: pageInfo, plugins };
|
|
2113
|
+
}
|
|
801
2114
|
function createEditorHandlers(config, tsConfigPath, projectRoot) {
|
|
802
2115
|
const onPublish = async (params) => {
|
|
803
2116
|
const status = [];
|
|
@@ -832,7 +2145,7 @@ function createEditorHandlers(config, tsConfigPath, projectRoot) {
|
|
|
832
2145
|
);
|
|
833
2146
|
const definitionFile = generateElementDefinitionFile(parsedJayHtml);
|
|
834
2147
|
if (definitionFile.validations.length > 0)
|
|
835
|
-
|
|
2148
|
+
getLogger().info(
|
|
836
2149
|
`failed to generate .d.ts for ${fullPath} with validation errors: ${definitionFile.validations.join("\n")}`
|
|
837
2150
|
);
|
|
838
2151
|
else
|
|
@@ -851,14 +2164,14 @@ function createEditorHandlers(config, tsConfigPath, projectRoot) {
|
|
|
851
2164
|
const filename = `${params.imageId}.png`;
|
|
852
2165
|
const imagePath = path.join(imagesDir, filename);
|
|
853
2166
|
await fs.promises.writeFile(imagePath, Buffer.from(params.imageData, "base64"));
|
|
854
|
-
|
|
2167
|
+
getLogger().info(`🖼️ Saved image: ${imagePath}`);
|
|
855
2168
|
return {
|
|
856
2169
|
type: "saveImage",
|
|
857
2170
|
success: true,
|
|
858
2171
|
imageUrl: `/images/${filename}`
|
|
859
2172
|
};
|
|
860
2173
|
} catch (error) {
|
|
861
|
-
|
|
2174
|
+
getLogger().error("Failed to save image:", error);
|
|
862
2175
|
return {
|
|
863
2176
|
type: "saveImage",
|
|
864
2177
|
success: false,
|
|
@@ -882,7 +2195,7 @@ function createEditorHandlers(config, tsConfigPath, projectRoot) {
|
|
|
882
2195
|
imageUrl: exists ? `/images/${filename}` : void 0
|
|
883
2196
|
};
|
|
884
2197
|
} catch (error) {
|
|
885
|
-
|
|
2198
|
+
getLogger().error("Failed to check image:", error);
|
|
886
2199
|
return {
|
|
887
2200
|
type: "hasImage",
|
|
888
2201
|
success: false,
|
|
@@ -896,25 +2209,23 @@ function createEditorHandlers(config, tsConfigPath, projectRoot) {
|
|
|
896
2209
|
const pagesBasePath = path.resolve(config.devServer.pagesBase);
|
|
897
2210
|
const componentsBasePath = path.resolve(config.devServer.componentsBase);
|
|
898
2211
|
const configBasePath = path.resolve(config.devServer.configBase);
|
|
899
|
-
const projectRootPath = process.cwd();
|
|
900
2212
|
const info = await scanProjectInfo(
|
|
901
2213
|
pagesBasePath,
|
|
902
2214
|
componentsBasePath,
|
|
903
2215
|
configBasePath,
|
|
904
|
-
|
|
2216
|
+
projectRoot
|
|
905
2217
|
);
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
console.log(` App Contracts: ${Object.keys(info.installedAppContracts).length}`);
|
|
2218
|
+
getLogger().info(`📋 Retrieved project info: ${info.name}`);
|
|
2219
|
+
getLogger().info(` Pages: ${info.pages.length}`);
|
|
2220
|
+
getLogger().info(` Components: ${info.components.length}`);
|
|
2221
|
+
getLogger().info(` plugins: ${info.plugins.length}`);
|
|
911
2222
|
return {
|
|
912
2223
|
type: "getProjectInfo",
|
|
913
2224
|
success: true,
|
|
914
2225
|
info
|
|
915
2226
|
};
|
|
916
2227
|
} catch (error) {
|
|
917
|
-
|
|
2228
|
+
getLogger().error("Failed to get project info:", error);
|
|
918
2229
|
return {
|
|
919
2230
|
type: "getProjectInfo",
|
|
920
2231
|
success: false,
|
|
@@ -924,18 +2235,118 @@ function createEditorHandlers(config, tsConfigPath, projectRoot) {
|
|
|
924
2235
|
localPath: process.cwd(),
|
|
925
2236
|
pages: [],
|
|
926
2237
|
components: [],
|
|
927
|
-
installedApps: [],
|
|
928
|
-
installedAppContracts: {},
|
|
929
2238
|
plugins: []
|
|
930
2239
|
}
|
|
931
2240
|
};
|
|
932
2241
|
}
|
|
933
2242
|
};
|
|
2243
|
+
const onExport = async (params) => {
|
|
2244
|
+
try {
|
|
2245
|
+
const pagesBasePath = path.resolve(config.devServer.pagesBase);
|
|
2246
|
+
const { vendorId, pageUrl, vendorDoc } = params;
|
|
2247
|
+
const dirname = pageUrlToDirectoryPath(pageUrl, pagesBasePath);
|
|
2248
|
+
const vendorFilename = `page.${vendorId}.json`;
|
|
2249
|
+
const vendorFilePath = path.join(dirname, vendorFilename);
|
|
2250
|
+
await fs.promises.mkdir(dirname, { recursive: true });
|
|
2251
|
+
await fs.promises.writeFile(
|
|
2252
|
+
vendorFilePath,
|
|
2253
|
+
JSON.stringify(vendorDoc, null, 2),
|
|
2254
|
+
"utf-8"
|
|
2255
|
+
);
|
|
2256
|
+
getLogger().info(`📦 Exported ${vendorId} document to: ${vendorFilePath}`);
|
|
2257
|
+
if (hasVendor(vendorId)) {
|
|
2258
|
+
getLogger().info(`🔄 Converting ${vendorId} document to Jay HTML...`);
|
|
2259
|
+
const vendor = getVendor(vendorId);
|
|
2260
|
+
try {
|
|
2261
|
+
const { projectPage, plugins } = await loadPageContracts(
|
|
2262
|
+
dirname,
|
|
2263
|
+
pageUrl,
|
|
2264
|
+
projectRoot
|
|
2265
|
+
);
|
|
2266
|
+
const conversionResult = await vendor.convertToBodyHtml(
|
|
2267
|
+
vendorDoc,
|
|
2268
|
+
pageUrl,
|
|
2269
|
+
projectPage,
|
|
2270
|
+
plugins
|
|
2271
|
+
);
|
|
2272
|
+
const fullJayHtml = await buildJayHtmlFromVendorResult(
|
|
2273
|
+
conversionResult,
|
|
2274
|
+
dirname,
|
|
2275
|
+
path.basename(dirname)
|
|
2276
|
+
);
|
|
2277
|
+
const jayHtmlPath = path.join(dirname, "page.jay-html");
|
|
2278
|
+
await fs.promises.writeFile(jayHtmlPath, fullJayHtml, "utf-8");
|
|
2279
|
+
getLogger().info(`✅ Successfully converted to Jay HTML: ${jayHtmlPath}`);
|
|
2280
|
+
return {
|
|
2281
|
+
type: "export",
|
|
2282
|
+
success: true,
|
|
2283
|
+
vendorSourcePath: vendorFilePath,
|
|
2284
|
+
jayHtmlPath
|
|
2285
|
+
};
|
|
2286
|
+
} catch (conversionError) {
|
|
2287
|
+
getLogger().error(`❌ Vendor conversion threw an error:`, conversionError);
|
|
2288
|
+
return {
|
|
2289
|
+
type: "export",
|
|
2290
|
+
success: false,
|
|
2291
|
+
vendorSourcePath: vendorFilePath,
|
|
2292
|
+
error: conversionError instanceof Error ? conversionError.message : "Unknown conversion error"
|
|
2293
|
+
};
|
|
2294
|
+
}
|
|
2295
|
+
} else {
|
|
2296
|
+
getLogger().info(`ℹ️ No vendor found for '${vendorId}'. Skipping conversion.`);
|
|
2297
|
+
}
|
|
2298
|
+
return {
|
|
2299
|
+
type: "export",
|
|
2300
|
+
success: true,
|
|
2301
|
+
vendorSourcePath: vendorFilePath
|
|
2302
|
+
};
|
|
2303
|
+
} catch (error) {
|
|
2304
|
+
getLogger().error("Failed to export vendor document:", error);
|
|
2305
|
+
return {
|
|
2306
|
+
type: "export",
|
|
2307
|
+
success: false,
|
|
2308
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
2309
|
+
};
|
|
2310
|
+
}
|
|
2311
|
+
};
|
|
2312
|
+
const onImport = async (params) => {
|
|
2313
|
+
try {
|
|
2314
|
+
const pagesBasePath = path.resolve(config.devServer.pagesBase);
|
|
2315
|
+
const { vendorId, pageUrl } = params;
|
|
2316
|
+
const dirname = pageUrlToDirectoryPath(pageUrl, pagesBasePath);
|
|
2317
|
+
const vendorFilename = `page.${vendorId}.json`;
|
|
2318
|
+
const vendorFilePath = path.join(dirname, vendorFilename);
|
|
2319
|
+
if (!fs.existsSync(vendorFilePath)) {
|
|
2320
|
+
return {
|
|
2321
|
+
type: "import",
|
|
2322
|
+
success: false,
|
|
2323
|
+
error: `No ${vendorId} document found at ${pageUrl}. File not found: ${vendorFilePath}`
|
|
2324
|
+
};
|
|
2325
|
+
}
|
|
2326
|
+
const fileContent = await fs.promises.readFile(vendorFilePath, "utf-8");
|
|
2327
|
+
const vendorDoc = JSON.parse(fileContent);
|
|
2328
|
+
getLogger().info(`📥 Imported ${vendorId} document from: ${vendorFilePath}`);
|
|
2329
|
+
return {
|
|
2330
|
+
type: "import",
|
|
2331
|
+
success: true,
|
|
2332
|
+
vendorDoc
|
|
2333
|
+
};
|
|
2334
|
+
} catch (error) {
|
|
2335
|
+
getLogger().error("Failed to import vendor document:", error);
|
|
2336
|
+
return {
|
|
2337
|
+
type: "import",
|
|
2338
|
+
success: false,
|
|
2339
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
2340
|
+
};
|
|
2341
|
+
}
|
|
2342
|
+
};
|
|
934
2343
|
return {
|
|
935
2344
|
onPublish,
|
|
936
2345
|
onSaveImage,
|
|
937
2346
|
onHasImage,
|
|
938
|
-
onGetProjectInfo
|
|
2347
|
+
onGetProjectInfo,
|
|
2348
|
+
onExport,
|
|
2349
|
+
onImport
|
|
939
2350
|
};
|
|
940
2351
|
}
|
|
941
2352
|
async function generatePageDefinitionFiles(routes, tsConfigPath, projectRoot) {
|
|
@@ -969,16 +2380,16 @@ async function generatePageDefinitionFiles(routes, tsConfigPath, projectRoot) {
|
|
|
969
2380
|
);
|
|
970
2381
|
const definitionFile = generateElementDefinitionFile(parsedJayHtml);
|
|
971
2382
|
if (definitionFile.validations.length > 0) {
|
|
972
|
-
|
|
2383
|
+
getLogger().warn(
|
|
973
2384
|
`failed to generate .d.ts for ${jayHtmlPath} with validation errors: ${definitionFile.validations.join("\n")}`
|
|
974
2385
|
);
|
|
975
2386
|
} else {
|
|
976
2387
|
const definitionFilePath2 = jayHtmlPath + ".d.ts";
|
|
977
2388
|
await fs.promises.writeFile(definitionFilePath2, definitionFile.val, "utf-8");
|
|
978
|
-
|
|
2389
|
+
getLogger().info(`📦 Generated definition file: ${definitionFilePath2}`);
|
|
979
2390
|
}
|
|
980
2391
|
} catch (error) {
|
|
981
|
-
|
|
2392
|
+
getLogger().error(`Failed to generate definition file for ${jayHtmlPath}: ${error}`);
|
|
982
2393
|
}
|
|
983
2394
|
}
|
|
984
2395
|
}
|
|
@@ -995,11 +2406,12 @@ async function startDevServer(options = {}) {
|
|
|
995
2406
|
};
|
|
996
2407
|
const app = express();
|
|
997
2408
|
const devServerPort = await getPort({ port: resolvedConfig.devServer.portRange });
|
|
2409
|
+
const log = getLogger();
|
|
998
2410
|
const editorServer = createEditorServer({
|
|
999
2411
|
portRange: resolvedConfig.editorServer.portRange,
|
|
1000
2412
|
editorId: resolvedConfig.editorServer.editorId,
|
|
1001
2413
|
onEditorId: (editorId2) => {
|
|
1002
|
-
|
|
2414
|
+
log.info(`Editor connected with ID: ${editorId2}`);
|
|
1003
2415
|
updateConfig({
|
|
1004
2416
|
editorServer: {
|
|
1005
2417
|
editorId: editorId2
|
|
@@ -1008,6 +2420,12 @@ async function startDevServer(options = {}) {
|
|
|
1008
2420
|
}
|
|
1009
2421
|
});
|
|
1010
2422
|
const { port: editorPort, editorId } = await editorServer.start();
|
|
2423
|
+
const registeredVendors = getRegisteredVendors();
|
|
2424
|
+
if (registeredVendors.length > 0) {
|
|
2425
|
+
log.info(
|
|
2426
|
+
`📦 Registered ${registeredVendors.length} vendor(s): ${registeredVendors.join(", ")}`
|
|
2427
|
+
);
|
|
2428
|
+
}
|
|
1011
2429
|
const handlers = createEditorHandlers(
|
|
1012
2430
|
resolvedConfig,
|
|
1013
2431
|
jayOptions.tsConfigFilePath,
|
|
@@ -1017,40 +2435,76 @@ async function startDevServer(options = {}) {
|
|
|
1017
2435
|
editorServer.onSaveImage(handlers.onSaveImage);
|
|
1018
2436
|
editorServer.onHasImage(handlers.onHasImage);
|
|
1019
2437
|
editorServer.onGetProjectInfo(handlers.onGetProjectInfo);
|
|
2438
|
+
editorServer.onExport(handlers.onExport);
|
|
2439
|
+
editorServer.onImport(handlers.onImport);
|
|
1020
2440
|
const { server, viteServer, routes } = await mkDevServer({
|
|
1021
2441
|
pagesRootFolder: path.resolve(resolvedConfig.devServer.pagesBase),
|
|
1022
2442
|
projectRootFolder: process.cwd(),
|
|
1023
2443
|
publicBaseUrlPath: "/",
|
|
1024
2444
|
dontCacheSlowly: false,
|
|
1025
|
-
jayRollupConfig: jayOptions
|
|
2445
|
+
jayRollupConfig: jayOptions,
|
|
2446
|
+
logLevel: options.logLevel
|
|
1026
2447
|
});
|
|
1027
2448
|
app.use(server);
|
|
1028
2449
|
const publicPath = path.resolve(resolvedConfig.devServer.publicFolder);
|
|
1029
2450
|
if (fs.existsSync(publicPath)) {
|
|
1030
2451
|
app.use(express.static(publicPath));
|
|
1031
2452
|
} else {
|
|
1032
|
-
|
|
2453
|
+
log.important(`⚠️ Public folder not found: ${resolvedConfig.devServer.publicFolder}`);
|
|
1033
2454
|
}
|
|
1034
2455
|
routes.forEach((route) => {
|
|
1035
2456
|
app.get(route.path, route.handler);
|
|
1036
2457
|
});
|
|
1037
2458
|
generatePageDefinitionFiles(routes, jayOptions.tsConfigFilePath, process.cwd());
|
|
1038
2459
|
const expressServer = app.listen(devServerPort, () => {
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
2460
|
+
log.important(`🚀 Jay Stack dev server started successfully!`);
|
|
2461
|
+
log.important(`📱 Dev Server: http://localhost:${devServerPort}`);
|
|
2462
|
+
log.important(`🎨 Editor Server: http://localhost:${editorPort} (ID: ${editorId})`);
|
|
2463
|
+
log.important(`📁 Pages directory: ${resolvedConfig.devServer.pagesBase}`);
|
|
1043
2464
|
if (fs.existsSync(publicPath)) {
|
|
1044
|
-
|
|
2465
|
+
log.important(`📁 Public folder: ${resolvedConfig.devServer.publicFolder}`);
|
|
2466
|
+
}
|
|
2467
|
+
if (options.testMode) {
|
|
2468
|
+
log.important(`🧪 Test Mode: enabled`);
|
|
2469
|
+
log.important(` Health: http://localhost:${devServerPort}/_jay/health`);
|
|
2470
|
+
log.important(
|
|
2471
|
+
` Shutdown: curl -X POST http://localhost:${devServerPort}/_jay/shutdown`
|
|
2472
|
+
);
|
|
2473
|
+
if (options.timeout) {
|
|
2474
|
+
log.important(` Timeout: ${options.timeout}s`);
|
|
2475
|
+
}
|
|
1045
2476
|
}
|
|
1046
2477
|
});
|
|
1047
2478
|
const shutdown = async () => {
|
|
1048
|
-
|
|
2479
|
+
log.important("\n🛑 Shutting down servers...");
|
|
1049
2480
|
await editorServer.stop();
|
|
1050
2481
|
expressServer.closeAllConnections();
|
|
1051
2482
|
await new Promise((resolve) => expressServer.close(resolve));
|
|
1052
2483
|
process.exit(0);
|
|
1053
2484
|
};
|
|
2485
|
+
if (options.testMode) {
|
|
2486
|
+
app.get("/_jay/health", (_req, res) => {
|
|
2487
|
+
res.json({
|
|
2488
|
+
status: "ready",
|
|
2489
|
+
port: devServerPort,
|
|
2490
|
+
editorPort,
|
|
2491
|
+
uptime: process.uptime()
|
|
2492
|
+
});
|
|
2493
|
+
});
|
|
2494
|
+
app.post("/_jay/shutdown", async (_req, res) => {
|
|
2495
|
+
res.json({ status: "shutting_down" });
|
|
2496
|
+
setTimeout(async () => {
|
|
2497
|
+
await shutdown();
|
|
2498
|
+
}, 100);
|
|
2499
|
+
});
|
|
2500
|
+
if (options.timeout) {
|
|
2501
|
+
setTimeout(async () => {
|
|
2502
|
+
log.important(`
|
|
2503
|
+
⏰ Timeout (${options.timeout}s) reached, shutting down...`);
|
|
2504
|
+
await shutdown();
|
|
2505
|
+
}, options.timeout * 1e3);
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
1054
2508
|
process.on("SIGTERM", shutdown);
|
|
1055
2509
|
process.on("SIGINT", shutdown);
|
|
1056
2510
|
}
|
|
@@ -1208,29 +2662,33 @@ async function validateSchema(context, result) {
|
|
|
1208
2662
|
}
|
|
1209
2663
|
}
|
|
1210
2664
|
if (manifest.dynamic_contracts) {
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
2665
|
+
const dynamicConfigs = Array.isArray(manifest.dynamic_contracts) ? manifest.dynamic_contracts : [manifest.dynamic_contracts];
|
|
2666
|
+
for (const config of dynamicConfigs) {
|
|
2667
|
+
const prefix = config.prefix || "(unknown)";
|
|
2668
|
+
if (!config.component) {
|
|
2669
|
+
result.errors.push({
|
|
2670
|
+
type: "schema",
|
|
2671
|
+
message: `dynamic_contracts[${prefix}] is missing "component" field`,
|
|
2672
|
+
location: "plugin.yaml",
|
|
2673
|
+
suggestion: "Specify path to shared component for dynamic contracts"
|
|
2674
|
+
});
|
|
2675
|
+
}
|
|
2676
|
+
if (!config.generator) {
|
|
2677
|
+
result.errors.push({
|
|
2678
|
+
type: "schema",
|
|
2679
|
+
message: `dynamic_contracts[${prefix}] is missing "generator" field`,
|
|
2680
|
+
location: "plugin.yaml",
|
|
2681
|
+
suggestion: "Specify path to generator file or export name"
|
|
2682
|
+
});
|
|
2683
|
+
}
|
|
2684
|
+
if (!config.prefix) {
|
|
2685
|
+
result.errors.push({
|
|
2686
|
+
type: "schema",
|
|
2687
|
+
message: 'dynamic_contracts entry is missing "prefix" field',
|
|
2688
|
+
location: "plugin.yaml",
|
|
2689
|
+
suggestion: 'Specify prefix for dynamic contract names (e.g., "cms")'
|
|
2690
|
+
});
|
|
2691
|
+
}
|
|
1234
2692
|
}
|
|
1235
2693
|
}
|
|
1236
2694
|
if (!manifest.contracts && !manifest.dynamic_contracts) {
|
|
@@ -1421,43 +2879,53 @@ async function validateDynamicContracts(context, result) {
|
|
|
1421
2879
|
const { dynamic_contracts } = context.manifest;
|
|
1422
2880
|
if (!dynamic_contracts)
|
|
1423
2881
|
return;
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
const
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
if (
|
|
1430
|
-
|
|
1431
|
-
|
|
2882
|
+
const dynamicConfigs = Array.isArray(dynamic_contracts) ? dynamic_contracts : [dynamic_contracts];
|
|
2883
|
+
for (const config of dynamicConfigs) {
|
|
2884
|
+
const prefix = config.prefix || "(unknown)";
|
|
2885
|
+
if (config.generator) {
|
|
2886
|
+
const isFilePath = config.generator.startsWith("./") || config.generator.startsWith("/") || config.generator.includes(".ts") || config.generator.includes(".js");
|
|
2887
|
+
if (isFilePath) {
|
|
2888
|
+
const generatorPath = path.join(context.pluginPath, config.generator);
|
|
2889
|
+
const possibleExtensions = ["", ".ts", ".js", "/index.ts", "/index.js"];
|
|
2890
|
+
let found = false;
|
|
2891
|
+
for (const ext of possibleExtensions) {
|
|
2892
|
+
if (fs.existsSync(generatorPath + ext)) {
|
|
2893
|
+
found = true;
|
|
2894
|
+
break;
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
if (!found && !context.isNpmPackage) {
|
|
2898
|
+
result.errors.push({
|
|
2899
|
+
type: "file-missing",
|
|
2900
|
+
message: `Generator file not found for ${prefix}: ${config.generator}`,
|
|
2901
|
+
location: "plugin.yaml dynamic_contracts",
|
|
2902
|
+
suggestion: `Create generator file at ${generatorPath}.ts`
|
|
2903
|
+
});
|
|
2904
|
+
}
|
|
1432
2905
|
}
|
|
1433
2906
|
}
|
|
1434
|
-
if (
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
2907
|
+
if (config.component) {
|
|
2908
|
+
const isFilePath = config.component.startsWith("./") || config.component.startsWith("/") || config.component.includes(".ts") || config.component.includes(".js");
|
|
2909
|
+
if (isFilePath) {
|
|
2910
|
+
const componentPath = path.join(context.pluginPath, config.component);
|
|
2911
|
+
const possibleExtensions = ["", ".ts", ".js", "/index.ts", "/index.js"];
|
|
2912
|
+
let found = false;
|
|
2913
|
+
for (const ext of possibleExtensions) {
|
|
2914
|
+
if (fs.existsSync(componentPath + ext)) {
|
|
2915
|
+
found = true;
|
|
2916
|
+
break;
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
if (!found && !context.isNpmPackage) {
|
|
2920
|
+
result.errors.push({
|
|
2921
|
+
type: "file-missing",
|
|
2922
|
+
message: `Dynamic contracts component not found for ${prefix}: ${config.component}`,
|
|
2923
|
+
location: "plugin.yaml dynamic_contracts",
|
|
2924
|
+
suggestion: `Create component file at ${componentPath}.ts`
|
|
2925
|
+
});
|
|
2926
|
+
}
|
|
1451
2927
|
}
|
|
1452
2928
|
}
|
|
1453
|
-
if (!found && !context.isNpmPackage) {
|
|
1454
|
-
result.errors.push({
|
|
1455
|
-
type: "file-missing",
|
|
1456
|
-
message: `Dynamic contracts component not found: ${dynamic_contracts.component}`,
|
|
1457
|
-
location: "plugin.yaml dynamic_contracts",
|
|
1458
|
-
suggestion: `Create component file at ${componentPath}.ts`
|
|
1459
|
-
});
|
|
1460
|
-
}
|
|
1461
2929
|
}
|
|
1462
2930
|
}
|
|
1463
2931
|
async function findJayFiles(dir) {
|
|
@@ -1476,9 +2944,9 @@ async function validateJayFiles(options = {}) {
|
|
|
1476
2944
|
const jayHtmlFiles = await findJayFiles(scanDir);
|
|
1477
2945
|
const contractFiles = await findContractFiles(scanDir);
|
|
1478
2946
|
if (options.verbose) {
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
2947
|
+
getLogger().info(chalk.gray(`Scanning directory: ${scanDir}`));
|
|
2948
|
+
getLogger().info(chalk.gray(`Found ${jayHtmlFiles.length} .jay-html files`));
|
|
2949
|
+
getLogger().info(chalk.gray(`Found ${contractFiles.length} .jay-contract files
|
|
1482
2950
|
`));
|
|
1483
2951
|
}
|
|
1484
2952
|
for (const contractFile of contractFiles) {
|
|
@@ -1495,10 +2963,10 @@ async function validateJayFiles(options = {}) {
|
|
|
1495
2963
|
});
|
|
1496
2964
|
}
|
|
1497
2965
|
if (options.verbose) {
|
|
1498
|
-
|
|
2966
|
+
getLogger().info(chalk.red(`❌ ${relativePath}`));
|
|
1499
2967
|
}
|
|
1500
2968
|
} else if (options.verbose) {
|
|
1501
|
-
|
|
2969
|
+
getLogger().info(chalk.green(`✓ ${relativePath}`));
|
|
1502
2970
|
}
|
|
1503
2971
|
} catch (error) {
|
|
1504
2972
|
errors.push({
|
|
@@ -1507,7 +2975,7 @@ async function validateJayFiles(options = {}) {
|
|
|
1507
2975
|
stage: "parse"
|
|
1508
2976
|
});
|
|
1509
2977
|
if (options.verbose) {
|
|
1510
|
-
|
|
2978
|
+
getLogger().info(chalk.red(`❌ ${relativePath}`));
|
|
1511
2979
|
}
|
|
1512
2980
|
}
|
|
1513
2981
|
}
|
|
@@ -1534,7 +3002,7 @@ async function validateJayFiles(options = {}) {
|
|
|
1534
3002
|
});
|
|
1535
3003
|
}
|
|
1536
3004
|
if (options.verbose) {
|
|
1537
|
-
|
|
3005
|
+
getLogger().info(chalk.red(`❌ ${relativePath}`));
|
|
1538
3006
|
}
|
|
1539
3007
|
continue;
|
|
1540
3008
|
}
|
|
@@ -1552,10 +3020,10 @@ async function validateJayFiles(options = {}) {
|
|
|
1552
3020
|
});
|
|
1553
3021
|
}
|
|
1554
3022
|
if (options.verbose) {
|
|
1555
|
-
|
|
3023
|
+
getLogger().info(chalk.red(`❌ ${relativePath}`));
|
|
1556
3024
|
}
|
|
1557
3025
|
} else if (options.verbose) {
|
|
1558
|
-
|
|
3026
|
+
getLogger().info(chalk.green(`✓ ${relativePath}`));
|
|
1559
3027
|
}
|
|
1560
3028
|
} catch (error) {
|
|
1561
3029
|
errors.push({
|
|
@@ -1564,7 +3032,7 @@ async function validateJayFiles(options = {}) {
|
|
|
1564
3032
|
stage: "parse"
|
|
1565
3033
|
});
|
|
1566
3034
|
if (options.verbose) {
|
|
1567
|
-
|
|
3035
|
+
getLogger().info(chalk.red(`❌ ${relativePath}`));
|
|
1568
3036
|
}
|
|
1569
3037
|
}
|
|
1570
3038
|
}
|
|
@@ -1577,40 +3045,357 @@ async function validateJayFiles(options = {}) {
|
|
|
1577
3045
|
};
|
|
1578
3046
|
}
|
|
1579
3047
|
function printJayValidationResult(result, options) {
|
|
3048
|
+
const logger = getLogger();
|
|
1580
3049
|
if (options.json) {
|
|
1581
|
-
|
|
3050
|
+
logger.important(JSON.stringify(result, null, 2));
|
|
1582
3051
|
return;
|
|
1583
3052
|
}
|
|
1584
|
-
|
|
3053
|
+
logger.important("");
|
|
1585
3054
|
if (result.valid) {
|
|
1586
|
-
|
|
1587
|
-
|
|
3055
|
+
logger.important(chalk.green("✅ Jay Stack validation successful!\n"));
|
|
3056
|
+
logger.important(
|
|
1588
3057
|
`Scanned ${result.jayHtmlFilesScanned} .jay-html files, ${result.contractFilesScanned} .jay-contract files`
|
|
1589
3058
|
);
|
|
1590
|
-
|
|
3059
|
+
logger.important("No errors found.");
|
|
1591
3060
|
} else {
|
|
1592
|
-
|
|
1593
|
-
|
|
3061
|
+
logger.important(chalk.red("❌ Jay Stack validation failed\n"));
|
|
3062
|
+
logger.important("Errors:");
|
|
1594
3063
|
for (const error of result.errors) {
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
3064
|
+
logger.important(chalk.red(` ❌ ${error.file}`));
|
|
3065
|
+
logger.important(chalk.gray(` ${error.message}`));
|
|
3066
|
+
logger.important("");
|
|
1598
3067
|
}
|
|
1599
3068
|
const validFiles = result.jayHtmlFilesScanned + result.contractFilesScanned - result.errors.length;
|
|
1600
|
-
|
|
3069
|
+
logger.important(
|
|
1601
3070
|
chalk.red(`${result.errors.length} error(s) found, ${validFiles} file(s) valid.`)
|
|
1602
3071
|
);
|
|
1603
3072
|
}
|
|
1604
3073
|
}
|
|
3074
|
+
async function initializeServicesForCli(projectRoot, viteServer) {
|
|
3075
|
+
const path2 = await import("node:path");
|
|
3076
|
+
const fs2 = await import("node:fs");
|
|
3077
|
+
const {
|
|
3078
|
+
runInitCallbacks,
|
|
3079
|
+
getServiceRegistry,
|
|
3080
|
+
discoverPluginsWithInit,
|
|
3081
|
+
sortPluginsByDependencies,
|
|
3082
|
+
executePluginServerInits
|
|
3083
|
+
} = await import("@jay-framework/stack-server-runtime");
|
|
3084
|
+
let initErrors = /* @__PURE__ */ new Map();
|
|
3085
|
+
try {
|
|
3086
|
+
const discoveredPlugins = await discoverPluginsWithInit({
|
|
3087
|
+
projectRoot,
|
|
3088
|
+
verbose: false
|
|
3089
|
+
});
|
|
3090
|
+
const pluginsWithInit = sortPluginsByDependencies(discoveredPlugins);
|
|
3091
|
+
try {
|
|
3092
|
+
initErrors = await executePluginServerInits(pluginsWithInit, viteServer, false);
|
|
3093
|
+
} catch (error) {
|
|
3094
|
+
getLogger().warn(chalk.yellow(`⚠️ Plugin initialization skipped: ${error.message}`));
|
|
3095
|
+
}
|
|
3096
|
+
const initPathTs = path2.join(projectRoot, "src", "init.ts");
|
|
3097
|
+
const initPathJs = path2.join(projectRoot, "src", "init.js");
|
|
3098
|
+
let initModule;
|
|
3099
|
+
if (fs2.existsSync(initPathTs) && viteServer) {
|
|
3100
|
+
initModule = await viteServer.ssrLoadModule(initPathTs);
|
|
3101
|
+
} else if (fs2.existsSync(initPathJs)) {
|
|
3102
|
+
initModule = await import(initPathJs);
|
|
3103
|
+
}
|
|
3104
|
+
if (initModule?.init?._serverInit) {
|
|
3105
|
+
await initModule.init._serverInit();
|
|
3106
|
+
}
|
|
3107
|
+
await runInitCallbacks();
|
|
3108
|
+
} catch (error) {
|
|
3109
|
+
getLogger().warn(chalk.yellow(`⚠️ Service initialization failed: ${error.message}`));
|
|
3110
|
+
getLogger().warn(chalk.gray(" Static contracts will still be listed."));
|
|
3111
|
+
}
|
|
3112
|
+
return { services: getServiceRegistry(), initErrors };
|
|
3113
|
+
}
|
|
3114
|
+
async function runAction(actionRef, options, projectRoot, initializeServices) {
|
|
3115
|
+
let viteServer;
|
|
3116
|
+
try {
|
|
3117
|
+
const slashIndex = actionRef.indexOf("/");
|
|
3118
|
+
if (slashIndex === -1) {
|
|
3119
|
+
getLogger().error(
|
|
3120
|
+
chalk.red("❌ Invalid action reference. Use format: <plugin>/<action>")
|
|
3121
|
+
);
|
|
3122
|
+
process.exit(1);
|
|
3123
|
+
}
|
|
3124
|
+
const actionExport = actionRef.substring(slashIndex + 1);
|
|
3125
|
+
const input = options.input ? JSON.parse(options.input) : {};
|
|
3126
|
+
if (options.verbose) {
|
|
3127
|
+
getLogger().info("Starting Vite for TypeScript support...");
|
|
3128
|
+
}
|
|
3129
|
+
viteServer = await createViteForCli({ projectRoot });
|
|
3130
|
+
await initializeServices(projectRoot, viteServer);
|
|
3131
|
+
const { discoverAndRegisterActions, discoverAllPluginActions, ActionRegistry } = await import("@jay-framework/stack-server-runtime");
|
|
3132
|
+
const registry = new ActionRegistry();
|
|
3133
|
+
await discoverAndRegisterActions({
|
|
3134
|
+
projectRoot,
|
|
3135
|
+
registry,
|
|
3136
|
+
verbose: options.verbose,
|
|
3137
|
+
viteServer
|
|
3138
|
+
});
|
|
3139
|
+
await discoverAllPluginActions({
|
|
3140
|
+
projectRoot,
|
|
3141
|
+
registry,
|
|
3142
|
+
verbose: options.verbose,
|
|
3143
|
+
viteServer
|
|
3144
|
+
});
|
|
3145
|
+
const allNames = registry.getNames();
|
|
3146
|
+
const matchedName = allNames.find((name) => name === actionExport) || allNames.find((name) => name.endsWith("." + actionExport)) || allNames.find((name) => name === actionRef);
|
|
3147
|
+
if (!matchedName) {
|
|
3148
|
+
getLogger().error(chalk.red(`❌ Action "${actionExport}" not found.`));
|
|
3149
|
+
if (allNames.length > 0) {
|
|
3150
|
+
getLogger().error(` Available actions: ${allNames.join(", ")}`);
|
|
3151
|
+
} else {
|
|
3152
|
+
getLogger().error(" No actions registered. Does the plugin have actions?");
|
|
3153
|
+
}
|
|
3154
|
+
process.exit(1);
|
|
3155
|
+
}
|
|
3156
|
+
if (options.verbose) {
|
|
3157
|
+
getLogger().info(`Executing action: ${matchedName}`);
|
|
3158
|
+
getLogger().info(`Input: ${JSON.stringify(input)}`);
|
|
3159
|
+
}
|
|
3160
|
+
const result = await registry.execute(matchedName, input);
|
|
3161
|
+
if (result.success) {
|
|
3162
|
+
if (options.yaml) {
|
|
3163
|
+
getLogger().important(YAML.stringify(result.data));
|
|
3164
|
+
} else {
|
|
3165
|
+
getLogger().important(JSON.stringify(result.data, null, 2));
|
|
3166
|
+
}
|
|
3167
|
+
} else {
|
|
3168
|
+
getLogger().error(
|
|
3169
|
+
chalk.red(`❌ Action failed: [${result.error.code}] ${result.error.message}`)
|
|
3170
|
+
);
|
|
3171
|
+
process.exit(1);
|
|
3172
|
+
}
|
|
3173
|
+
} catch (error) {
|
|
3174
|
+
getLogger().error(chalk.red("❌ Failed to run action:") + " " + error.message);
|
|
3175
|
+
if (options.verbose) {
|
|
3176
|
+
getLogger().error(error.stack);
|
|
3177
|
+
}
|
|
3178
|
+
process.exit(1);
|
|
3179
|
+
} finally {
|
|
3180
|
+
if (viteServer) {
|
|
3181
|
+
await viteServer.close();
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
async function runParams(contractRef, options, projectRoot, initializeServices) {
|
|
3186
|
+
let viteServer;
|
|
3187
|
+
try {
|
|
3188
|
+
const slashIndex = contractRef.indexOf("/");
|
|
3189
|
+
if (slashIndex === -1) {
|
|
3190
|
+
getLogger().error(
|
|
3191
|
+
chalk.red("❌ Invalid contract reference. Use format: <plugin>/<contract>")
|
|
3192
|
+
);
|
|
3193
|
+
process.exit(1);
|
|
3194
|
+
}
|
|
3195
|
+
const pluginName = contractRef.substring(0, slashIndex);
|
|
3196
|
+
const contractName = contractRef.substring(slashIndex + 1);
|
|
3197
|
+
if (options.verbose) {
|
|
3198
|
+
getLogger().info("Starting Vite for TypeScript support...");
|
|
3199
|
+
}
|
|
3200
|
+
viteServer = await createViteForCli({ projectRoot });
|
|
3201
|
+
await initializeServices(projectRoot, viteServer);
|
|
3202
|
+
const { resolvePluginComponent } = await import("@jay-framework/compiler-shared");
|
|
3203
|
+
const resolution = resolvePluginComponent(projectRoot, pluginName, contractName);
|
|
3204
|
+
if (resolution.validations.length > 0 || !resolution.val) {
|
|
3205
|
+
getLogger().error(
|
|
3206
|
+
chalk.red(`❌ Could not resolve plugin "${pluginName}" contract "${contractName}"`)
|
|
3207
|
+
);
|
|
3208
|
+
for (const msg of resolution.validations) {
|
|
3209
|
+
getLogger().error(` ${msg}`);
|
|
3210
|
+
}
|
|
3211
|
+
process.exit(1);
|
|
3212
|
+
}
|
|
3213
|
+
const componentPath = resolution.val.componentPath;
|
|
3214
|
+
const componentName = resolution.val.componentName;
|
|
3215
|
+
if (options.verbose) {
|
|
3216
|
+
getLogger().info(`Loading component "${componentName}" from ${componentPath}`);
|
|
3217
|
+
}
|
|
3218
|
+
const module = await viteServer.ssrLoadModule(componentPath);
|
|
3219
|
+
const component = module[componentName];
|
|
3220
|
+
if (!component || !component.loadParams) {
|
|
3221
|
+
getLogger().error(
|
|
3222
|
+
chalk.red(`❌ Component "${componentName}" does not have loadParams.`)
|
|
3223
|
+
);
|
|
3224
|
+
getLogger().error(
|
|
3225
|
+
" Only components with withLoadParams() expose discoverable URL params."
|
|
3226
|
+
);
|
|
3227
|
+
process.exit(1);
|
|
3228
|
+
}
|
|
3229
|
+
const { resolveServices } = await import("@jay-framework/stack-server-runtime");
|
|
3230
|
+
const resolvedServices = resolveServices(component.services || []);
|
|
3231
|
+
const allParams = [];
|
|
3232
|
+
const paramsGenerator = component.loadParams(resolvedServices);
|
|
3233
|
+
for await (const batch of paramsGenerator) {
|
|
3234
|
+
allParams.push(...batch);
|
|
3235
|
+
}
|
|
3236
|
+
if (options.yaml) {
|
|
3237
|
+
getLogger().important(YAML.stringify(allParams));
|
|
3238
|
+
} else {
|
|
3239
|
+
getLogger().important(JSON.stringify(allParams, null, 2));
|
|
3240
|
+
}
|
|
3241
|
+
if (!options.yaml) {
|
|
3242
|
+
getLogger().important(
|
|
3243
|
+
chalk.green(`
|
|
3244
|
+
✅ Found ${allParams.length} param combination(s)`)
|
|
3245
|
+
);
|
|
3246
|
+
}
|
|
3247
|
+
} catch (error) {
|
|
3248
|
+
getLogger().error(chalk.red("❌ Failed to discover params:") + " " + error.message);
|
|
3249
|
+
if (options.verbose) {
|
|
3250
|
+
getLogger().error(error.stack);
|
|
3251
|
+
}
|
|
3252
|
+
process.exit(1);
|
|
3253
|
+
} finally {
|
|
3254
|
+
if (viteServer) {
|
|
3255
|
+
await viteServer.close();
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
async function runSetup(pluginFilter, options, projectRoot, initializeServices) {
|
|
3260
|
+
let viteServer;
|
|
3261
|
+
try {
|
|
3262
|
+
const logger = getLogger();
|
|
3263
|
+
const path2 = await import("node:path");
|
|
3264
|
+
const jayConfig = loadConfig();
|
|
3265
|
+
const configDir = path2.resolve(projectRoot, jayConfig.devServer?.configBase || "./config");
|
|
3266
|
+
logger.important(chalk.bold("\n🔧 Setting up plugins...\n"));
|
|
3267
|
+
if (options.verbose) {
|
|
3268
|
+
logger.info("Starting Vite for TypeScript support...");
|
|
3269
|
+
}
|
|
3270
|
+
viteServer = await createViteForCli({ projectRoot });
|
|
3271
|
+
const { discoverPluginsWithSetup, executePluginSetup } = await import("@jay-framework/stack-server-runtime");
|
|
3272
|
+
const pluginsWithSetup = await discoverPluginsWithSetup({
|
|
3273
|
+
projectRoot,
|
|
3274
|
+
verbose: options.verbose,
|
|
3275
|
+
pluginFilter
|
|
3276
|
+
});
|
|
3277
|
+
if (pluginsWithSetup.length === 0) {
|
|
3278
|
+
if (pluginFilter) {
|
|
3279
|
+
logger.important(
|
|
3280
|
+
chalk.yellow(`⚠️ Plugin "${pluginFilter}" not found or has no setup handler.`)
|
|
3281
|
+
);
|
|
3282
|
+
} else {
|
|
3283
|
+
logger.important(chalk.gray("No plugins with setup handlers found."));
|
|
3284
|
+
}
|
|
3285
|
+
return;
|
|
3286
|
+
}
|
|
3287
|
+
if (options.verbose) {
|
|
3288
|
+
logger.info(
|
|
3289
|
+
`Found ${pluginsWithSetup.length} plugin(s) with setup: ${pluginsWithSetup.map((p) => p.name).join(", ")}`
|
|
3290
|
+
);
|
|
3291
|
+
}
|
|
3292
|
+
const { initErrors } = await initializeServices(projectRoot, viteServer);
|
|
3293
|
+
if (initErrors.size > 0 && options.verbose) {
|
|
3294
|
+
for (const [name, err] of initErrors) {
|
|
3295
|
+
logger.info(chalk.yellow(`⚠️ ${name} init error: ${err.message}`));
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
let configured = 0;
|
|
3299
|
+
let needsConfig = 0;
|
|
3300
|
+
let errors = 0;
|
|
3301
|
+
for (const plugin of pluginsWithSetup) {
|
|
3302
|
+
logger.important(chalk.bold(`📦 ${plugin.name}`));
|
|
3303
|
+
if (plugin.setupDescription && options.verbose) {
|
|
3304
|
+
logger.important(chalk.gray(` ${plugin.setupDescription}`));
|
|
3305
|
+
}
|
|
3306
|
+
try {
|
|
3307
|
+
const result = await executePluginSetup(plugin, {
|
|
3308
|
+
projectRoot,
|
|
3309
|
+
configDir,
|
|
3310
|
+
force: options.force ?? false,
|
|
3311
|
+
initError: initErrors.get(plugin.name),
|
|
3312
|
+
viteServer,
|
|
3313
|
+
verbose: options.verbose
|
|
3314
|
+
});
|
|
3315
|
+
switch (result.status) {
|
|
3316
|
+
case "configured":
|
|
3317
|
+
configured++;
|
|
3318
|
+
logger.important(chalk.green(" ✅ Services verified"));
|
|
3319
|
+
if (result.configCreated?.length) {
|
|
3320
|
+
for (const cfg of result.configCreated) {
|
|
3321
|
+
logger.important(chalk.green(` ✅ Created ${cfg}`));
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
if (result.message) {
|
|
3325
|
+
logger.important(chalk.gray(` ${result.message}`));
|
|
3326
|
+
}
|
|
3327
|
+
break;
|
|
3328
|
+
case "needs-config":
|
|
3329
|
+
needsConfig++;
|
|
3330
|
+
if (result.configCreated?.length) {
|
|
3331
|
+
for (const cfg of result.configCreated) {
|
|
3332
|
+
logger.important(
|
|
3333
|
+
chalk.yellow(` ⚠️ Config template created: ${cfg}`)
|
|
3334
|
+
);
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
if (result.message) {
|
|
3338
|
+
logger.important(chalk.yellow(` → ${result.message}`));
|
|
3339
|
+
} else {
|
|
3340
|
+
logger.important(
|
|
3341
|
+
chalk.yellow(
|
|
3342
|
+
` → Fill in credentials and re-run: jay-stack setup ${plugin.name}`
|
|
3343
|
+
)
|
|
3344
|
+
);
|
|
3345
|
+
}
|
|
3346
|
+
break;
|
|
3347
|
+
case "error":
|
|
3348
|
+
errors++;
|
|
3349
|
+
logger.important(chalk.red(` ❌ ${result.message || "Setup failed"}`));
|
|
3350
|
+
break;
|
|
3351
|
+
}
|
|
3352
|
+
} catch (error) {
|
|
3353
|
+
errors++;
|
|
3354
|
+
logger.important(chalk.red(` ❌ Setup failed: ${error.message}`));
|
|
3355
|
+
if (options.verbose) {
|
|
3356
|
+
logger.error(error.stack);
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
logger.important("");
|
|
3360
|
+
}
|
|
3361
|
+
const parts = [];
|
|
3362
|
+
if (configured > 0)
|
|
3363
|
+
parts.push(`${configured} configured`);
|
|
3364
|
+
if (needsConfig > 0)
|
|
3365
|
+
parts.push(`${needsConfig} needs config`);
|
|
3366
|
+
if (errors > 0)
|
|
3367
|
+
parts.push(`${errors} error(s)`);
|
|
3368
|
+
logger.important(`Setup complete: ${parts.join(", ")}`);
|
|
3369
|
+
if (errors > 0) {
|
|
3370
|
+
process.exit(1);
|
|
3371
|
+
}
|
|
3372
|
+
} catch (error) {
|
|
3373
|
+
getLogger().error(chalk.red("❌ Setup failed:") + " " + error.message);
|
|
3374
|
+
if (options.verbose) {
|
|
3375
|
+
getLogger().error(error.stack);
|
|
3376
|
+
}
|
|
3377
|
+
process.exit(1);
|
|
3378
|
+
} finally {
|
|
3379
|
+
if (viteServer) {
|
|
3380
|
+
await viteServer.close();
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
1605
3384
|
const program = new Command();
|
|
1606
3385
|
program.name("jay-stack").description("Jay Stack CLI - Development server and plugin validation").version("0.9.0");
|
|
1607
|
-
program.command("dev [path]").description("Start the Jay Stack development server").action(async (path2, options) => {
|
|
3386
|
+
program.command("dev [path]").description("Start the Jay Stack development server").option("-v, --verbose", "Enable verbose logging output").option("-q, --quiet", "Suppress all non-error output").option("--test-mode", "Enable test endpoints (/_jay/health, /_jay/shutdown)").option("--timeout <seconds>", "Auto-shutdown after N seconds (implies --test-mode)", parseInt).action(async (path2, options) => {
|
|
1608
3387
|
try {
|
|
3388
|
+
const logLevel = options.quiet ? "silent" : options.verbose ? "verbose" : "info";
|
|
3389
|
+
setDevLogger(createDevLogger(logLevel));
|
|
3390
|
+
const testMode = options.testMode || options.timeout !== void 0;
|
|
1609
3391
|
await startDevServer({
|
|
1610
|
-
projectPath: path2 || process.cwd()
|
|
3392
|
+
projectPath: path2 || process.cwd(),
|
|
3393
|
+
testMode,
|
|
3394
|
+
timeout: options.timeout,
|
|
3395
|
+
logLevel
|
|
1611
3396
|
});
|
|
1612
3397
|
} catch (error) {
|
|
1613
|
-
|
|
3398
|
+
getLogger().error(chalk.red("Error starting dev server:") + " " + error.message);
|
|
1614
3399
|
process.exit(1);
|
|
1615
3400
|
}
|
|
1616
3401
|
});
|
|
@@ -1627,9 +3412,11 @@ program.command("validate [path]").description("Validate all .jay-html and .jay-
|
|
|
1627
3412
|
}
|
|
1628
3413
|
} catch (error) {
|
|
1629
3414
|
if (options.json) {
|
|
1630
|
-
|
|
3415
|
+
getLogger().important(
|
|
3416
|
+
JSON.stringify({ valid: false, error: error.message }, null, 2)
|
|
3417
|
+
);
|
|
1631
3418
|
} else {
|
|
1632
|
-
|
|
3419
|
+
getLogger().error(chalk.red("Validation error:") + " " + error.message);
|
|
1633
3420
|
}
|
|
1634
3421
|
process.exit(1);
|
|
1635
3422
|
}
|
|
@@ -1648,64 +3435,268 @@ program.command("validate-plugin [path]").description("Validate a Jay Stack plug
|
|
|
1648
3435
|
process.exit(1);
|
|
1649
3436
|
}
|
|
1650
3437
|
} catch (error) {
|
|
1651
|
-
|
|
3438
|
+
getLogger().error(chalk.red("Validation error:") + " " + error.message);
|
|
3439
|
+
process.exit(1);
|
|
3440
|
+
}
|
|
3441
|
+
});
|
|
3442
|
+
async function ensureAgentKitDocs(projectRoot, force) {
|
|
3443
|
+
const path2 = await import("node:path");
|
|
3444
|
+
const fs2 = await import("node:fs/promises");
|
|
3445
|
+
const { fileURLToPath } = await import("node:url");
|
|
3446
|
+
const agentKitDir = path2.join(projectRoot, "agent-kit");
|
|
3447
|
+
await fs2.mkdir(agentKitDir, { recursive: true });
|
|
3448
|
+
const thisDir = path2.dirname(fileURLToPath(import.meta.url));
|
|
3449
|
+
const templateDir = path2.resolve(thisDir, "..", "agent-kit-template");
|
|
3450
|
+
let files;
|
|
3451
|
+
try {
|
|
3452
|
+
files = (await fs2.readdir(templateDir)).filter((f) => f.endsWith(".md"));
|
|
3453
|
+
} catch {
|
|
3454
|
+
getLogger().warn(chalk.yellow(" Agent-kit template folder not found: " + templateDir));
|
|
3455
|
+
return;
|
|
3456
|
+
}
|
|
3457
|
+
for (const filename of files) {
|
|
3458
|
+
const destPath = path2.join(agentKitDir, filename);
|
|
3459
|
+
if (!force) {
|
|
3460
|
+
try {
|
|
3461
|
+
await fs2.access(destPath);
|
|
3462
|
+
continue;
|
|
3463
|
+
} catch {
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
await fs2.copyFile(path2.join(templateDir, filename), destPath);
|
|
3467
|
+
getLogger().info(chalk.gray(` Created agent-kit/${filename}`));
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
async function generatePluginReferences(projectRoot, options, initErrors, viteServer) {
|
|
3471
|
+
const { discoverPluginsWithReferences, executePluginReferences } = await import("@jay-framework/stack-server-runtime");
|
|
3472
|
+
const plugins = await discoverPluginsWithReferences({
|
|
3473
|
+
projectRoot,
|
|
3474
|
+
verbose: options.verbose,
|
|
3475
|
+
pluginFilter: options.plugin
|
|
3476
|
+
});
|
|
3477
|
+
if (plugins.length === 0)
|
|
3478
|
+
return;
|
|
3479
|
+
const logger = getLogger();
|
|
3480
|
+
logger.important("");
|
|
3481
|
+
logger.important(chalk.bold("📚 Generating plugin references..."));
|
|
3482
|
+
for (const plugin of plugins) {
|
|
3483
|
+
const pluginInitError = initErrors.get(plugin.name);
|
|
3484
|
+
if (pluginInitError) {
|
|
3485
|
+
logger.warn(
|
|
3486
|
+
chalk.yellow(
|
|
3487
|
+
` ⚠️ ${plugin.name}: references skipped — init failed: ${pluginInitError.message}`
|
|
3488
|
+
)
|
|
3489
|
+
);
|
|
3490
|
+
continue;
|
|
3491
|
+
}
|
|
3492
|
+
try {
|
|
3493
|
+
const result = await executePluginReferences(plugin, {
|
|
3494
|
+
projectRoot,
|
|
3495
|
+
force: options.force ?? false,
|
|
3496
|
+
viteServer,
|
|
3497
|
+
verbose: options.verbose
|
|
3498
|
+
});
|
|
3499
|
+
if (result.referencesCreated.length > 0) {
|
|
3500
|
+
logger.important(chalk.green(` ✅ ${plugin.name}:`));
|
|
3501
|
+
for (const ref of result.referencesCreated) {
|
|
3502
|
+
logger.important(chalk.gray(` ${ref}`));
|
|
3503
|
+
}
|
|
3504
|
+
if (result.message) {
|
|
3505
|
+
logger.important(chalk.gray(` ${result.message}`));
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
} catch (error) {
|
|
3509
|
+
logger.warn(
|
|
3510
|
+
chalk.yellow(` ⚠️ ${plugin.name}: references skipped — ${error.message}`)
|
|
3511
|
+
);
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
3515
|
+
async function runMaterialize(projectRoot, options, defaultOutputRelative, keepViteAlive = false) {
|
|
3516
|
+
const path2 = await import("node:path");
|
|
3517
|
+
const outputDir = options.output ?? path2.join(projectRoot, defaultOutputRelative);
|
|
3518
|
+
let viteServer;
|
|
3519
|
+
let initErrors = /* @__PURE__ */ new Map();
|
|
3520
|
+
try {
|
|
3521
|
+
if (options.list) {
|
|
3522
|
+
const index = await listContracts({
|
|
3523
|
+
projectRoot,
|
|
3524
|
+
dynamicOnly: options.dynamicOnly,
|
|
3525
|
+
pluginFilter: options.plugin
|
|
3526
|
+
});
|
|
3527
|
+
if (options.yaml) {
|
|
3528
|
+
getLogger().important(YAML.stringify(index));
|
|
3529
|
+
} else {
|
|
3530
|
+
printContractList(index);
|
|
3531
|
+
}
|
|
3532
|
+
return { initErrors };
|
|
3533
|
+
}
|
|
3534
|
+
if (options.verbose) {
|
|
3535
|
+
getLogger().info("Starting Vite for TypeScript support...");
|
|
3536
|
+
}
|
|
3537
|
+
viteServer = await createViteForCli({ projectRoot });
|
|
3538
|
+
const { services, initErrors: errors } = await initializeServicesForCli(
|
|
3539
|
+
projectRoot,
|
|
3540
|
+
viteServer
|
|
3541
|
+
);
|
|
3542
|
+
initErrors = errors;
|
|
3543
|
+
const result = await materializeContracts(
|
|
3544
|
+
{
|
|
3545
|
+
projectRoot,
|
|
3546
|
+
outputDir,
|
|
3547
|
+
force: options.force,
|
|
3548
|
+
dynamicOnly: options.dynamicOnly,
|
|
3549
|
+
pluginFilter: options.plugin,
|
|
3550
|
+
verbose: options.verbose,
|
|
3551
|
+
viteServer
|
|
3552
|
+
},
|
|
3553
|
+
services
|
|
3554
|
+
);
|
|
3555
|
+
if (options.yaml) {
|
|
3556
|
+
getLogger().important(YAML.stringify(result.index));
|
|
3557
|
+
} else {
|
|
3558
|
+
getLogger().important(
|
|
3559
|
+
chalk.green(`
|
|
3560
|
+
✅ Materialized ${result.index.contracts.length} contracts`)
|
|
3561
|
+
);
|
|
3562
|
+
getLogger().important(` Static: ${result.staticCount}`);
|
|
3563
|
+
getLogger().important(` Dynamic: ${result.dynamicCount}`);
|
|
3564
|
+
getLogger().important(` Output: ${result.outputDir}`);
|
|
3565
|
+
}
|
|
3566
|
+
return { initErrors, viteServer: keepViteAlive ? viteServer : void 0 };
|
|
3567
|
+
} catch (error) {
|
|
3568
|
+
getLogger().error(chalk.red("❌ Failed to materialize contracts:") + " " + error.message);
|
|
3569
|
+
if (options.verbose) {
|
|
3570
|
+
getLogger().error(error.stack);
|
|
3571
|
+
}
|
|
1652
3572
|
process.exit(1);
|
|
3573
|
+
} finally {
|
|
3574
|
+
if (viteServer && !keepViteAlive) {
|
|
3575
|
+
await viteServer.close();
|
|
3576
|
+
}
|
|
1653
3577
|
}
|
|
3578
|
+
return { initErrors };
|
|
3579
|
+
}
|
|
3580
|
+
program.command("setup [plugin]").description(
|
|
3581
|
+
"Run plugin setup: create config templates, validate credentials, generate reference data"
|
|
3582
|
+
).option("--force", "Force re-run (overwrite config templates and regenerate references)").option("-v, --verbose", "Show detailed output").action(async (plugin, options) => {
|
|
3583
|
+
await runSetup(plugin, options, process.cwd(), initializeServicesForCli);
|
|
3584
|
+
});
|
|
3585
|
+
program.command("agent-kit").description(
|
|
3586
|
+
"Prepare the agent kit: materialize contracts, generate references, and write docs to agent-kit/"
|
|
3587
|
+
).option("-o, --output <dir>", "Output directory (default: agent-kit/materialized-contracts)").option("--yaml", "Output contract index as YAML to stdout").option("--list", "List contracts without writing files").option("--plugin <name>", "Filter to specific plugin").option("--dynamic-only", "Only process dynamic contracts").option("--force", "Force re-materialization").option("--no-references", "Skip reference data generation").option("-v, --verbose", "Show detailed output").action(async (options) => {
|
|
3588
|
+
const projectRoot = process.cwd();
|
|
3589
|
+
const { initErrors, viteServer } = await runMaterialize(
|
|
3590
|
+
projectRoot,
|
|
3591
|
+
options,
|
|
3592
|
+
"agent-kit/materialized-contracts",
|
|
3593
|
+
/* keepViteAlive */
|
|
3594
|
+
true
|
|
3595
|
+
);
|
|
3596
|
+
try {
|
|
3597
|
+
if (!options.list) {
|
|
3598
|
+
await ensureAgentKitDocs(projectRoot, options.force);
|
|
3599
|
+
if (options.references !== false) {
|
|
3600
|
+
await generatePluginReferences(projectRoot, options, initErrors, viteServer);
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
} finally {
|
|
3604
|
+
if (viteServer) {
|
|
3605
|
+
await viteServer.close();
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
});
|
|
3609
|
+
program.command("action <plugin/action>").description(
|
|
3610
|
+
`Run a plugin action (e.g., jay-stack action wix-stores/searchProducts --input '{"query":""}')`
|
|
3611
|
+
).option("--input <json>", "JSON input for the action (default: {})").option("--yaml", "Output result as YAML instead of JSON").option("-v, --verbose", "Show detailed output").action(async (actionRef, options) => {
|
|
3612
|
+
await runAction(actionRef, options, process.cwd(), initializeServicesForCli);
|
|
3613
|
+
});
|
|
3614
|
+
program.command("params <plugin/contract>").description(
|
|
3615
|
+
"Discover load param values for a contract (e.g., jay-stack params wix-stores/product-page)"
|
|
3616
|
+
).option("--yaml", "Output result as YAML instead of JSON").option("-v, --verbose", "Show detailed output").action(async (contractRef, options) => {
|
|
3617
|
+
await runParams(contractRef, options, process.cwd(), initializeServicesForCli);
|
|
1654
3618
|
});
|
|
1655
3619
|
program.parse(process.argv);
|
|
1656
3620
|
if (!process.argv.slice(2).length) {
|
|
1657
3621
|
program.outputHelp();
|
|
1658
3622
|
}
|
|
1659
3623
|
function printValidationResult(result, verbose) {
|
|
3624
|
+
const logger = getLogger();
|
|
1660
3625
|
if (result.valid && result.warnings.length === 0) {
|
|
1661
|
-
|
|
3626
|
+
logger.important(chalk.green("✅ Plugin validation successful!\n"));
|
|
1662
3627
|
if (verbose) {
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
3628
|
+
logger.important("Plugin: " + result.pluginName);
|
|
3629
|
+
logger.important(" ✅ plugin.yaml valid");
|
|
3630
|
+
logger.important(` ✅ ${result.contractsChecked} contracts validated`);
|
|
1666
3631
|
if (result.typesGenerated) {
|
|
1667
|
-
|
|
3632
|
+
logger.important(` ✅ ${result.typesGenerated} type definitions generated`);
|
|
1668
3633
|
}
|
|
1669
|
-
|
|
3634
|
+
logger.important(` ✅ ${result.componentsChecked} components validated`);
|
|
1670
3635
|
if (result.packageJsonChecked) {
|
|
1671
|
-
|
|
3636
|
+
logger.important(" ✅ package.json valid");
|
|
1672
3637
|
}
|
|
1673
|
-
|
|
3638
|
+
logger.important("\nNo errors found.");
|
|
1674
3639
|
}
|
|
1675
3640
|
} else if (result.valid && result.warnings.length > 0) {
|
|
1676
|
-
|
|
1677
|
-
|
|
3641
|
+
logger.important(chalk.yellow("⚠️ Plugin validation passed with warnings\n"));
|
|
3642
|
+
logger.important("Warnings:");
|
|
1678
3643
|
result.warnings.forEach((warning) => {
|
|
1679
|
-
|
|
3644
|
+
logger.important(chalk.yellow(` ⚠️ ${warning.message}`));
|
|
1680
3645
|
if (warning.location) {
|
|
1681
|
-
|
|
3646
|
+
logger.important(chalk.gray(` Location: ${warning.location}`));
|
|
1682
3647
|
}
|
|
1683
3648
|
if (warning.suggestion) {
|
|
1684
|
-
|
|
3649
|
+
logger.important(chalk.gray(` → ${warning.suggestion}`));
|
|
1685
3650
|
}
|
|
1686
|
-
|
|
3651
|
+
logger.important("");
|
|
1687
3652
|
});
|
|
1688
|
-
|
|
3653
|
+
logger.important(chalk.gray("Use --strict to treat warnings as errors."));
|
|
1689
3654
|
} else {
|
|
1690
|
-
|
|
1691
|
-
|
|
3655
|
+
logger.important(chalk.red("❌ Plugin validation failed\n"));
|
|
3656
|
+
logger.important("Errors:");
|
|
1692
3657
|
result.errors.forEach((error) => {
|
|
1693
|
-
|
|
3658
|
+
logger.important(chalk.red(` ❌ ${error.message}`));
|
|
1694
3659
|
if (error.location) {
|
|
1695
|
-
|
|
3660
|
+
logger.important(chalk.gray(` Location: ${error.location}`));
|
|
1696
3661
|
}
|
|
1697
3662
|
if (error.suggestion) {
|
|
1698
|
-
|
|
3663
|
+
logger.important(chalk.gray(` → ${error.suggestion}`));
|
|
1699
3664
|
}
|
|
1700
|
-
|
|
3665
|
+
logger.important("");
|
|
1701
3666
|
});
|
|
1702
|
-
|
|
3667
|
+
logger.important(chalk.red(`${result.errors.length} errors found.`));
|
|
3668
|
+
}
|
|
3669
|
+
}
|
|
3670
|
+
function printContractList(index) {
|
|
3671
|
+
const logger = getLogger();
|
|
3672
|
+
logger.important("\nAvailable Contracts:\n");
|
|
3673
|
+
const byPlugin = /* @__PURE__ */ new Map();
|
|
3674
|
+
for (const contract of index.contracts) {
|
|
3675
|
+
const existing = byPlugin.get(contract.plugin) || [];
|
|
3676
|
+
existing.push(contract);
|
|
3677
|
+
byPlugin.set(contract.plugin, existing);
|
|
3678
|
+
}
|
|
3679
|
+
for (const [plugin, contracts] of byPlugin) {
|
|
3680
|
+
logger.important(chalk.bold(`📦 ${plugin}`));
|
|
3681
|
+
for (const contract of contracts) {
|
|
3682
|
+
const typeIcon = contract.type === "static" ? "📄" : "⚡";
|
|
3683
|
+
logger.important(` ${typeIcon} ${contract.name}`);
|
|
3684
|
+
}
|
|
3685
|
+
logger.important("");
|
|
3686
|
+
}
|
|
3687
|
+
if (index.contracts.length === 0) {
|
|
3688
|
+
logger.important(chalk.gray("No contracts found."));
|
|
1703
3689
|
}
|
|
1704
3690
|
}
|
|
1705
3691
|
export {
|
|
1706
3692
|
createEditorHandlers,
|
|
1707
3693
|
getConfigWithDefaults,
|
|
3694
|
+
getRegisteredVendors,
|
|
3695
|
+
getVendor,
|
|
3696
|
+
hasVendor,
|
|
3697
|
+
listContracts2 as listContracts,
|
|
1708
3698
|
loadConfig,
|
|
3699
|
+
materializeContracts2 as materializeContracts,
|
|
1709
3700
|
startDevServer,
|
|
1710
3701
|
updateConfig
|
|
1711
3702
|
};
|