@jay-framework/jay-stack-cli 0.9.0 → 0.11.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 +76 -10
- package/dist/index.d.ts +10 -3
- package/dist/index.js +1375 -37
- package/package.json +10 -4
package/dist/index.js
CHANGED
|
@@ -4,16 +4,22 @@ import { mkDevServer } 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
|
-
import fs from "fs";
|
|
7
|
+
import fs, { promises } from "fs";
|
|
8
8
|
import YAML from "yaml";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { parse } from "node-html-parser";
|
|
10
|
+
import { createRequire } from "module";
|
|
11
|
+
import { parseJayFile, JAY_IMPORT_RESOLVER, generateElementDefinitionFile, parseContract, ContractTagType, generateElementFile } from "@jay-framework/compiler-jay-html";
|
|
12
|
+
import { JAY_CONTRACT_EXTENSION, JAY_EXTENSION, JayAtomicType, JayEnumType, loadPluginManifest, RuntimeMode, GenerateTarget } from "@jay-framework/compiler-shared";
|
|
13
|
+
import { Command } from "commander";
|
|
14
|
+
import chalk from "chalk";
|
|
15
|
+
import { glob } from "glob";
|
|
11
16
|
const DEFAULT_CONFIG = {
|
|
12
17
|
devServer: {
|
|
13
18
|
portRange: [3e3, 3100],
|
|
14
19
|
pagesBase: "./src/pages",
|
|
15
20
|
componentsBase: "./src/components",
|
|
16
|
-
publicFolder: "./public"
|
|
21
|
+
publicFolder: "./public",
|
|
22
|
+
configBase: "./config"
|
|
17
23
|
},
|
|
18
24
|
editorServer: {
|
|
19
25
|
portRange: [3101, 3200]
|
|
@@ -42,17 +48,18 @@ function loadConfig() {
|
|
|
42
48
|
return DEFAULT_CONFIG;
|
|
43
49
|
}
|
|
44
50
|
}
|
|
45
|
-
function getConfigWithDefaults(
|
|
51
|
+
function getConfigWithDefaults(config) {
|
|
46
52
|
return {
|
|
47
53
|
devServer: {
|
|
48
|
-
portRange:
|
|
49
|
-
pagesBase:
|
|
50
|
-
componentsBase:
|
|
51
|
-
publicFolder:
|
|
54
|
+
portRange: config.devServer?.portRange || DEFAULT_CONFIG.devServer.portRange,
|
|
55
|
+
pagesBase: config.devServer?.pagesBase || DEFAULT_CONFIG.devServer.pagesBase,
|
|
56
|
+
componentsBase: config.devServer?.componentsBase || DEFAULT_CONFIG.devServer.componentsBase,
|
|
57
|
+
publicFolder: config.devServer?.publicFolder || DEFAULT_CONFIG.devServer.publicFolder,
|
|
58
|
+
configBase: config.devServer?.configBase || DEFAULT_CONFIG.devServer.configBase
|
|
52
59
|
},
|
|
53
60
|
editorServer: {
|
|
54
|
-
portRange:
|
|
55
|
-
editorId:
|
|
61
|
+
portRange: config.editorServer?.portRange || DEFAULT_CONFIG.editorServer.portRange,
|
|
62
|
+
editorId: config.editorServer?.editorId
|
|
56
63
|
}
|
|
57
64
|
};
|
|
58
65
|
}
|
|
@@ -79,9 +86,643 @@ function updateConfig(updates) {
|
|
|
79
86
|
}
|
|
80
87
|
}
|
|
81
88
|
const PAGE_FILENAME = `page${JAY_EXTENSION}`;
|
|
82
|
-
|
|
89
|
+
const PAGE_CONTRACT_FILENAME = `page${JAY_CONTRACT_EXTENSION}`;
|
|
90
|
+
const PAGE_CONFIG_FILENAME = "page.conf.yaml";
|
|
91
|
+
function jayTypeToString(jayType) {
|
|
92
|
+
if (!jayType)
|
|
93
|
+
return void 0;
|
|
94
|
+
if (jayType instanceof JayAtomicType) {
|
|
95
|
+
return jayType.name;
|
|
96
|
+
} else if (jayType instanceof JayEnumType) {
|
|
97
|
+
return `enum (${jayType.values.join(" | ")})`;
|
|
98
|
+
} else {
|
|
99
|
+
return jayType.name || "unknown";
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function convertContractTagToProtocol(tag) {
|
|
103
|
+
const protocolTag = {
|
|
104
|
+
tag: tag.tag,
|
|
105
|
+
type: tag.type.length === 1 ? ContractTagType[tag.type[0]] : tag.type.map((t) => ContractTagType[t])
|
|
106
|
+
};
|
|
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
|
+
}
|
|
127
|
+
function isPageDirectory(entries) {
|
|
128
|
+
const hasPageHtml = entries.some((e) => e.name === PAGE_FILENAME);
|
|
129
|
+
const hasPageContract = entries.some((e) => e.name === PAGE_CONTRACT_FILENAME);
|
|
130
|
+
const hasPageConfig = entries.some((e) => e.name === PAGE_CONFIG_FILENAME);
|
|
131
|
+
const isPage = hasPageHtml || hasPageContract || hasPageConfig;
|
|
132
|
+
return { isPage, hasPageHtml, hasPageContract, hasPageConfig };
|
|
133
|
+
}
|
|
134
|
+
async function scanPageDirectories(pagesBasePath, onPageFound) {
|
|
135
|
+
async function scanDirectory(dirPath, urlPath = "") {
|
|
136
|
+
try {
|
|
137
|
+
const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
|
|
138
|
+
const { isPage, hasPageHtml, hasPageContract, hasPageConfig } = isPageDirectory(entries);
|
|
139
|
+
if (isPage) {
|
|
140
|
+
const pageUrl = urlPath || "/";
|
|
141
|
+
const pageName = dirPath === pagesBasePath ? "Home" : path.basename(dirPath);
|
|
142
|
+
await onPageFound({
|
|
143
|
+
dirPath,
|
|
144
|
+
pageUrl,
|
|
145
|
+
pageName,
|
|
146
|
+
hasPageHtml,
|
|
147
|
+
hasPageContract,
|
|
148
|
+
hasPageConfig
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
for (const entry of entries) {
|
|
152
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
153
|
+
if (entry.isDirectory()) {
|
|
154
|
+
const isParam = entry.name.startsWith("[") && entry.name.endsWith("]");
|
|
155
|
+
const segmentUrl = isParam ? `:${entry.name.slice(1, -1)}` : entry.name;
|
|
156
|
+
const newUrlPath = urlPath + "/" + segmentUrl;
|
|
157
|
+
await scanDirectory(fullPath, newUrlPath);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
} catch (error) {
|
|
161
|
+
console.warn(`Failed to scan directory ${dirPath}:`, error);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
await scanDirectory(pagesBasePath);
|
|
165
|
+
}
|
|
166
|
+
async function parseContractFile(contractFilePath) {
|
|
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) {
|
|
192
|
+
const resolvedTags = [];
|
|
193
|
+
for (const tag of tags) {
|
|
194
|
+
if (tag.link) {
|
|
195
|
+
try {
|
|
196
|
+
const linkedPath = path.resolve(baseDir, tag.link);
|
|
197
|
+
const linkedContract = await parseContractFile(linkedPath);
|
|
198
|
+
if (linkedContract) {
|
|
199
|
+
const resolvedTag = {
|
|
200
|
+
tag: tag.tag,
|
|
201
|
+
type: tag.type.length === 1 ? ContractTagType[tag.type[0]] : tag.type.map((t) => ContractTagType[t]),
|
|
202
|
+
tags: linkedContract.tags
|
|
203
|
+
// Use tags from linked contract
|
|
204
|
+
};
|
|
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
|
+
resolvedTags.push(resolvedTag);
|
|
212
|
+
} else {
|
|
213
|
+
console.warn(`Failed to load linked contract: ${tag.link} from ${baseDir}`);
|
|
214
|
+
resolvedTags.push(convertContractTagToProtocol(tag));
|
|
215
|
+
}
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.warn(`Error resolving linked contract ${tag.link}:`, error);
|
|
218
|
+
resolvedTags.push(convertContractTagToProtocol(tag));
|
|
219
|
+
}
|
|
220
|
+
} else if (tag.tags) {
|
|
221
|
+
const resolvedSubTags = await resolveLinkedTags(tag.tags, baseDir);
|
|
222
|
+
const protocolTag = convertContractTagToProtocol(tag);
|
|
223
|
+
protocolTag.tags = resolvedSubTags;
|
|
224
|
+
resolvedTags.push(protocolTag);
|
|
225
|
+
} else {
|
|
226
|
+
resolvedTags.push(convertContractTagToProtocol(tag));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return resolvedTags;
|
|
230
|
+
}
|
|
231
|
+
function resolveAppContractPath(appModule, contractFileName, projectRootPath) {
|
|
83
232
|
try {
|
|
84
|
-
const
|
|
233
|
+
const require2 = createRequire(path.join(projectRootPath, "package.json"));
|
|
234
|
+
const modulePath = `${appModule}/${contractFileName}`;
|
|
235
|
+
const resolvedPath = require2.resolve(modulePath);
|
|
236
|
+
return resolvedPath;
|
|
237
|
+
} catch (error) {
|
|
238
|
+
console.warn(
|
|
239
|
+
`Failed to resolve contract: ${appModule}/${contractFileName}`,
|
|
240
|
+
error instanceof Error ? error.message : error
|
|
241
|
+
);
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
async function scanInstalledAppContracts(configBasePath, projectRootPath) {
|
|
246
|
+
const installedAppContracts = {};
|
|
247
|
+
const installedAppsPath = path.join(configBasePath, "installedApps");
|
|
248
|
+
try {
|
|
249
|
+
if (!fs.existsSync(installedAppsPath)) {
|
|
250
|
+
return installedAppContracts;
|
|
251
|
+
}
|
|
252
|
+
const appDirs = await fs.promises.readdir(installedAppsPath, { withFileTypes: true });
|
|
253
|
+
for (const appDir of appDirs) {
|
|
254
|
+
if (appDir.isDirectory()) {
|
|
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
|
+
}
|
|
322
|
+
}
|
|
323
|
+
} catch (error) {
|
|
324
|
+
console.warn(`Failed to scan installed apps directory ${installedAppsPath}:`, error);
|
|
325
|
+
}
|
|
326
|
+
return installedAppContracts;
|
|
327
|
+
}
|
|
328
|
+
function extractHeadlessComponents(jayHtmlContent, installedApps, installedAppContracts) {
|
|
329
|
+
const root = parse(jayHtmlContent);
|
|
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
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (resolved)
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
if (resolved)
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
if (!resolved) {
|
|
393
|
+
resolvedComponents.push({
|
|
394
|
+
appName: src,
|
|
395
|
+
componentName: name,
|
|
396
|
+
key
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return resolvedComponents;
|
|
401
|
+
}
|
|
402
|
+
async function scanProjectComponents(componentsBasePath) {
|
|
403
|
+
const components = [];
|
|
404
|
+
try {
|
|
405
|
+
const entries = await fs.promises.readdir(componentsBasePath, { withFileTypes: true });
|
|
406
|
+
for (const entry of entries) {
|
|
407
|
+
if (entry.isFile() && entry.name.endsWith(JAY_EXTENSION)) {
|
|
408
|
+
const componentName = path.basename(entry.name, JAY_EXTENSION);
|
|
409
|
+
const componentPath = path.join(componentsBasePath, entry.name);
|
|
410
|
+
const contractPath = path.join(
|
|
411
|
+
componentsBasePath,
|
|
412
|
+
`${componentName}${JAY_CONTRACT_EXTENSION}`
|
|
413
|
+
);
|
|
414
|
+
const hasContract = fs.existsSync(contractPath);
|
|
415
|
+
components.push({
|
|
416
|
+
name: componentName,
|
|
417
|
+
filePath: componentPath,
|
|
418
|
+
contractPath: hasContract ? contractPath : void 0
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
} catch (error) {
|
|
423
|
+
console.warn(`Failed to scan components directory ${componentsBasePath}:`, error);
|
|
424
|
+
}
|
|
425
|
+
return components;
|
|
426
|
+
}
|
|
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
|
+
async function getProjectName(configBasePath) {
|
|
461
|
+
const projectConfigPath = path.join(configBasePath, "project.conf.yaml");
|
|
462
|
+
try {
|
|
463
|
+
if (fs.existsSync(projectConfigPath)) {
|
|
464
|
+
const configContent = await fs.promises.readFile(projectConfigPath, "utf-8");
|
|
465
|
+
const projectConfig = YAML.parse(configContent);
|
|
466
|
+
return projectConfig.name || "Unnamed Project";
|
|
467
|
+
}
|
|
468
|
+
} catch (error) {
|
|
469
|
+
console.warn(`Failed to read project config ${projectConfigPath}:`, error);
|
|
470
|
+
}
|
|
471
|
+
return "Unnamed Project";
|
|
472
|
+
}
|
|
473
|
+
async function scanPlugins(projectRootPath) {
|
|
474
|
+
const plugins = [];
|
|
475
|
+
const localPluginsPath = path.join(projectRootPath, "src/plugins");
|
|
476
|
+
if (fs.existsSync(localPluginsPath)) {
|
|
477
|
+
try {
|
|
478
|
+
const pluginDirs = await fs.promises.readdir(localPluginsPath, { withFileTypes: true });
|
|
479
|
+
for (const dir of pluginDirs) {
|
|
480
|
+
if (!dir.isDirectory())
|
|
481
|
+
continue;
|
|
482
|
+
const pluginPath = path.join(localPluginsPath, dir.name);
|
|
483
|
+
const pluginYamlPath = path.join(pluginPath, "plugin.yaml");
|
|
484
|
+
if (fs.existsSync(pluginYamlPath)) {
|
|
485
|
+
try {
|
|
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
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
} catch (error) {
|
|
501
|
+
console.warn(`Failed to scan local plugins directory ${localPluginsPath}:`, error);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
const nodeModulesPath = path.join(projectRootPath, "node_modules");
|
|
505
|
+
if (fs.existsSync(nodeModulesPath)) {
|
|
506
|
+
try {
|
|
507
|
+
const topLevelDirs = await fs.promises.readdir(nodeModulesPath, {
|
|
508
|
+
withFileTypes: true
|
|
509
|
+
});
|
|
510
|
+
for (const entry of topLevelDirs) {
|
|
511
|
+
if (!entry.isDirectory())
|
|
512
|
+
continue;
|
|
513
|
+
const packageDirs = [];
|
|
514
|
+
if (entry.name.startsWith("@")) {
|
|
515
|
+
const scopePath = path.join(nodeModulesPath, entry.name);
|
|
516
|
+
const scopedPackages = await fs.promises.readdir(scopePath, {
|
|
517
|
+
withFileTypes: true
|
|
518
|
+
});
|
|
519
|
+
for (const scopedPkg of scopedPackages) {
|
|
520
|
+
if (scopedPkg.isDirectory()) {
|
|
521
|
+
packageDirs.push(path.join(scopePath, scopedPkg.name));
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
} else {
|
|
525
|
+
packageDirs.push(path.join(nodeModulesPath, entry.name));
|
|
526
|
+
}
|
|
527
|
+
for (const pkgPath of packageDirs) {
|
|
528
|
+
const pluginYamlPath = path.join(pkgPath, "plugin.yaml");
|
|
529
|
+
if (fs.existsSync(pluginYamlPath)) {
|
|
530
|
+
try {
|
|
531
|
+
const yamlContent = await fs.promises.readFile(pluginYamlPath, "utf-8");
|
|
532
|
+
const manifest = YAML.parse(yamlContent);
|
|
533
|
+
const packageJsonPath = path.join(pkgPath, "package.json");
|
|
534
|
+
let moduleName = manifest.module;
|
|
535
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
536
|
+
const packageJson = JSON.parse(
|
|
537
|
+
await fs.promises.readFile(packageJsonPath, "utf-8")
|
|
538
|
+
);
|
|
539
|
+
moduleName = packageJson.name;
|
|
540
|
+
}
|
|
541
|
+
plugins.push({
|
|
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
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
} catch (error) {
|
|
561
|
+
console.warn(`Failed to scan node_modules for plugins:`, error);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return plugins;
|
|
565
|
+
}
|
|
566
|
+
async function scanProjectInfo(pagesBasePath, componentsBasePath, configBasePath, projectRootPath) {
|
|
567
|
+
const [projectName, components, installedApps, plugins] = await Promise.all([
|
|
568
|
+
getProjectName(configBasePath),
|
|
569
|
+
scanProjectComponents(componentsBasePath),
|
|
570
|
+
scanInstalledApps(configBasePath),
|
|
571
|
+
scanPlugins(projectRootPath)
|
|
572
|
+
]);
|
|
573
|
+
const installedAppContracts = await scanInstalledAppContracts(configBasePath, projectRootPath);
|
|
574
|
+
const pages = [];
|
|
575
|
+
await scanPageDirectories(pagesBasePath, async (context) => {
|
|
576
|
+
const { dirPath, pageUrl, pageName, hasPageHtml, hasPageContract, hasPageConfig } = context;
|
|
577
|
+
const pageFilePath = path.join(dirPath, PAGE_FILENAME);
|
|
578
|
+
const pageConfigPath = path.join(dirPath, PAGE_CONFIG_FILENAME);
|
|
579
|
+
const contractPath = path.join(dirPath, PAGE_CONTRACT_FILENAME);
|
|
580
|
+
let usedComponents = [];
|
|
581
|
+
let contractSchema;
|
|
582
|
+
if (hasPageContract) {
|
|
583
|
+
try {
|
|
584
|
+
const parsedContract = await parseContractFile(contractPath);
|
|
585
|
+
if (parsedContract) {
|
|
586
|
+
contractSchema = parsedContract;
|
|
587
|
+
}
|
|
588
|
+
} catch (error) {
|
|
589
|
+
console.warn(`Failed to parse contract file ${contractPath}:`, error);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (hasPageHtml) {
|
|
593
|
+
try {
|
|
594
|
+
const jayHtmlContent = await fs.promises.readFile(pageFilePath, "utf-8");
|
|
595
|
+
usedComponents = extractHeadlessComponents(
|
|
596
|
+
jayHtmlContent,
|
|
597
|
+
installedApps,
|
|
598
|
+
installedAppContracts
|
|
599
|
+
);
|
|
600
|
+
} catch (error) {
|
|
601
|
+
console.warn(`Failed to read page file ${pageFilePath}:`, error);
|
|
602
|
+
}
|
|
603
|
+
} else if (hasPageConfig) {
|
|
604
|
+
try {
|
|
605
|
+
const configContent = await fs.promises.readFile(pageConfigPath, "utf-8");
|
|
606
|
+
const pageConfig = YAML.parse(configContent);
|
|
607
|
+
if (pageConfig.used_components && Array.isArray(pageConfig.used_components)) {
|
|
608
|
+
for (const comp of pageConfig.used_components) {
|
|
609
|
+
const key = comp.key || "";
|
|
610
|
+
let src = "";
|
|
611
|
+
let name = "";
|
|
612
|
+
if (comp.plugin && comp.contract) {
|
|
613
|
+
const plugin = plugins.find((p) => p.manifest.name === comp.plugin);
|
|
614
|
+
if (plugin && plugin.manifest.contracts) {
|
|
615
|
+
const contract = plugin.manifest.contracts.find(
|
|
616
|
+
(c) => c.name === comp.contract
|
|
617
|
+
);
|
|
618
|
+
if (contract) {
|
|
619
|
+
usedComponents.push({
|
|
620
|
+
appName: comp.plugin,
|
|
621
|
+
componentName: comp.contract,
|
|
622
|
+
key
|
|
623
|
+
});
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
usedComponents.push({
|
|
628
|
+
appName: comp.plugin,
|
|
629
|
+
componentName: comp.contract,
|
|
630
|
+
key
|
|
631
|
+
});
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
src = comp.src || "";
|
|
635
|
+
name = comp.name || "";
|
|
636
|
+
let resolved = false;
|
|
637
|
+
for (const app of installedApps) {
|
|
638
|
+
if (app.module !== src && app.name !== src) {
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
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
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
} catch (error) {
|
|
702
|
+
console.warn(`Failed to parse page config ${pageConfigPath}:`, error);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
pages.push({
|
|
706
|
+
name: pageName,
|
|
707
|
+
url: pageUrl,
|
|
708
|
+
filePath: pageFilePath,
|
|
709
|
+
contractSchema,
|
|
710
|
+
usedComponents
|
|
711
|
+
});
|
|
712
|
+
});
|
|
713
|
+
return {
|
|
714
|
+
name: projectName,
|
|
715
|
+
localPath: projectRootPath,
|
|
716
|
+
pages,
|
|
717
|
+
components,
|
|
718
|
+
installedApps,
|
|
719
|
+
installedAppContracts,
|
|
720
|
+
plugins
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
async function handlePagePublish(resolvedConfig, page) {
|
|
724
|
+
try {
|
|
725
|
+
const pagesBasePath = path.resolve(resolvedConfig.devServer.pagesBase);
|
|
85
726
|
const routePath = page.route === "/" ? "" : page.route;
|
|
86
727
|
const dirname = path.join(pagesBasePath, routePath);
|
|
87
728
|
const fullPath = path.join(dirname, PAGE_FILENAME);
|
|
@@ -119,9 +760,9 @@ async function handlePagePublish(resolvedConfig2, page) {
|
|
|
119
760
|
];
|
|
120
761
|
}
|
|
121
762
|
}
|
|
122
|
-
async function handleComponentPublish(
|
|
763
|
+
async function handleComponentPublish(resolvedConfig, component) {
|
|
123
764
|
try {
|
|
124
|
-
const dirname = path.resolve(
|
|
765
|
+
const dirname = path.resolve(resolvedConfig.devServer.componentsBase);
|
|
125
766
|
const filename = `${component.name}${JAY_EXTENSION}`;
|
|
126
767
|
const fullPath = path.join(dirname, filename);
|
|
127
768
|
await fs.promises.mkdir(dirname, { recursive: true });
|
|
@@ -157,13 +798,13 @@ async function handleComponentPublish(resolvedConfig2, component) {
|
|
|
157
798
|
];
|
|
158
799
|
}
|
|
159
800
|
}
|
|
160
|
-
function createEditorHandlers(
|
|
801
|
+
function createEditorHandlers(config, tsConfigPath, projectRoot) {
|
|
161
802
|
const onPublish = async (params) => {
|
|
162
803
|
const status = [];
|
|
163
804
|
const createdJayHtmls = [];
|
|
164
805
|
if (params.pages) {
|
|
165
806
|
for (const page of params.pages) {
|
|
166
|
-
const [pageStatus, createdJayHtml] = await handlePagePublish(
|
|
807
|
+
const [pageStatus, createdJayHtml] = await handlePagePublish(config, page);
|
|
167
808
|
status.push(pageStatus);
|
|
168
809
|
if (pageStatus.success)
|
|
169
810
|
createdJayHtmls.push(createdJayHtml);
|
|
@@ -172,7 +813,7 @@ function createEditorHandlers(config2, tsConfigPath) {
|
|
|
172
813
|
if (params.components) {
|
|
173
814
|
for (const component of params.components) {
|
|
174
815
|
const [compStatus, createdJayHtml] = await handleComponentPublish(
|
|
175
|
-
|
|
816
|
+
config,
|
|
176
817
|
component
|
|
177
818
|
);
|
|
178
819
|
status.push(compStatus);
|
|
@@ -186,7 +827,8 @@ function createEditorHandlers(config2, tsConfigPath) {
|
|
|
186
827
|
filename,
|
|
187
828
|
dirname,
|
|
188
829
|
{ relativePath: tsConfigPath },
|
|
189
|
-
JAY_IMPORT_RESOLVER
|
|
830
|
+
JAY_IMPORT_RESOLVER,
|
|
831
|
+
projectRoot
|
|
190
832
|
);
|
|
191
833
|
const definitionFile = generateElementDefinitionFile(parsedJayHtml);
|
|
192
834
|
if (definitionFile.validations.length > 0)
|
|
@@ -204,7 +846,7 @@ function createEditorHandlers(config2, tsConfigPath) {
|
|
|
204
846
|
};
|
|
205
847
|
const onSaveImage = async (params) => {
|
|
206
848
|
try {
|
|
207
|
-
const imagesDir = path.join(path.resolve(
|
|
849
|
+
const imagesDir = path.join(path.resolve(config.devServer.publicFolder), "images");
|
|
208
850
|
await fs.promises.mkdir(imagesDir, { recursive: true });
|
|
209
851
|
const filename = `${params.imageId}.png`;
|
|
210
852
|
const imagePath = path.join(imagesDir, filename);
|
|
@@ -228,7 +870,7 @@ function createEditorHandlers(config2, tsConfigPath) {
|
|
|
228
870
|
try {
|
|
229
871
|
const filename = `${params.imageId}.png`;
|
|
230
872
|
const imagePath = path.join(
|
|
231
|
-
path.resolve(
|
|
873
|
+
path.resolve(config.devServer.publicFolder),
|
|
232
874
|
"images",
|
|
233
875
|
filename
|
|
234
876
|
);
|
|
@@ -249,13 +891,54 @@ function createEditorHandlers(config2, tsConfigPath) {
|
|
|
249
891
|
};
|
|
250
892
|
}
|
|
251
893
|
};
|
|
894
|
+
const onGetProjectInfo = async (params) => {
|
|
895
|
+
try {
|
|
896
|
+
const pagesBasePath = path.resolve(config.devServer.pagesBase);
|
|
897
|
+
const componentsBasePath = path.resolve(config.devServer.componentsBase);
|
|
898
|
+
const configBasePath = path.resolve(config.devServer.configBase);
|
|
899
|
+
const projectRootPath = process.cwd();
|
|
900
|
+
const info = await scanProjectInfo(
|
|
901
|
+
pagesBasePath,
|
|
902
|
+
componentsBasePath,
|
|
903
|
+
configBasePath,
|
|
904
|
+
projectRootPath
|
|
905
|
+
);
|
|
906
|
+
console.log(`📋 Retrieved project info: ${info.name}`);
|
|
907
|
+
console.log(` Pages: ${info.pages.length}`);
|
|
908
|
+
console.log(` Components: ${info.components.length}`);
|
|
909
|
+
console.log(` Installed Apps: ${info.installedApps.length}`);
|
|
910
|
+
console.log(` App Contracts: ${Object.keys(info.installedAppContracts).length}`);
|
|
911
|
+
return {
|
|
912
|
+
type: "getProjectInfo",
|
|
913
|
+
success: true,
|
|
914
|
+
info
|
|
915
|
+
};
|
|
916
|
+
} catch (error) {
|
|
917
|
+
console.error("Failed to get project info:", error);
|
|
918
|
+
return {
|
|
919
|
+
type: "getProjectInfo",
|
|
920
|
+
success: false,
|
|
921
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
922
|
+
info: {
|
|
923
|
+
name: "Error",
|
|
924
|
+
localPath: process.cwd(),
|
|
925
|
+
pages: [],
|
|
926
|
+
components: [],
|
|
927
|
+
installedApps: [],
|
|
928
|
+
installedAppContracts: {},
|
|
929
|
+
plugins: []
|
|
930
|
+
}
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
};
|
|
252
934
|
return {
|
|
253
935
|
onPublish,
|
|
254
936
|
onSaveImage,
|
|
255
|
-
onHasImage
|
|
937
|
+
onHasImage,
|
|
938
|
+
onGetProjectInfo
|
|
256
939
|
};
|
|
257
940
|
}
|
|
258
|
-
async function generatePageDefinitionFiles(routes, tsConfigPath) {
|
|
941
|
+
async function generatePageDefinitionFiles(routes, tsConfigPath, projectRoot) {
|
|
259
942
|
for (const route of routes) {
|
|
260
943
|
const jayHtmlPath = route.fsRoute.jayHtmlPath;
|
|
261
944
|
if (!fs.existsSync(jayHtmlPath)) {
|
|
@@ -281,7 +964,8 @@ async function generatePageDefinitionFiles(routes, tsConfigPath) {
|
|
|
281
964
|
filename,
|
|
282
965
|
dirname,
|
|
283
966
|
{ relativePath: tsConfigPath },
|
|
284
|
-
JAY_IMPORT_RESOLVER
|
|
967
|
+
JAY_IMPORT_RESOLVER,
|
|
968
|
+
projectRoot
|
|
285
969
|
);
|
|
286
970
|
const definitionFile = generateElementDefinitionFile(parsedJayHtml);
|
|
287
971
|
if (definitionFile.validations.length > 0) {
|
|
@@ -298,14 +982,18 @@ async function generatePageDefinitionFiles(routes, tsConfigPath) {
|
|
|
298
982
|
}
|
|
299
983
|
}
|
|
300
984
|
}
|
|
301
|
-
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const
|
|
308
|
-
|
|
985
|
+
async function startDevServer(options = {}) {
|
|
986
|
+
const projectPath = options.projectPath || process.cwd();
|
|
987
|
+
if (projectPath !== process.cwd()) {
|
|
988
|
+
process.chdir(projectPath);
|
|
989
|
+
}
|
|
990
|
+
const config = loadConfig();
|
|
991
|
+
const resolvedConfig = getConfigWithDefaults(config);
|
|
992
|
+
const jayOptions = {
|
|
993
|
+
tsConfigFilePath: "./tsconfig.json",
|
|
994
|
+
outputDir: "build/jay-runtime"
|
|
995
|
+
};
|
|
996
|
+
const app = express();
|
|
309
997
|
const devServerPort = await getPort({ port: resolvedConfig.devServer.portRange });
|
|
310
998
|
const editorServer = createEditorServer({
|
|
311
999
|
portRange: resolvedConfig.editorServer.portRange,
|
|
@@ -320,10 +1008,15 @@ async function initApp() {
|
|
|
320
1008
|
}
|
|
321
1009
|
});
|
|
322
1010
|
const { port: editorPort, editorId } = await editorServer.start();
|
|
323
|
-
const handlers = createEditorHandlers(
|
|
1011
|
+
const handlers = createEditorHandlers(
|
|
1012
|
+
resolvedConfig,
|
|
1013
|
+
jayOptions.tsConfigFilePath,
|
|
1014
|
+
process.cwd()
|
|
1015
|
+
);
|
|
324
1016
|
editorServer.onPublish(handlers.onPublish);
|
|
325
1017
|
editorServer.onSaveImage(handlers.onSaveImage);
|
|
326
1018
|
editorServer.onHasImage(handlers.onHasImage);
|
|
1019
|
+
editorServer.onGetProjectInfo(handlers.onGetProjectInfo);
|
|
327
1020
|
const { server, viteServer, routes } = await mkDevServer({
|
|
328
1021
|
pagesRootFolder: path.resolve(resolvedConfig.devServer.pagesBase),
|
|
329
1022
|
projectRootFolder: process.cwd(),
|
|
@@ -341,7 +1034,7 @@ async function initApp() {
|
|
|
341
1034
|
routes.forEach((route) => {
|
|
342
1035
|
app.get(route.path, route.handler);
|
|
343
1036
|
});
|
|
344
|
-
generatePageDefinitionFiles(routes, jayOptions.tsConfigFilePath);
|
|
1037
|
+
generatePageDefinitionFiles(routes, jayOptions.tsConfigFilePath, process.cwd());
|
|
345
1038
|
const expressServer = app.listen(devServerPort, () => {
|
|
346
1039
|
console.log(`🚀 Jay Stack dev server started successfully!`);
|
|
347
1040
|
console.log(`📱 Dev Server: http://localhost:${devServerPort}`);
|
|
@@ -361,13 +1054,658 @@ async function initApp() {
|
|
|
361
1054
|
process.on("SIGTERM", shutdown);
|
|
362
1055
|
process.on("SIGINT", shutdown);
|
|
363
1056
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
1057
|
+
async function validatePlugin(options = {}) {
|
|
1058
|
+
const pluginPath = options.pluginPath || process.cwd();
|
|
1059
|
+
if (options.local) {
|
|
1060
|
+
return validateLocalPlugins(pluginPath, options);
|
|
1061
|
+
} else {
|
|
1062
|
+
return validatePluginPackage(pluginPath, options);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
async function validatePluginPackage(pluginPath, options) {
|
|
1066
|
+
const result = {
|
|
1067
|
+
valid: true,
|
|
1068
|
+
errors: [],
|
|
1069
|
+
warnings: [],
|
|
1070
|
+
contractsChecked: 0,
|
|
1071
|
+
componentsChecked: 0
|
|
1072
|
+
};
|
|
1073
|
+
const pluginYamlPath = path.join(pluginPath, "plugin.yaml");
|
|
1074
|
+
const pluginManifest = loadPluginManifest(pluginPath);
|
|
1075
|
+
if (!pluginManifest) {
|
|
1076
|
+
if (!fs.existsSync(pluginYamlPath)) {
|
|
1077
|
+
result.errors.push({
|
|
1078
|
+
type: "file-missing",
|
|
1079
|
+
message: "plugin.yaml not found",
|
|
1080
|
+
location: pluginPath,
|
|
1081
|
+
suggestion: "Create a plugin.yaml file in the plugin root directory"
|
|
1082
|
+
});
|
|
1083
|
+
} else {
|
|
1084
|
+
result.errors.push({
|
|
1085
|
+
type: "schema",
|
|
1086
|
+
message: "Invalid YAML syntax or format",
|
|
1087
|
+
location: pluginYamlPath
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
result.valid = false;
|
|
1091
|
+
return result;
|
|
1092
|
+
}
|
|
1093
|
+
result.pluginName = pluginManifest.name;
|
|
1094
|
+
const context = {
|
|
1095
|
+
manifest: pluginManifest,
|
|
1096
|
+
pluginPath,
|
|
1097
|
+
isNpmPackage: fs.existsSync(path.join(pluginPath, "package.json"))
|
|
1098
|
+
};
|
|
1099
|
+
await validateSchema(context, result);
|
|
1100
|
+
if (pluginManifest.contracts) {
|
|
1101
|
+
for (let i = 0; i < pluginManifest.contracts.length; i++) {
|
|
1102
|
+
await validateContract(
|
|
1103
|
+
pluginManifest.contracts[i],
|
|
1104
|
+
i,
|
|
1105
|
+
context,
|
|
1106
|
+
options.generateTypes || false,
|
|
1107
|
+
result
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
if (pluginManifest.contracts) {
|
|
1112
|
+
for (let i = 0; i < pluginManifest.contracts.length; i++) {
|
|
1113
|
+
await validateComponent(pluginManifest.contracts[i], i, context, result);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
if (context.isNpmPackage) {
|
|
1117
|
+
await validatePackageJson(context, result);
|
|
1118
|
+
result.packageJsonChecked = true;
|
|
1119
|
+
}
|
|
1120
|
+
if (pluginManifest.dynamic_contracts) {
|
|
1121
|
+
await validateDynamicContracts(context, result);
|
|
1122
|
+
}
|
|
1123
|
+
result.valid = result.errors.length === 0;
|
|
1124
|
+
return result;
|
|
1125
|
+
}
|
|
1126
|
+
async function validateLocalPlugins(projectPath, options) {
|
|
1127
|
+
const pluginsPath = path.join(projectPath, "src/plugins");
|
|
1128
|
+
if (!fs.existsSync(pluginsPath)) {
|
|
1129
|
+
return {
|
|
1130
|
+
valid: false,
|
|
1131
|
+
errors: [
|
|
1132
|
+
{
|
|
1133
|
+
type: "file-missing",
|
|
1134
|
+
message: "src/plugins/ directory not found",
|
|
1135
|
+
location: projectPath,
|
|
1136
|
+
suggestion: "Create src/plugins/ directory for local plugins"
|
|
1137
|
+
}
|
|
1138
|
+
],
|
|
1139
|
+
warnings: []
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
const pluginDirs = fs.readdirSync(pluginsPath, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
1143
|
+
const allResults = [];
|
|
1144
|
+
for (const pluginDir of pluginDirs) {
|
|
1145
|
+
const pluginPath = path.join(pluginsPath, pluginDir.name);
|
|
1146
|
+
const result = await validatePluginPackage(pluginPath, options);
|
|
1147
|
+
allResults.push(result);
|
|
1148
|
+
}
|
|
1149
|
+
return {
|
|
1150
|
+
valid: allResults.every((r) => r.valid),
|
|
1151
|
+
errors: allResults.flatMap((r) => r.errors),
|
|
1152
|
+
warnings: allResults.flatMap((r) => r.warnings),
|
|
1153
|
+
contractsChecked: allResults.reduce((sum, r) => sum + (r.contractsChecked || 0), 0),
|
|
1154
|
+
componentsChecked: allResults.reduce((sum, r) => sum + (r.componentsChecked || 0), 0),
|
|
1155
|
+
typesGenerated: allResults.reduce((sum, r) => sum + (r.typesGenerated || 0), 0)
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
async function validateSchema(context, result) {
|
|
1159
|
+
const { manifest } = context;
|
|
1160
|
+
if (!manifest.name) {
|
|
1161
|
+
result.errors.push({
|
|
1162
|
+
type: "schema",
|
|
1163
|
+
message: "Missing required field: name",
|
|
1164
|
+
location: "plugin.yaml",
|
|
1165
|
+
suggestion: 'Add a "name" field with a kebab-case plugin name'
|
|
1166
|
+
});
|
|
1167
|
+
} else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(manifest.name)) {
|
|
1168
|
+
result.errors.push({
|
|
1169
|
+
type: "schema",
|
|
1170
|
+
message: `Invalid plugin name: "${manifest.name}". Must be kebab-case.`,
|
|
1171
|
+
location: "plugin.yaml",
|
|
1172
|
+
suggestion: 'Use lowercase letters, numbers, and hyphens only (e.g., "my-plugin")'
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
if (manifest.contracts) {
|
|
1176
|
+
if (!Array.isArray(manifest.contracts)) {
|
|
1177
|
+
result.errors.push({
|
|
1178
|
+
type: "schema",
|
|
1179
|
+
message: 'Field "contracts" must be an array',
|
|
1180
|
+
location: "plugin.yaml"
|
|
1181
|
+
});
|
|
1182
|
+
} else {
|
|
1183
|
+
manifest.contracts.forEach((contract, index) => {
|
|
1184
|
+
if (!contract.name) {
|
|
1185
|
+
result.errors.push({
|
|
1186
|
+
type: "schema",
|
|
1187
|
+
message: `Contract at index ${index} is missing "name" field`,
|
|
1188
|
+
location: "plugin.yaml"
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
if (!contract.contract) {
|
|
1192
|
+
result.errors.push({
|
|
1193
|
+
type: "schema",
|
|
1194
|
+
message: `Contract "${contract.name || index}" is missing "contract" field`,
|
|
1195
|
+
location: "plugin.yaml",
|
|
1196
|
+
suggestion: "Specify path to .jay-contract file"
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
if (!contract.component) {
|
|
1200
|
+
result.errors.push({
|
|
1201
|
+
type: "schema",
|
|
1202
|
+
message: `Contract "${contract.name || index}" is missing "component" field`,
|
|
1203
|
+
location: "plugin.yaml",
|
|
1204
|
+
suggestion: 'Specify the exported member name from the module (e.g., "moodTracker")'
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
if (manifest.dynamic_contracts) {
|
|
1211
|
+
if (!manifest.dynamic_contracts.component) {
|
|
1212
|
+
result.errors.push({
|
|
1213
|
+
type: "schema",
|
|
1214
|
+
message: 'dynamic_contracts is missing "component" field',
|
|
1215
|
+
location: "plugin.yaml",
|
|
1216
|
+
suggestion: "Specify path to shared component for dynamic contracts"
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
if (!manifest.dynamic_contracts.generator) {
|
|
1220
|
+
result.errors.push({
|
|
1221
|
+
type: "schema",
|
|
1222
|
+
message: 'dynamic_contracts is missing "generator" field',
|
|
1223
|
+
location: "plugin.yaml",
|
|
1224
|
+
suggestion: "Specify path to generator file"
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
if (!manifest.dynamic_contracts.prefix) {
|
|
1228
|
+
result.errors.push({
|
|
1229
|
+
type: "schema",
|
|
1230
|
+
message: 'dynamic_contracts is missing "prefix" field',
|
|
1231
|
+
location: "plugin.yaml",
|
|
1232
|
+
suggestion: 'Specify prefix for dynamic contract names (e.g., "cms")'
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
if (!manifest.contracts && !manifest.dynamic_contracts) {
|
|
1237
|
+
result.warnings.push({
|
|
1238
|
+
type: "schema",
|
|
1239
|
+
message: "Plugin has no contracts or dynamic_contracts defined",
|
|
1240
|
+
location: "plugin.yaml",
|
|
1241
|
+
suggestion: 'Add either "contracts" or "dynamic_contracts" to expose functionality'
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
async function validateContract(contract, index, context, generateTypes, result) {
|
|
1246
|
+
result.contractsChecked = (result.contractsChecked || 0) + 1;
|
|
1247
|
+
let contractPath;
|
|
1248
|
+
if (context.isNpmPackage) {
|
|
1249
|
+
const contractSpec = contract.contract;
|
|
1250
|
+
const possiblePaths = [
|
|
1251
|
+
path.join(context.pluginPath, "dist", contractSpec),
|
|
1252
|
+
path.join(context.pluginPath, "lib", contractSpec),
|
|
1253
|
+
path.join(context.pluginPath, contractSpec)
|
|
1254
|
+
];
|
|
1255
|
+
let found = false;
|
|
1256
|
+
for (const possiblePath of possiblePaths) {
|
|
1257
|
+
if (fs.existsSync(possiblePath)) {
|
|
1258
|
+
contractPath = possiblePath;
|
|
1259
|
+
found = true;
|
|
1260
|
+
break;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
if (!found) {
|
|
1264
|
+
result.errors.push({
|
|
1265
|
+
type: "file-missing",
|
|
1266
|
+
message: `Contract file not found: ${contractSpec}`,
|
|
1267
|
+
location: `plugin.yaml contracts[${index}]`,
|
|
1268
|
+
suggestion: `Ensure the contract file exists (looked in dist/, lib/, and root)`
|
|
1269
|
+
});
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
} else {
|
|
1273
|
+
contractPath = path.join(context.pluginPath, contract.contract);
|
|
1274
|
+
if (!fs.existsSync(contractPath)) {
|
|
1275
|
+
result.errors.push({
|
|
1276
|
+
type: "file-missing",
|
|
1277
|
+
message: `Contract file not found: ${contract.contract}`,
|
|
1278
|
+
location: `plugin.yaml contracts[${index}]`,
|
|
1279
|
+
suggestion: `Create the contract file at ${contractPath}`
|
|
1280
|
+
});
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
try {
|
|
1285
|
+
const contractContent = await fs.promises.readFile(contractPath, "utf-8");
|
|
1286
|
+
const parsedContract = YAML.parse(contractContent);
|
|
1287
|
+
if (!parsedContract.name) {
|
|
1288
|
+
result.errors.push({
|
|
1289
|
+
type: "contract-invalid",
|
|
1290
|
+
message: `Contract file ${contract.contract} is missing "name" field`,
|
|
1291
|
+
location: contractPath
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
if (!parsedContract.tags || !Array.isArray(parsedContract.tags)) {
|
|
1295
|
+
result.errors.push({
|
|
1296
|
+
type: "contract-invalid",
|
|
1297
|
+
message: `Contract file ${contract.contract} is missing "tags" array`,
|
|
1298
|
+
location: contractPath
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
} catch (error) {
|
|
1302
|
+
result.errors.push({
|
|
1303
|
+
type: "contract-invalid",
|
|
1304
|
+
message: `Invalid contract YAML: ${error.message}`,
|
|
1305
|
+
location: contractPath,
|
|
1306
|
+
suggestion: "Check YAML syntax and ensure it follows Jay contract format"
|
|
1307
|
+
});
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
if (generateTypes) {
|
|
1311
|
+
try {
|
|
1312
|
+
const { compileContractFile } = await import("@jay-framework/compiler-jay-html");
|
|
1313
|
+
const dtsPath = contractPath + ".d.ts";
|
|
1314
|
+
await compileContractFile(contractPath, dtsPath);
|
|
1315
|
+
result.typesGenerated = (result.typesGenerated || 0) + 1;
|
|
1316
|
+
} catch (error) {
|
|
1317
|
+
result.errors.push({
|
|
1318
|
+
type: "type-generation-failed",
|
|
1319
|
+
message: `Failed to generate types for ${contract.contract}: ${error.message}`,
|
|
1320
|
+
location: contractPath
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
async function validateComponent(contract, index, context, result) {
|
|
1326
|
+
result.componentsChecked = (result.componentsChecked || 0) + 1;
|
|
1327
|
+
if (typeof contract.component !== "string" || contract.component.length === 0) {
|
|
1328
|
+
result.errors.push({
|
|
1329
|
+
type: "schema",
|
|
1330
|
+
message: `Invalid component name: ${contract.component}`,
|
|
1331
|
+
location: `plugin.yaml contracts[${index}]`,
|
|
1332
|
+
suggestion: 'Component should be the exported member name (e.g., "moodTracker")'
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
if (contract.component.includes("/") || contract.component.includes(".")) {
|
|
1336
|
+
result.warnings.push({
|
|
1337
|
+
type: "schema",
|
|
1338
|
+
message: `Component "${contract.component}" looks like a path. Should it be an export name?`,
|
|
1339
|
+
location: `plugin.yaml contracts[${index}]`,
|
|
1340
|
+
suggestion: 'Component should be the exported member name (e.g., "moodTracker"), not a file path'
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
async function validatePackageJson(context, result) {
|
|
1345
|
+
const packageJsonPath = path.join(context.pluginPath, "package.json");
|
|
1346
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
1347
|
+
result.warnings.push({
|
|
1348
|
+
type: "file-missing",
|
|
1349
|
+
message: "package.json not found",
|
|
1350
|
+
location: context.pluginPath,
|
|
1351
|
+
suggestion: "Create a package.json file for NPM package distribution"
|
|
1352
|
+
});
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
try {
|
|
1356
|
+
const packageJson = JSON.parse(await fs.promises.readFile(packageJsonPath, "utf-8"));
|
|
1357
|
+
if (!packageJson.exports) {
|
|
1358
|
+
result.warnings.push({
|
|
1359
|
+
type: "export-mismatch",
|
|
1360
|
+
message: 'package.json missing "exports" field',
|
|
1361
|
+
location: packageJsonPath,
|
|
1362
|
+
suggestion: "Add exports field to define entry points for server/client builds"
|
|
1363
|
+
});
|
|
1364
|
+
} else {
|
|
1365
|
+
if (!packageJson.exports["."]) {
|
|
1366
|
+
result.warnings.push({
|
|
1367
|
+
type: "export-mismatch",
|
|
1368
|
+
message: 'package.json exports missing "." entry point',
|
|
1369
|
+
location: packageJsonPath,
|
|
1370
|
+
suggestion: 'Add "." export for the main module entry'
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
if (context.manifest.contracts) {
|
|
1374
|
+
for (const contract of context.manifest.contracts) {
|
|
1375
|
+
const contractExport = "./" + contract.contract;
|
|
1376
|
+
if (!packageJson.exports[contractExport]) {
|
|
1377
|
+
result.errors.push({
|
|
1378
|
+
type: "export-mismatch",
|
|
1379
|
+
message: `Contract "${contract.name}" not exported in package.json`,
|
|
1380
|
+
location: packageJsonPath,
|
|
1381
|
+
suggestion: `Add "${contractExport}": "./dist/${contract.contract}" to exports field`
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
if (!packageJson.exports["."]) {
|
|
1387
|
+
result.errors.push({
|
|
1388
|
+
type: "export-mismatch",
|
|
1389
|
+
message: 'NPM package missing "." export in package.json',
|
|
1390
|
+
location: packageJsonPath,
|
|
1391
|
+
suggestion: 'Add ".": "./dist/index.js" (or your main file) to exports field'
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
if (context.manifest.module) {
|
|
1395
|
+
const moduleName = context.manifest.module;
|
|
1396
|
+
result.warnings.push({
|
|
1397
|
+
type: "schema",
|
|
1398
|
+
message: 'NPM packages should omit the "module" field - the package main export will be used',
|
|
1399
|
+
location: "plugin.yaml",
|
|
1400
|
+
suggestion: 'Remove the "module" field from plugin.yaml'
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
if (!packageJson.exports || !packageJson.exports["./plugin.yaml"]) {
|
|
1405
|
+
result.errors.push({
|
|
1406
|
+
type: "export-mismatch",
|
|
1407
|
+
message: "plugin.yaml not exported in package.json (required for plugin resolution)",
|
|
1408
|
+
location: packageJsonPath,
|
|
1409
|
+
suggestion: 'Add "./plugin.yaml": "./plugin.yaml" to exports field'
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
} catch (error) {
|
|
1413
|
+
result.errors.push({
|
|
1414
|
+
type: "schema",
|
|
1415
|
+
message: `Invalid package.json: ${error.message}`,
|
|
1416
|
+
location: packageJsonPath
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
async function validateDynamicContracts(context, result) {
|
|
1421
|
+
const { dynamic_contracts } = context.manifest;
|
|
1422
|
+
if (!dynamic_contracts)
|
|
1423
|
+
return;
|
|
1424
|
+
if (dynamic_contracts.generator) {
|
|
1425
|
+
const generatorPath = path.join(context.pluginPath, dynamic_contracts.generator);
|
|
1426
|
+
const possibleExtensions = [".ts", ".js", "/index.ts", "/index.js"];
|
|
1427
|
+
let found = false;
|
|
1428
|
+
for (const ext of possibleExtensions) {
|
|
1429
|
+
if (fs.existsSync(generatorPath + ext)) {
|
|
1430
|
+
found = true;
|
|
1431
|
+
break;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
if (!found && !context.isNpmPackage) {
|
|
1435
|
+
result.errors.push({
|
|
1436
|
+
type: "file-missing",
|
|
1437
|
+
message: `Generator file not found: ${dynamic_contracts.generator}`,
|
|
1438
|
+
location: "plugin.yaml dynamic_contracts",
|
|
1439
|
+
suggestion: `Create generator file at ${generatorPath}.ts`
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
if (dynamic_contracts.component) {
|
|
1444
|
+
const componentPath = path.join(context.pluginPath, dynamic_contracts.component);
|
|
1445
|
+
const possibleExtensions = [".ts", ".js", "/index.ts", "/index.js"];
|
|
1446
|
+
let found = false;
|
|
1447
|
+
for (const ext of possibleExtensions) {
|
|
1448
|
+
if (fs.existsSync(componentPath + ext)) {
|
|
1449
|
+
found = true;
|
|
1450
|
+
break;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
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
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
async function findJayFiles(dir) {
|
|
1464
|
+
return await glob(`${dir}/**/*${JAY_EXTENSION}`);
|
|
1465
|
+
}
|
|
1466
|
+
async function findContractFiles(dir) {
|
|
1467
|
+
return await glob(`${dir}/**/*${JAY_CONTRACT_EXTENSION}`);
|
|
1468
|
+
}
|
|
1469
|
+
async function validateJayFiles(options = {}) {
|
|
1470
|
+
const config = loadConfig();
|
|
1471
|
+
const resolvedConfig = getConfigWithDefaults(config);
|
|
1472
|
+
const projectRoot = process.cwd();
|
|
1473
|
+
const scanDir = options.path ? path.resolve(options.path) : path.resolve(resolvedConfig.devServer.pagesBase);
|
|
1474
|
+
const errors = [];
|
|
1475
|
+
const warnings = [];
|
|
1476
|
+
const jayHtmlFiles = await findJayFiles(scanDir);
|
|
1477
|
+
const contractFiles = await findContractFiles(scanDir);
|
|
1478
|
+
if (options.verbose) {
|
|
1479
|
+
console.log(chalk.gray(`Scanning directory: ${scanDir}`));
|
|
1480
|
+
console.log(chalk.gray(`Found ${jayHtmlFiles.length} .jay-html files`));
|
|
1481
|
+
console.log(chalk.gray(`Found ${contractFiles.length} .jay-contract files
|
|
1482
|
+
`));
|
|
1483
|
+
}
|
|
1484
|
+
for (const contractFile of contractFiles) {
|
|
1485
|
+
const relativePath = path.relative(projectRoot, contractFile);
|
|
1486
|
+
try {
|
|
1487
|
+
const content = await promises.readFile(contractFile, "utf-8");
|
|
1488
|
+
const result = parseContract(content, path.basename(contractFile));
|
|
1489
|
+
if (result.validations.length > 0) {
|
|
1490
|
+
for (const validation of result.validations) {
|
|
1491
|
+
errors.push({
|
|
1492
|
+
file: relativePath,
|
|
1493
|
+
message: validation,
|
|
1494
|
+
stage: "parse"
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
if (options.verbose) {
|
|
1498
|
+
console.log(chalk.red(`❌ ${relativePath}`));
|
|
1499
|
+
}
|
|
1500
|
+
} else if (options.verbose) {
|
|
1501
|
+
console.log(chalk.green(`✓ ${relativePath}`));
|
|
1502
|
+
}
|
|
1503
|
+
} catch (error) {
|
|
1504
|
+
errors.push({
|
|
1505
|
+
file: relativePath,
|
|
1506
|
+
message: error.message,
|
|
1507
|
+
stage: "parse"
|
|
1508
|
+
});
|
|
1509
|
+
if (options.verbose) {
|
|
1510
|
+
console.log(chalk.red(`❌ ${relativePath}`));
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
for (const jayFile of jayHtmlFiles) {
|
|
1515
|
+
const relativePath = path.relative(projectRoot, jayFile);
|
|
1516
|
+
const filename = path.basename(jayFile.replace(JAY_EXTENSION, ""));
|
|
1517
|
+
const dirname = path.dirname(jayFile);
|
|
1518
|
+
try {
|
|
1519
|
+
const content = await promises.readFile(jayFile, "utf-8");
|
|
1520
|
+
const parsedFile = await parseJayFile(
|
|
1521
|
+
content,
|
|
1522
|
+
filename,
|
|
1523
|
+
dirname,
|
|
1524
|
+
{},
|
|
1525
|
+
JAY_IMPORT_RESOLVER,
|
|
1526
|
+
projectRoot
|
|
1527
|
+
);
|
|
1528
|
+
if (parsedFile.validations.length > 0) {
|
|
1529
|
+
for (const validation of parsedFile.validations) {
|
|
1530
|
+
errors.push({
|
|
1531
|
+
file: relativePath,
|
|
1532
|
+
message: validation,
|
|
1533
|
+
stage: "parse"
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
if (options.verbose) {
|
|
1537
|
+
console.log(chalk.red(`❌ ${relativePath}`));
|
|
1538
|
+
}
|
|
1539
|
+
continue;
|
|
1540
|
+
}
|
|
1541
|
+
const generatedFile = generateElementFile(
|
|
1542
|
+
parsedFile.val,
|
|
1543
|
+
RuntimeMode.MainTrusted,
|
|
1544
|
+
GenerateTarget.jay
|
|
1545
|
+
);
|
|
1546
|
+
if (generatedFile.validations.length > 0) {
|
|
1547
|
+
for (const validation of generatedFile.validations) {
|
|
1548
|
+
errors.push({
|
|
1549
|
+
file: relativePath,
|
|
1550
|
+
message: validation,
|
|
1551
|
+
stage: "generate"
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1554
|
+
if (options.verbose) {
|
|
1555
|
+
console.log(chalk.red(`❌ ${relativePath}`));
|
|
1556
|
+
}
|
|
1557
|
+
} else if (options.verbose) {
|
|
1558
|
+
console.log(chalk.green(`✓ ${relativePath}`));
|
|
1559
|
+
}
|
|
1560
|
+
} catch (error) {
|
|
1561
|
+
errors.push({
|
|
1562
|
+
file: relativePath,
|
|
1563
|
+
message: error.message,
|
|
1564
|
+
stage: "parse"
|
|
1565
|
+
});
|
|
1566
|
+
if (options.verbose) {
|
|
1567
|
+
console.log(chalk.red(`❌ ${relativePath}`));
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
return {
|
|
1572
|
+
valid: errors.length === 0,
|
|
1573
|
+
jayHtmlFilesScanned: jayHtmlFiles.length,
|
|
1574
|
+
contractFilesScanned: contractFiles.length,
|
|
1575
|
+
errors,
|
|
1576
|
+
warnings
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
function printJayValidationResult(result, options) {
|
|
1580
|
+
if (options.json) {
|
|
1581
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
console.log("");
|
|
1585
|
+
if (result.valid) {
|
|
1586
|
+
console.log(chalk.green("✅ Jay Stack validation successful!\n"));
|
|
1587
|
+
console.log(
|
|
1588
|
+
`Scanned ${result.jayHtmlFilesScanned} .jay-html files, ${result.contractFilesScanned} .jay-contract files`
|
|
1589
|
+
);
|
|
1590
|
+
console.log("No errors found.");
|
|
1591
|
+
} else {
|
|
1592
|
+
console.log(chalk.red("❌ Jay Stack validation failed\n"));
|
|
1593
|
+
console.log("Errors:");
|
|
1594
|
+
for (const error of result.errors) {
|
|
1595
|
+
console.log(chalk.red(` ❌ ${error.file}`));
|
|
1596
|
+
console.log(chalk.gray(` ${error.message}`));
|
|
1597
|
+
console.log("");
|
|
1598
|
+
}
|
|
1599
|
+
const validFiles = result.jayHtmlFilesScanned + result.contractFilesScanned - result.errors.length;
|
|
1600
|
+
console.log(
|
|
1601
|
+
chalk.red(`${result.errors.length} error(s) found, ${validFiles} file(s) valid.`)
|
|
1602
|
+
);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
const program = new Command();
|
|
1606
|
+
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) => {
|
|
1608
|
+
try {
|
|
1609
|
+
await startDevServer({
|
|
1610
|
+
projectPath: path2 || process.cwd()
|
|
1611
|
+
});
|
|
1612
|
+
} catch (error) {
|
|
1613
|
+
console.error(chalk.red("Error starting dev server:"), error.message);
|
|
1614
|
+
process.exit(1);
|
|
1615
|
+
}
|
|
1616
|
+
});
|
|
1617
|
+
program.command("validate [path]").description("Validate all .jay-html and .jay-contract files in the project").option("-v, --verbose", "Show per-file validation status").option("--json", "Output results as JSON").action(async (scanPath, options) => {
|
|
1618
|
+
try {
|
|
1619
|
+
const result = await validateJayFiles({
|
|
1620
|
+
path: scanPath,
|
|
1621
|
+
verbose: options.verbose,
|
|
1622
|
+
json: options.json
|
|
1623
|
+
});
|
|
1624
|
+
printJayValidationResult(result, options);
|
|
1625
|
+
if (!result.valid) {
|
|
1626
|
+
process.exit(1);
|
|
1627
|
+
}
|
|
1628
|
+
} catch (error) {
|
|
1629
|
+
if (options.json) {
|
|
1630
|
+
console.log(JSON.stringify({ valid: false, error: error.message }, null, 2));
|
|
1631
|
+
} else {
|
|
1632
|
+
console.error(chalk.red("Validation error:"), error.message);
|
|
1633
|
+
}
|
|
1634
|
+
process.exit(1);
|
|
1635
|
+
}
|
|
367
1636
|
});
|
|
1637
|
+
program.command("validate-plugin [path]").description("Validate a Jay Stack plugin package").option("--local", "Validate local plugins in src/plugins/").option("-v, --verbose", "Show detailed validation output").option("--strict", "Treat warnings as errors (for CI)").option("--generate-types", "Generate .d.ts files for contracts").action(async (pluginPath, options) => {
|
|
1638
|
+
try {
|
|
1639
|
+
const result = await validatePlugin({
|
|
1640
|
+
pluginPath: pluginPath || process.cwd(),
|
|
1641
|
+
local: options.local,
|
|
1642
|
+
verbose: options.verbose,
|
|
1643
|
+
strict: options.strict,
|
|
1644
|
+
generateTypes: options.generateTypes
|
|
1645
|
+
});
|
|
1646
|
+
printValidationResult(result, options.verbose);
|
|
1647
|
+
if (!result.valid || options.strict && result.warnings.length > 0) {
|
|
1648
|
+
process.exit(1);
|
|
1649
|
+
}
|
|
1650
|
+
} catch (error) {
|
|
1651
|
+
console.error(chalk.red("Validation error:"), error.message);
|
|
1652
|
+
process.exit(1);
|
|
1653
|
+
}
|
|
1654
|
+
});
|
|
1655
|
+
program.parse(process.argv);
|
|
1656
|
+
if (!process.argv.slice(2).length) {
|
|
1657
|
+
program.outputHelp();
|
|
1658
|
+
}
|
|
1659
|
+
function printValidationResult(result, verbose) {
|
|
1660
|
+
if (result.valid && result.warnings.length === 0) {
|
|
1661
|
+
console.log(chalk.green("✅ Plugin validation successful!\n"));
|
|
1662
|
+
if (verbose) {
|
|
1663
|
+
console.log("Plugin:", result.pluginName);
|
|
1664
|
+
console.log(" ✅ plugin.yaml valid");
|
|
1665
|
+
console.log(` ✅ ${result.contractsChecked} contracts validated`);
|
|
1666
|
+
if (result.typesGenerated) {
|
|
1667
|
+
console.log(` ✅ ${result.typesGenerated} type definitions generated`);
|
|
1668
|
+
}
|
|
1669
|
+
console.log(` ✅ ${result.componentsChecked} components validated`);
|
|
1670
|
+
if (result.packageJsonChecked) {
|
|
1671
|
+
console.log(" ✅ package.json valid");
|
|
1672
|
+
}
|
|
1673
|
+
console.log("\nNo errors found.");
|
|
1674
|
+
}
|
|
1675
|
+
} else if (result.valid && result.warnings.length > 0) {
|
|
1676
|
+
console.log(chalk.yellow("⚠️ Plugin validation passed with warnings\n"));
|
|
1677
|
+
console.log("Warnings:");
|
|
1678
|
+
result.warnings.forEach((warning) => {
|
|
1679
|
+
console.log(chalk.yellow(` ⚠️ ${warning.message}`));
|
|
1680
|
+
if (warning.location) {
|
|
1681
|
+
console.log(chalk.gray(` Location: ${warning.location}`));
|
|
1682
|
+
}
|
|
1683
|
+
if (warning.suggestion) {
|
|
1684
|
+
console.log(chalk.gray(` → ${warning.suggestion}`));
|
|
1685
|
+
}
|
|
1686
|
+
console.log();
|
|
1687
|
+
});
|
|
1688
|
+
console.log(chalk.gray("Use --strict to treat warnings as errors."));
|
|
1689
|
+
} else {
|
|
1690
|
+
console.log(chalk.red("❌ Plugin validation failed\n"));
|
|
1691
|
+
console.log("Errors:");
|
|
1692
|
+
result.errors.forEach((error) => {
|
|
1693
|
+
console.log(chalk.red(` ❌ ${error.message}`));
|
|
1694
|
+
if (error.location) {
|
|
1695
|
+
console.log(chalk.gray(` Location: ${error.location}`));
|
|
1696
|
+
}
|
|
1697
|
+
if (error.suggestion) {
|
|
1698
|
+
console.log(chalk.gray(` → ${error.suggestion}`));
|
|
1699
|
+
}
|
|
1700
|
+
console.log();
|
|
1701
|
+
});
|
|
1702
|
+
console.log(chalk.red(`${result.errors.length} errors found.`));
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
368
1705
|
export {
|
|
369
1706
|
createEditorHandlers,
|
|
370
1707
|
getConfigWithDefaults,
|
|
371
1708
|
loadConfig,
|
|
1709
|
+
startDevServer,
|
|
372
1710
|
updateConfig
|
|
373
1711
|
};
|