@jay-framework/jay-stack-cli 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +10 -3
- package/dist/index.js +1211 -36
- package/package.json +7 -4
package/dist/index.js
CHANGED
|
@@ -6,14 +6,19 @@ import getPort from "get-port";
|
|
|
6
6
|
import path from "path";
|
|
7
7
|
import fs 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 } from "@jay-framework/compiler-jay-html";
|
|
12
|
+
import { JAY_CONTRACT_EXTENSION, JAY_EXTENSION, JayAtomicType, JayEnumType, loadPluginManifest } from "@jay-framework/compiler-shared";
|
|
13
|
+
import { Command } from "commander";
|
|
14
|
+
import chalk from "chalk";
|
|
11
15
|
const DEFAULT_CONFIG = {
|
|
12
16
|
devServer: {
|
|
13
17
|
portRange: [3e3, 3100],
|
|
14
18
|
pagesBase: "./src/pages",
|
|
15
19
|
componentsBase: "./src/components",
|
|
16
|
-
publicFolder: "./public"
|
|
20
|
+
publicFolder: "./public",
|
|
21
|
+
configBase: "./config"
|
|
17
22
|
},
|
|
18
23
|
editorServer: {
|
|
19
24
|
portRange: [3101, 3200]
|
|
@@ -42,17 +47,18 @@ function loadConfig() {
|
|
|
42
47
|
return DEFAULT_CONFIG;
|
|
43
48
|
}
|
|
44
49
|
}
|
|
45
|
-
function getConfigWithDefaults(
|
|
50
|
+
function getConfigWithDefaults(config) {
|
|
46
51
|
return {
|
|
47
52
|
devServer: {
|
|
48
|
-
portRange:
|
|
49
|
-
pagesBase:
|
|
50
|
-
componentsBase:
|
|
51
|
-
publicFolder:
|
|
53
|
+
portRange: config.devServer?.portRange || DEFAULT_CONFIG.devServer.portRange,
|
|
54
|
+
pagesBase: config.devServer?.pagesBase || DEFAULT_CONFIG.devServer.pagesBase,
|
|
55
|
+
componentsBase: config.devServer?.componentsBase || DEFAULT_CONFIG.devServer.componentsBase,
|
|
56
|
+
publicFolder: config.devServer?.publicFolder || DEFAULT_CONFIG.devServer.publicFolder,
|
|
57
|
+
configBase: config.devServer?.configBase || DEFAULT_CONFIG.devServer.configBase
|
|
52
58
|
},
|
|
53
59
|
editorServer: {
|
|
54
|
-
portRange:
|
|
55
|
-
editorId:
|
|
60
|
+
portRange: config.editorServer?.portRange || DEFAULT_CONFIG.editorServer.portRange,
|
|
61
|
+
editorId: config.editorServer?.editorId
|
|
56
62
|
}
|
|
57
63
|
};
|
|
58
64
|
}
|
|
@@ -79,9 +85,643 @@ function updateConfig(updates) {
|
|
|
79
85
|
}
|
|
80
86
|
}
|
|
81
87
|
const PAGE_FILENAME = `page${JAY_EXTENSION}`;
|
|
82
|
-
|
|
88
|
+
const PAGE_CONTRACT_FILENAME = `page${JAY_CONTRACT_EXTENSION}`;
|
|
89
|
+
const PAGE_CONFIG_FILENAME = "page.conf.yaml";
|
|
90
|
+
function jayTypeToString(jayType) {
|
|
91
|
+
if (!jayType)
|
|
92
|
+
return void 0;
|
|
93
|
+
if (jayType instanceof JayAtomicType) {
|
|
94
|
+
return jayType.name;
|
|
95
|
+
} else if (jayType instanceof JayEnumType) {
|
|
96
|
+
return `enum (${jayType.values.join(" | ")})`;
|
|
97
|
+
} else {
|
|
98
|
+
return jayType.name || "unknown";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function convertContractTagToProtocol(tag) {
|
|
102
|
+
const protocolTag = {
|
|
103
|
+
tag: tag.tag,
|
|
104
|
+
type: tag.type.length === 1 ? ContractTagType[tag.type[0]] : tag.type.map((t) => ContractTagType[t])
|
|
105
|
+
};
|
|
106
|
+
if (tag.dataType) {
|
|
107
|
+
protocolTag.dataType = jayTypeToString(tag.dataType);
|
|
108
|
+
}
|
|
109
|
+
if (tag.elementType) {
|
|
110
|
+
protocolTag.elementType = tag.elementType.join(" | ");
|
|
111
|
+
}
|
|
112
|
+
if (tag.required !== void 0) {
|
|
113
|
+
protocolTag.required = tag.required;
|
|
114
|
+
}
|
|
115
|
+
if (tag.repeated !== void 0) {
|
|
116
|
+
protocolTag.repeated = tag.repeated;
|
|
117
|
+
}
|
|
118
|
+
if (tag.link) {
|
|
119
|
+
protocolTag.link = tag.link;
|
|
120
|
+
}
|
|
121
|
+
if (tag.tags) {
|
|
122
|
+
protocolTag.tags = tag.tags.map(convertContractTagToProtocol);
|
|
123
|
+
}
|
|
124
|
+
return protocolTag;
|
|
125
|
+
}
|
|
126
|
+
function isPageDirectory(entries) {
|
|
127
|
+
const hasPageHtml = entries.some((e) => e.name === PAGE_FILENAME);
|
|
128
|
+
const hasPageContract = entries.some((e) => e.name === PAGE_CONTRACT_FILENAME);
|
|
129
|
+
const hasPageConfig = entries.some((e) => e.name === PAGE_CONFIG_FILENAME);
|
|
130
|
+
const isPage = hasPageHtml || hasPageContract || hasPageConfig;
|
|
131
|
+
return { isPage, hasPageHtml, hasPageContract, hasPageConfig };
|
|
132
|
+
}
|
|
133
|
+
async function scanPageDirectories(pagesBasePath, onPageFound) {
|
|
134
|
+
async function scanDirectory(dirPath, urlPath = "") {
|
|
135
|
+
try {
|
|
136
|
+
const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
|
|
137
|
+
const { isPage, hasPageHtml, hasPageContract, hasPageConfig } = isPageDirectory(entries);
|
|
138
|
+
if (isPage) {
|
|
139
|
+
const pageUrl = urlPath || "/";
|
|
140
|
+
const pageName = dirPath === pagesBasePath ? "Home" : path.basename(dirPath);
|
|
141
|
+
await onPageFound({
|
|
142
|
+
dirPath,
|
|
143
|
+
pageUrl,
|
|
144
|
+
pageName,
|
|
145
|
+
hasPageHtml,
|
|
146
|
+
hasPageContract,
|
|
147
|
+
hasPageConfig
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
for (const entry of entries) {
|
|
151
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
152
|
+
if (entry.isDirectory()) {
|
|
153
|
+
const isParam = entry.name.startsWith("[") && entry.name.endsWith("]");
|
|
154
|
+
const segmentUrl = isParam ? `:${entry.name.slice(1, -1)}` : entry.name;
|
|
155
|
+
const newUrlPath = urlPath + "/" + segmentUrl;
|
|
156
|
+
await scanDirectory(fullPath, newUrlPath);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.warn(`Failed to scan directory ${dirPath}:`, error);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
await scanDirectory(pagesBasePath);
|
|
164
|
+
}
|
|
165
|
+
async function parseContractFile(contractFilePath) {
|
|
166
|
+
try {
|
|
167
|
+
const contractYaml = await fs.promises.readFile(contractFilePath, "utf-8");
|
|
168
|
+
const parsedContract = parseContract(contractYaml, contractFilePath);
|
|
169
|
+
if (parsedContract.validations.length > 0) {
|
|
170
|
+
console.warn(
|
|
171
|
+
`Contract validation errors in ${contractFilePath}:`,
|
|
172
|
+
parsedContract.validations
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
if (parsedContract.val) {
|
|
176
|
+
const resolvedTags = await resolveLinkedTags(
|
|
177
|
+
parsedContract.val.tags,
|
|
178
|
+
path.dirname(contractFilePath)
|
|
179
|
+
);
|
|
180
|
+
return {
|
|
181
|
+
name: parsedContract.val.name,
|
|
182
|
+
tags: resolvedTags
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
} catch (error) {
|
|
186
|
+
console.warn(`Failed to parse contract file ${contractFilePath}:`, error);
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
async function resolveLinkedTags(tags, baseDir) {
|
|
191
|
+
const resolvedTags = [];
|
|
192
|
+
for (const tag of tags) {
|
|
193
|
+
if (tag.link) {
|
|
194
|
+
try {
|
|
195
|
+
const linkedPath = path.resolve(baseDir, tag.link);
|
|
196
|
+
const linkedContract = await parseContractFile(linkedPath);
|
|
197
|
+
if (linkedContract) {
|
|
198
|
+
const resolvedTag = {
|
|
199
|
+
tag: tag.tag,
|
|
200
|
+
type: tag.type.length === 1 ? ContractTagType[tag.type[0]] : tag.type.map((t) => ContractTagType[t]),
|
|
201
|
+
tags: linkedContract.tags
|
|
202
|
+
// Use tags from linked contract
|
|
203
|
+
};
|
|
204
|
+
if (tag.required !== void 0) {
|
|
205
|
+
resolvedTag.required = tag.required;
|
|
206
|
+
}
|
|
207
|
+
if (tag.repeated !== void 0) {
|
|
208
|
+
resolvedTag.repeated = tag.repeated;
|
|
209
|
+
}
|
|
210
|
+
resolvedTags.push(resolvedTag);
|
|
211
|
+
} else {
|
|
212
|
+
console.warn(`Failed to load linked contract: ${tag.link} from ${baseDir}`);
|
|
213
|
+
resolvedTags.push(convertContractTagToProtocol(tag));
|
|
214
|
+
}
|
|
215
|
+
} catch (error) {
|
|
216
|
+
console.warn(`Error resolving linked contract ${tag.link}:`, error);
|
|
217
|
+
resolvedTags.push(convertContractTagToProtocol(tag));
|
|
218
|
+
}
|
|
219
|
+
} else if (tag.tags) {
|
|
220
|
+
const resolvedSubTags = await resolveLinkedTags(tag.tags, baseDir);
|
|
221
|
+
const protocolTag = convertContractTagToProtocol(tag);
|
|
222
|
+
protocolTag.tags = resolvedSubTags;
|
|
223
|
+
resolvedTags.push(protocolTag);
|
|
224
|
+
} else {
|
|
225
|
+
resolvedTags.push(convertContractTagToProtocol(tag));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return resolvedTags;
|
|
229
|
+
}
|
|
230
|
+
function resolveAppContractPath(appModule, contractFileName, projectRootPath) {
|
|
231
|
+
try {
|
|
232
|
+
const require2 = createRequire(path.join(projectRootPath, "package.json"));
|
|
233
|
+
const modulePath = `${appModule}/${contractFileName}`;
|
|
234
|
+
const resolvedPath = require2.resolve(modulePath);
|
|
235
|
+
return resolvedPath;
|
|
236
|
+
} catch (error) {
|
|
237
|
+
console.warn(
|
|
238
|
+
`Failed to resolve contract: ${appModule}/${contractFileName}`,
|
|
239
|
+
error instanceof Error ? error.message : error
|
|
240
|
+
);
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
async function scanInstalledAppContracts(configBasePath, projectRootPath) {
|
|
245
|
+
const installedAppContracts = {};
|
|
246
|
+
const installedAppsPath = path.join(configBasePath, "installedApps");
|
|
247
|
+
try {
|
|
248
|
+
if (!fs.existsSync(installedAppsPath)) {
|
|
249
|
+
return installedAppContracts;
|
|
250
|
+
}
|
|
251
|
+
const appDirs = await fs.promises.readdir(installedAppsPath, { withFileTypes: true });
|
|
252
|
+
for (const appDir of appDirs) {
|
|
253
|
+
if (appDir.isDirectory()) {
|
|
254
|
+
const appConfigPath = path.join(installedAppsPath, appDir.name, "app.conf.yaml");
|
|
255
|
+
try {
|
|
256
|
+
if (fs.existsSync(appConfigPath)) {
|
|
257
|
+
const configContent = await fs.promises.readFile(appConfigPath, "utf-8");
|
|
258
|
+
const appConfig = YAML.parse(configContent);
|
|
259
|
+
const appName = appConfig.name || appDir.name;
|
|
260
|
+
const appModule = appConfig.module || appDir.name;
|
|
261
|
+
const appContracts = {
|
|
262
|
+
appName,
|
|
263
|
+
module: appModule,
|
|
264
|
+
pages: [],
|
|
265
|
+
components: []
|
|
266
|
+
};
|
|
267
|
+
if (appConfig.pages && Array.isArray(appConfig.pages)) {
|
|
268
|
+
for (const page of appConfig.pages) {
|
|
269
|
+
if (page.headless_components && Array.isArray(page.headless_components)) {
|
|
270
|
+
for (const component of page.headless_components) {
|
|
271
|
+
if (component.contract) {
|
|
272
|
+
const contractPath = resolveAppContractPath(
|
|
273
|
+
appModule,
|
|
274
|
+
component.contract,
|
|
275
|
+
projectRootPath
|
|
276
|
+
);
|
|
277
|
+
if (contractPath) {
|
|
278
|
+
const contractSchema = await parseContractFile(contractPath);
|
|
279
|
+
if (contractSchema) {
|
|
280
|
+
appContracts.pages.push({
|
|
281
|
+
pageName: page.name,
|
|
282
|
+
contractSchema
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (appConfig.components && Array.isArray(appConfig.components)) {
|
|
292
|
+
for (const component of appConfig.components) {
|
|
293
|
+
if (component.headless_components && Array.isArray(component.headless_components)) {
|
|
294
|
+
for (const headlessComp of component.headless_components) {
|
|
295
|
+
if (headlessComp.contract) {
|
|
296
|
+
const contractPath = resolveAppContractPath(
|
|
297
|
+
appModule,
|
|
298
|
+
headlessComp.contract,
|
|
299
|
+
projectRootPath
|
|
300
|
+
);
|
|
301
|
+
if (contractPath) {
|
|
302
|
+
const contractSchema = await parseContractFile(contractPath);
|
|
303
|
+
if (contractSchema) {
|
|
304
|
+
appContracts.components.push({
|
|
305
|
+
componentName: component.name,
|
|
306
|
+
contractSchema
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
installedAppContracts[appName] = appContracts;
|
|
316
|
+
}
|
|
317
|
+
} catch (error) {
|
|
318
|
+
console.warn(`Failed to parse app config ${appConfigPath}:`, error);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
} catch (error) {
|
|
323
|
+
console.warn(`Failed to scan installed apps directory ${installedAppsPath}:`, error);
|
|
324
|
+
}
|
|
325
|
+
return installedAppContracts;
|
|
326
|
+
}
|
|
327
|
+
function extractHeadlessComponents(jayHtmlContent, installedApps, installedAppContracts) {
|
|
328
|
+
const root = parse(jayHtmlContent);
|
|
329
|
+
const headlessScripts = root.querySelectorAll('script[type="application/jay-headless"]');
|
|
330
|
+
const resolvedComponents = [];
|
|
331
|
+
for (const script of headlessScripts) {
|
|
332
|
+
const src = script.getAttribute("src") || "";
|
|
333
|
+
const name = script.getAttribute("name") || "";
|
|
334
|
+
const key = script.getAttribute("key") || "";
|
|
335
|
+
let resolved = false;
|
|
336
|
+
for (const app of installedApps) {
|
|
337
|
+
if (app.module !== src && app.name !== src) {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
for (const appPage of app.pages) {
|
|
341
|
+
for (const headlessComp of appPage.headless_components) {
|
|
342
|
+
if (headlessComp.name === name && headlessComp.key === key) {
|
|
343
|
+
const appContracts = installedAppContracts[app.name];
|
|
344
|
+
if (appContracts) {
|
|
345
|
+
const matchingPageContract = appContracts.pages.find(
|
|
346
|
+
(pc) => pc.pageName === appPage.name
|
|
347
|
+
);
|
|
348
|
+
if (matchingPageContract) {
|
|
349
|
+
resolvedComponents.push({
|
|
350
|
+
appName: app.name,
|
|
351
|
+
componentName: appPage.name,
|
|
352
|
+
key
|
|
353
|
+
});
|
|
354
|
+
resolved = true;
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (resolved)
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
if (resolved)
|
|
364
|
+
break;
|
|
365
|
+
for (const appComponent of app.components) {
|
|
366
|
+
for (const headlessComp of appComponent.headless_components) {
|
|
367
|
+
if (headlessComp.name === name && headlessComp.key === key) {
|
|
368
|
+
const appContracts = installedAppContracts[app.name];
|
|
369
|
+
if (appContracts) {
|
|
370
|
+
const matchingComponentContract = appContracts.components.find(
|
|
371
|
+
(cc) => cc.componentName === appComponent.name
|
|
372
|
+
);
|
|
373
|
+
if (matchingComponentContract) {
|
|
374
|
+
resolvedComponents.push({
|
|
375
|
+
appName: app.name,
|
|
376
|
+
componentName: appComponent.name,
|
|
377
|
+
key
|
|
378
|
+
});
|
|
379
|
+
resolved = true;
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (resolved)
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
if (resolved)
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
if (!resolved) {
|
|
392
|
+
resolvedComponents.push({
|
|
393
|
+
appName: src,
|
|
394
|
+
componentName: name,
|
|
395
|
+
key
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return resolvedComponents;
|
|
400
|
+
}
|
|
401
|
+
async function scanProjectComponents(componentsBasePath) {
|
|
402
|
+
const components = [];
|
|
83
403
|
try {
|
|
84
|
-
const
|
|
404
|
+
const entries = await fs.promises.readdir(componentsBasePath, { withFileTypes: true });
|
|
405
|
+
for (const entry of entries) {
|
|
406
|
+
if (entry.isFile() && entry.name.endsWith(JAY_EXTENSION)) {
|
|
407
|
+
const componentName = path.basename(entry.name, JAY_EXTENSION);
|
|
408
|
+
const componentPath = path.join(componentsBasePath, entry.name);
|
|
409
|
+
const contractPath = path.join(
|
|
410
|
+
componentsBasePath,
|
|
411
|
+
`${componentName}${JAY_CONTRACT_EXTENSION}`
|
|
412
|
+
);
|
|
413
|
+
const hasContract = fs.existsSync(contractPath);
|
|
414
|
+
components.push({
|
|
415
|
+
name: componentName,
|
|
416
|
+
filePath: componentPath,
|
|
417
|
+
contractPath: hasContract ? contractPath : void 0
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
} catch (error) {
|
|
422
|
+
console.warn(`Failed to scan components directory ${componentsBasePath}:`, error);
|
|
423
|
+
}
|
|
424
|
+
return components;
|
|
425
|
+
}
|
|
426
|
+
async function scanInstalledApps(configBasePath) {
|
|
427
|
+
const installedApps = [];
|
|
428
|
+
const installedAppsPath = path.join(configBasePath, "installedApps");
|
|
429
|
+
try {
|
|
430
|
+
if (!fs.existsSync(installedAppsPath)) {
|
|
431
|
+
return installedApps;
|
|
432
|
+
}
|
|
433
|
+
const appDirs = await fs.promises.readdir(installedAppsPath, { withFileTypes: true });
|
|
434
|
+
for (const appDir of appDirs) {
|
|
435
|
+
if (appDir.isDirectory()) {
|
|
436
|
+
const appConfigPath = path.join(installedAppsPath, appDir.name, "app.conf.yaml");
|
|
437
|
+
try {
|
|
438
|
+
if (fs.existsSync(appConfigPath)) {
|
|
439
|
+
const configContent = await fs.promises.readFile(appConfigPath, "utf-8");
|
|
440
|
+
const appConfig = YAML.parse(configContent);
|
|
441
|
+
installedApps.push({
|
|
442
|
+
name: appConfig.name || appDir.name,
|
|
443
|
+
module: appConfig.module || appDir.name,
|
|
444
|
+
pages: appConfig.pages || [],
|
|
445
|
+
components: appConfig.components || [],
|
|
446
|
+
config_map: appConfig.config_map || []
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
} catch (error) {
|
|
450
|
+
console.warn(`Failed to parse app config ${appConfigPath}:`, error);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
} catch (error) {
|
|
455
|
+
console.warn(`Failed to scan installed apps directory ${installedAppsPath}:`, error);
|
|
456
|
+
}
|
|
457
|
+
return installedApps;
|
|
458
|
+
}
|
|
459
|
+
async function getProjectName(configBasePath) {
|
|
460
|
+
const projectConfigPath = path.join(configBasePath, "project.conf.yaml");
|
|
461
|
+
try {
|
|
462
|
+
if (fs.existsSync(projectConfigPath)) {
|
|
463
|
+
const configContent = await fs.promises.readFile(projectConfigPath, "utf-8");
|
|
464
|
+
const projectConfig = YAML.parse(configContent);
|
|
465
|
+
return projectConfig.name || "Unnamed Project";
|
|
466
|
+
}
|
|
467
|
+
} catch (error) {
|
|
468
|
+
console.warn(`Failed to read project config ${projectConfigPath}:`, error);
|
|
469
|
+
}
|
|
470
|
+
return "Unnamed Project";
|
|
471
|
+
}
|
|
472
|
+
async function scanPlugins(projectRootPath) {
|
|
473
|
+
const plugins = [];
|
|
474
|
+
const localPluginsPath = path.join(projectRootPath, "src/plugins");
|
|
475
|
+
if (fs.existsSync(localPluginsPath)) {
|
|
476
|
+
try {
|
|
477
|
+
const pluginDirs = await fs.promises.readdir(localPluginsPath, { withFileTypes: true });
|
|
478
|
+
for (const dir of pluginDirs) {
|
|
479
|
+
if (!dir.isDirectory())
|
|
480
|
+
continue;
|
|
481
|
+
const pluginPath = path.join(localPluginsPath, dir.name);
|
|
482
|
+
const pluginYamlPath = path.join(pluginPath, "plugin.yaml");
|
|
483
|
+
if (fs.existsSync(pluginYamlPath)) {
|
|
484
|
+
try {
|
|
485
|
+
const yamlContent = await fs.promises.readFile(pluginYamlPath, "utf-8");
|
|
486
|
+
const manifest = YAML.parse(yamlContent);
|
|
487
|
+
plugins.push({
|
|
488
|
+
manifest,
|
|
489
|
+
location: {
|
|
490
|
+
type: "local",
|
|
491
|
+
path: pluginPath
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
} catch (error) {
|
|
495
|
+
console.warn(`Failed to parse plugin.yaml for ${dir.name}:`, error);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
} catch (error) {
|
|
500
|
+
console.warn(`Failed to scan local plugins directory ${localPluginsPath}:`, error);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
const nodeModulesPath = path.join(projectRootPath, "node_modules");
|
|
504
|
+
if (fs.existsSync(nodeModulesPath)) {
|
|
505
|
+
try {
|
|
506
|
+
const topLevelDirs = await fs.promises.readdir(nodeModulesPath, {
|
|
507
|
+
withFileTypes: true
|
|
508
|
+
});
|
|
509
|
+
for (const entry of topLevelDirs) {
|
|
510
|
+
if (!entry.isDirectory())
|
|
511
|
+
continue;
|
|
512
|
+
const packageDirs = [];
|
|
513
|
+
if (entry.name.startsWith("@")) {
|
|
514
|
+
const scopePath = path.join(nodeModulesPath, entry.name);
|
|
515
|
+
const scopedPackages = await fs.promises.readdir(scopePath, {
|
|
516
|
+
withFileTypes: true
|
|
517
|
+
});
|
|
518
|
+
for (const scopedPkg of scopedPackages) {
|
|
519
|
+
if (scopedPkg.isDirectory()) {
|
|
520
|
+
packageDirs.push(path.join(scopePath, scopedPkg.name));
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
} else {
|
|
524
|
+
packageDirs.push(path.join(nodeModulesPath, entry.name));
|
|
525
|
+
}
|
|
526
|
+
for (const pkgPath of packageDirs) {
|
|
527
|
+
const pluginYamlPath = path.join(pkgPath, "plugin.yaml");
|
|
528
|
+
if (fs.existsSync(pluginYamlPath)) {
|
|
529
|
+
try {
|
|
530
|
+
const yamlContent = await fs.promises.readFile(pluginYamlPath, "utf-8");
|
|
531
|
+
const manifest = YAML.parse(yamlContent);
|
|
532
|
+
const packageJsonPath = path.join(pkgPath, "package.json");
|
|
533
|
+
let moduleName = manifest.module;
|
|
534
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
535
|
+
const packageJson = JSON.parse(
|
|
536
|
+
await fs.promises.readFile(packageJsonPath, "utf-8")
|
|
537
|
+
);
|
|
538
|
+
moduleName = packageJson.name;
|
|
539
|
+
}
|
|
540
|
+
plugins.push({
|
|
541
|
+
manifest: {
|
|
542
|
+
...manifest,
|
|
543
|
+
module: moduleName
|
|
544
|
+
},
|
|
545
|
+
location: {
|
|
546
|
+
type: "npm",
|
|
547
|
+
module: moduleName || manifest.name
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
} catch (error) {
|
|
551
|
+
console.warn(
|
|
552
|
+
`Failed to parse plugin.yaml for package ${pkgPath}:`,
|
|
553
|
+
error
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
} catch (error) {
|
|
560
|
+
console.warn(`Failed to scan node_modules for plugins:`, error);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return plugins;
|
|
564
|
+
}
|
|
565
|
+
async function scanProjectInfo(pagesBasePath, componentsBasePath, configBasePath, projectRootPath) {
|
|
566
|
+
const [projectName, components, installedApps, plugins] = await Promise.all([
|
|
567
|
+
getProjectName(configBasePath),
|
|
568
|
+
scanProjectComponents(componentsBasePath),
|
|
569
|
+
scanInstalledApps(configBasePath),
|
|
570
|
+
scanPlugins(projectRootPath)
|
|
571
|
+
]);
|
|
572
|
+
const installedAppContracts = await scanInstalledAppContracts(configBasePath, projectRootPath);
|
|
573
|
+
const pages = [];
|
|
574
|
+
await scanPageDirectories(pagesBasePath, async (context) => {
|
|
575
|
+
const { dirPath, pageUrl, pageName, hasPageHtml, hasPageContract, hasPageConfig } = context;
|
|
576
|
+
const pageFilePath = path.join(dirPath, PAGE_FILENAME);
|
|
577
|
+
const pageConfigPath = path.join(dirPath, PAGE_CONFIG_FILENAME);
|
|
578
|
+
const contractPath = path.join(dirPath, PAGE_CONTRACT_FILENAME);
|
|
579
|
+
let usedComponents = [];
|
|
580
|
+
let contractSchema;
|
|
581
|
+
if (hasPageContract) {
|
|
582
|
+
try {
|
|
583
|
+
const parsedContract = await parseContractFile(contractPath);
|
|
584
|
+
if (parsedContract) {
|
|
585
|
+
contractSchema = parsedContract;
|
|
586
|
+
}
|
|
587
|
+
} catch (error) {
|
|
588
|
+
console.warn(`Failed to parse contract file ${contractPath}:`, error);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
if (hasPageHtml) {
|
|
592
|
+
try {
|
|
593
|
+
const jayHtmlContent = await fs.promises.readFile(pageFilePath, "utf-8");
|
|
594
|
+
usedComponents = extractHeadlessComponents(
|
|
595
|
+
jayHtmlContent,
|
|
596
|
+
installedApps,
|
|
597
|
+
installedAppContracts
|
|
598
|
+
);
|
|
599
|
+
} catch (error) {
|
|
600
|
+
console.warn(`Failed to read page file ${pageFilePath}:`, error);
|
|
601
|
+
}
|
|
602
|
+
} else if (hasPageConfig) {
|
|
603
|
+
try {
|
|
604
|
+
const configContent = await fs.promises.readFile(pageConfigPath, "utf-8");
|
|
605
|
+
const pageConfig = YAML.parse(configContent);
|
|
606
|
+
if (pageConfig.used_components && Array.isArray(pageConfig.used_components)) {
|
|
607
|
+
for (const comp of pageConfig.used_components) {
|
|
608
|
+
const key = comp.key || "";
|
|
609
|
+
let src = "";
|
|
610
|
+
let name = "";
|
|
611
|
+
if (comp.plugin && comp.contract) {
|
|
612
|
+
const plugin = plugins.find((p) => p.manifest.name === comp.plugin);
|
|
613
|
+
if (plugin && plugin.manifest.contracts) {
|
|
614
|
+
const contract = plugin.manifest.contracts.find(
|
|
615
|
+
(c) => c.name === comp.contract
|
|
616
|
+
);
|
|
617
|
+
if (contract) {
|
|
618
|
+
usedComponents.push({
|
|
619
|
+
appName: comp.plugin,
|
|
620
|
+
componentName: comp.contract,
|
|
621
|
+
key
|
|
622
|
+
});
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
usedComponents.push({
|
|
627
|
+
appName: comp.plugin,
|
|
628
|
+
componentName: comp.contract,
|
|
629
|
+
key
|
|
630
|
+
});
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
src = comp.src || "";
|
|
634
|
+
name = comp.name || "";
|
|
635
|
+
let resolved = false;
|
|
636
|
+
for (const app of installedApps) {
|
|
637
|
+
if (app.module !== src && app.name !== src) {
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
for (const appPage of app.pages) {
|
|
641
|
+
for (const headlessComp of appPage.headless_components) {
|
|
642
|
+
if (headlessComp.name === name && headlessComp.key === key) {
|
|
643
|
+
const appContracts = installedAppContracts[app.name];
|
|
644
|
+
if (appContracts) {
|
|
645
|
+
const matchingPageContract = appContracts.pages.find(
|
|
646
|
+
(pc) => pc.pageName === appPage.name
|
|
647
|
+
);
|
|
648
|
+
if (matchingPageContract) {
|
|
649
|
+
usedComponents.push({
|
|
650
|
+
appName: app.name,
|
|
651
|
+
componentName: appPage.name,
|
|
652
|
+
key
|
|
653
|
+
});
|
|
654
|
+
resolved = true;
|
|
655
|
+
break;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
if (resolved)
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
663
|
+
if (resolved)
|
|
664
|
+
break;
|
|
665
|
+
for (const appComponent of app.components) {
|
|
666
|
+
for (const headlessComp of appComponent.headless_components) {
|
|
667
|
+
if (headlessComp.name === name && headlessComp.key === key) {
|
|
668
|
+
const appContracts = installedAppContracts[app.name];
|
|
669
|
+
if (appContracts) {
|
|
670
|
+
const matchingComponentContract = appContracts.components.find(
|
|
671
|
+
(cc) => cc.componentName === appComponent.name
|
|
672
|
+
);
|
|
673
|
+
if (matchingComponentContract) {
|
|
674
|
+
usedComponents.push({
|
|
675
|
+
appName: app.name,
|
|
676
|
+
componentName: appComponent.name,
|
|
677
|
+
key
|
|
678
|
+
});
|
|
679
|
+
resolved = true;
|
|
680
|
+
break;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
if (resolved)
|
|
686
|
+
break;
|
|
687
|
+
}
|
|
688
|
+
if (resolved)
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
691
|
+
if (!resolved) {
|
|
692
|
+
usedComponents.push({
|
|
693
|
+
appName: src,
|
|
694
|
+
componentName: name,
|
|
695
|
+
key
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
} catch (error) {
|
|
701
|
+
console.warn(`Failed to parse page config ${pageConfigPath}:`, error);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
pages.push({
|
|
705
|
+
name: pageName,
|
|
706
|
+
url: pageUrl,
|
|
707
|
+
filePath: pageFilePath,
|
|
708
|
+
contractSchema,
|
|
709
|
+
usedComponents
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
return {
|
|
713
|
+
name: projectName,
|
|
714
|
+
localPath: projectRootPath,
|
|
715
|
+
pages,
|
|
716
|
+
components,
|
|
717
|
+
installedApps,
|
|
718
|
+
installedAppContracts,
|
|
719
|
+
plugins
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
async function handlePagePublish(resolvedConfig, page) {
|
|
723
|
+
try {
|
|
724
|
+
const pagesBasePath = path.resolve(resolvedConfig.devServer.pagesBase);
|
|
85
725
|
const routePath = page.route === "/" ? "" : page.route;
|
|
86
726
|
const dirname = path.join(pagesBasePath, routePath);
|
|
87
727
|
const fullPath = path.join(dirname, PAGE_FILENAME);
|
|
@@ -119,9 +759,9 @@ async function handlePagePublish(resolvedConfig2, page) {
|
|
|
119
759
|
];
|
|
120
760
|
}
|
|
121
761
|
}
|
|
122
|
-
async function handleComponentPublish(
|
|
762
|
+
async function handleComponentPublish(resolvedConfig, component) {
|
|
123
763
|
try {
|
|
124
|
-
const dirname = path.resolve(
|
|
764
|
+
const dirname = path.resolve(resolvedConfig.devServer.componentsBase);
|
|
125
765
|
const filename = `${component.name}${JAY_EXTENSION}`;
|
|
126
766
|
const fullPath = path.join(dirname, filename);
|
|
127
767
|
await fs.promises.mkdir(dirname, { recursive: true });
|
|
@@ -157,13 +797,13 @@ async function handleComponentPublish(resolvedConfig2, component) {
|
|
|
157
797
|
];
|
|
158
798
|
}
|
|
159
799
|
}
|
|
160
|
-
function createEditorHandlers(
|
|
800
|
+
function createEditorHandlers(config, tsConfigPath, projectRoot) {
|
|
161
801
|
const onPublish = async (params) => {
|
|
162
802
|
const status = [];
|
|
163
803
|
const createdJayHtmls = [];
|
|
164
804
|
if (params.pages) {
|
|
165
805
|
for (const page of params.pages) {
|
|
166
|
-
const [pageStatus, createdJayHtml] = await handlePagePublish(
|
|
806
|
+
const [pageStatus, createdJayHtml] = await handlePagePublish(config, page);
|
|
167
807
|
status.push(pageStatus);
|
|
168
808
|
if (pageStatus.success)
|
|
169
809
|
createdJayHtmls.push(createdJayHtml);
|
|
@@ -172,7 +812,7 @@ function createEditorHandlers(config2, tsConfigPath) {
|
|
|
172
812
|
if (params.components) {
|
|
173
813
|
for (const component of params.components) {
|
|
174
814
|
const [compStatus, createdJayHtml] = await handleComponentPublish(
|
|
175
|
-
|
|
815
|
+
config,
|
|
176
816
|
component
|
|
177
817
|
);
|
|
178
818
|
status.push(compStatus);
|
|
@@ -186,7 +826,8 @@ function createEditorHandlers(config2, tsConfigPath) {
|
|
|
186
826
|
filename,
|
|
187
827
|
dirname,
|
|
188
828
|
{ relativePath: tsConfigPath },
|
|
189
|
-
JAY_IMPORT_RESOLVER
|
|
829
|
+
JAY_IMPORT_RESOLVER,
|
|
830
|
+
projectRoot
|
|
190
831
|
);
|
|
191
832
|
const definitionFile = generateElementDefinitionFile(parsedJayHtml);
|
|
192
833
|
if (definitionFile.validations.length > 0)
|
|
@@ -204,7 +845,7 @@ function createEditorHandlers(config2, tsConfigPath) {
|
|
|
204
845
|
};
|
|
205
846
|
const onSaveImage = async (params) => {
|
|
206
847
|
try {
|
|
207
|
-
const imagesDir = path.join(path.resolve(
|
|
848
|
+
const imagesDir = path.join(path.resolve(config.devServer.publicFolder), "images");
|
|
208
849
|
await fs.promises.mkdir(imagesDir, { recursive: true });
|
|
209
850
|
const filename = `${params.imageId}.png`;
|
|
210
851
|
const imagePath = path.join(imagesDir, filename);
|
|
@@ -228,7 +869,7 @@ function createEditorHandlers(config2, tsConfigPath) {
|
|
|
228
869
|
try {
|
|
229
870
|
const filename = `${params.imageId}.png`;
|
|
230
871
|
const imagePath = path.join(
|
|
231
|
-
path.resolve(
|
|
872
|
+
path.resolve(config.devServer.publicFolder),
|
|
232
873
|
"images",
|
|
233
874
|
filename
|
|
234
875
|
);
|
|
@@ -249,13 +890,54 @@ function createEditorHandlers(config2, tsConfigPath) {
|
|
|
249
890
|
};
|
|
250
891
|
}
|
|
251
892
|
};
|
|
893
|
+
const onGetProjectInfo = async (params) => {
|
|
894
|
+
try {
|
|
895
|
+
const pagesBasePath = path.resolve(config.devServer.pagesBase);
|
|
896
|
+
const componentsBasePath = path.resolve(config.devServer.componentsBase);
|
|
897
|
+
const configBasePath = path.resolve(config.devServer.configBase);
|
|
898
|
+
const projectRootPath = process.cwd();
|
|
899
|
+
const info = await scanProjectInfo(
|
|
900
|
+
pagesBasePath,
|
|
901
|
+
componentsBasePath,
|
|
902
|
+
configBasePath,
|
|
903
|
+
projectRootPath
|
|
904
|
+
);
|
|
905
|
+
console.log(`📋 Retrieved project info: ${info.name}`);
|
|
906
|
+
console.log(` Pages: ${info.pages.length}`);
|
|
907
|
+
console.log(` Components: ${info.components.length}`);
|
|
908
|
+
console.log(` Installed Apps: ${info.installedApps.length}`);
|
|
909
|
+
console.log(` App Contracts: ${Object.keys(info.installedAppContracts).length}`);
|
|
910
|
+
return {
|
|
911
|
+
type: "getProjectInfo",
|
|
912
|
+
success: true,
|
|
913
|
+
info
|
|
914
|
+
};
|
|
915
|
+
} catch (error) {
|
|
916
|
+
console.error("Failed to get project info:", error);
|
|
917
|
+
return {
|
|
918
|
+
type: "getProjectInfo",
|
|
919
|
+
success: false,
|
|
920
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
921
|
+
info: {
|
|
922
|
+
name: "Error",
|
|
923
|
+
localPath: process.cwd(),
|
|
924
|
+
pages: [],
|
|
925
|
+
components: [],
|
|
926
|
+
installedApps: [],
|
|
927
|
+
installedAppContracts: {},
|
|
928
|
+
plugins: []
|
|
929
|
+
}
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
};
|
|
252
933
|
return {
|
|
253
934
|
onPublish,
|
|
254
935
|
onSaveImage,
|
|
255
|
-
onHasImage
|
|
936
|
+
onHasImage,
|
|
937
|
+
onGetProjectInfo
|
|
256
938
|
};
|
|
257
939
|
}
|
|
258
|
-
async function generatePageDefinitionFiles(routes, tsConfigPath) {
|
|
940
|
+
async function generatePageDefinitionFiles(routes, tsConfigPath, projectRoot) {
|
|
259
941
|
for (const route of routes) {
|
|
260
942
|
const jayHtmlPath = route.fsRoute.jayHtmlPath;
|
|
261
943
|
if (!fs.existsSync(jayHtmlPath)) {
|
|
@@ -281,7 +963,8 @@ async function generatePageDefinitionFiles(routes, tsConfigPath) {
|
|
|
281
963
|
filename,
|
|
282
964
|
dirname,
|
|
283
965
|
{ relativePath: tsConfigPath },
|
|
284
|
-
JAY_IMPORT_RESOLVER
|
|
966
|
+
JAY_IMPORT_RESOLVER,
|
|
967
|
+
projectRoot
|
|
285
968
|
);
|
|
286
969
|
const definitionFile = generateElementDefinitionFile(parsedJayHtml);
|
|
287
970
|
if (definitionFile.validations.length > 0) {
|
|
@@ -298,14 +981,18 @@ async function generatePageDefinitionFiles(routes, tsConfigPath) {
|
|
|
298
981
|
}
|
|
299
982
|
}
|
|
300
983
|
}
|
|
301
|
-
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const
|
|
308
|
-
|
|
984
|
+
async function startDevServer(options = {}) {
|
|
985
|
+
const projectPath = options.projectPath || process.cwd();
|
|
986
|
+
if (projectPath !== process.cwd()) {
|
|
987
|
+
process.chdir(projectPath);
|
|
988
|
+
}
|
|
989
|
+
const config = loadConfig();
|
|
990
|
+
const resolvedConfig = getConfigWithDefaults(config);
|
|
991
|
+
const jayOptions = {
|
|
992
|
+
tsConfigFilePath: "./tsconfig.json",
|
|
993
|
+
outputDir: "build/jay-runtime"
|
|
994
|
+
};
|
|
995
|
+
const app = express();
|
|
309
996
|
const devServerPort = await getPort({ port: resolvedConfig.devServer.portRange });
|
|
310
997
|
const editorServer = createEditorServer({
|
|
311
998
|
portRange: resolvedConfig.editorServer.portRange,
|
|
@@ -320,10 +1007,15 @@ async function initApp() {
|
|
|
320
1007
|
}
|
|
321
1008
|
});
|
|
322
1009
|
const { port: editorPort, editorId } = await editorServer.start();
|
|
323
|
-
const handlers = createEditorHandlers(
|
|
1010
|
+
const handlers = createEditorHandlers(
|
|
1011
|
+
resolvedConfig,
|
|
1012
|
+
jayOptions.tsConfigFilePath,
|
|
1013
|
+
process.cwd()
|
|
1014
|
+
);
|
|
324
1015
|
editorServer.onPublish(handlers.onPublish);
|
|
325
1016
|
editorServer.onSaveImage(handlers.onSaveImage);
|
|
326
1017
|
editorServer.onHasImage(handlers.onHasImage);
|
|
1018
|
+
editorServer.onGetProjectInfo(handlers.onGetProjectInfo);
|
|
327
1019
|
const { server, viteServer, routes } = await mkDevServer({
|
|
328
1020
|
pagesRootFolder: path.resolve(resolvedConfig.devServer.pagesBase),
|
|
329
1021
|
projectRootFolder: process.cwd(),
|
|
@@ -341,7 +1033,7 @@ async function initApp() {
|
|
|
341
1033
|
routes.forEach((route) => {
|
|
342
1034
|
app.get(route.path, route.handler);
|
|
343
1035
|
});
|
|
344
|
-
generatePageDefinitionFiles(routes, jayOptions.tsConfigFilePath);
|
|
1036
|
+
generatePageDefinitionFiles(routes, jayOptions.tsConfigFilePath, process.cwd());
|
|
345
1037
|
const expressServer = app.listen(devServerPort, () => {
|
|
346
1038
|
console.log(`🚀 Jay Stack dev server started successfully!`);
|
|
347
1039
|
console.log(`📱 Dev Server: http://localhost:${devServerPort}`);
|
|
@@ -361,13 +1053,496 @@ async function initApp() {
|
|
|
361
1053
|
process.on("SIGTERM", shutdown);
|
|
362
1054
|
process.on("SIGINT", shutdown);
|
|
363
1055
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
1056
|
+
async function validatePlugin(options = {}) {
|
|
1057
|
+
const pluginPath = options.pluginPath || process.cwd();
|
|
1058
|
+
if (options.local) {
|
|
1059
|
+
return validateLocalPlugins(pluginPath, options);
|
|
1060
|
+
} else {
|
|
1061
|
+
return validatePluginPackage(pluginPath, options);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
async function validatePluginPackage(pluginPath, options) {
|
|
1065
|
+
const result = {
|
|
1066
|
+
valid: true,
|
|
1067
|
+
errors: [],
|
|
1068
|
+
warnings: [],
|
|
1069
|
+
contractsChecked: 0,
|
|
1070
|
+
componentsChecked: 0
|
|
1071
|
+
};
|
|
1072
|
+
const pluginYamlPath = path.join(pluginPath, "plugin.yaml");
|
|
1073
|
+
const pluginManifest = loadPluginManifest(pluginPath);
|
|
1074
|
+
if (!pluginManifest) {
|
|
1075
|
+
if (!fs.existsSync(pluginYamlPath)) {
|
|
1076
|
+
result.errors.push({
|
|
1077
|
+
type: "file-missing",
|
|
1078
|
+
message: "plugin.yaml not found",
|
|
1079
|
+
location: pluginPath,
|
|
1080
|
+
suggestion: "Create a plugin.yaml file in the plugin root directory"
|
|
1081
|
+
});
|
|
1082
|
+
} else {
|
|
1083
|
+
result.errors.push({
|
|
1084
|
+
type: "schema",
|
|
1085
|
+
message: "Invalid YAML syntax or format",
|
|
1086
|
+
location: pluginYamlPath
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
result.valid = false;
|
|
1090
|
+
return result;
|
|
1091
|
+
}
|
|
1092
|
+
result.pluginName = pluginManifest.name;
|
|
1093
|
+
const context = {
|
|
1094
|
+
manifest: pluginManifest,
|
|
1095
|
+
pluginPath,
|
|
1096
|
+
isNpmPackage: fs.existsSync(path.join(pluginPath, "package.json"))
|
|
1097
|
+
};
|
|
1098
|
+
await validateSchema(context, result);
|
|
1099
|
+
if (pluginManifest.contracts) {
|
|
1100
|
+
for (let i = 0; i < pluginManifest.contracts.length; i++) {
|
|
1101
|
+
await validateContract(
|
|
1102
|
+
pluginManifest.contracts[i],
|
|
1103
|
+
i,
|
|
1104
|
+
context,
|
|
1105
|
+
options.generateTypes || false,
|
|
1106
|
+
result
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
if (pluginManifest.contracts) {
|
|
1111
|
+
for (let i = 0; i < pluginManifest.contracts.length; i++) {
|
|
1112
|
+
await validateComponent(pluginManifest.contracts[i], i, context, result);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
if (context.isNpmPackage) {
|
|
1116
|
+
await validatePackageJson(context, result);
|
|
1117
|
+
result.packageJsonChecked = true;
|
|
1118
|
+
}
|
|
1119
|
+
if (pluginManifest.dynamic_contracts) {
|
|
1120
|
+
await validateDynamicContracts(context, result);
|
|
1121
|
+
}
|
|
1122
|
+
result.valid = result.errors.length === 0;
|
|
1123
|
+
return result;
|
|
1124
|
+
}
|
|
1125
|
+
async function validateLocalPlugins(projectPath, options) {
|
|
1126
|
+
const pluginsPath = path.join(projectPath, "src/plugins");
|
|
1127
|
+
if (!fs.existsSync(pluginsPath)) {
|
|
1128
|
+
return {
|
|
1129
|
+
valid: false,
|
|
1130
|
+
errors: [
|
|
1131
|
+
{
|
|
1132
|
+
type: "file-missing",
|
|
1133
|
+
message: "src/plugins/ directory not found",
|
|
1134
|
+
location: projectPath,
|
|
1135
|
+
suggestion: "Create src/plugins/ directory for local plugins"
|
|
1136
|
+
}
|
|
1137
|
+
],
|
|
1138
|
+
warnings: []
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
const pluginDirs = fs.readdirSync(pluginsPath, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
1142
|
+
const allResults = [];
|
|
1143
|
+
for (const pluginDir of pluginDirs) {
|
|
1144
|
+
const pluginPath = path.join(pluginsPath, pluginDir.name);
|
|
1145
|
+
const result = await validatePluginPackage(pluginPath, options);
|
|
1146
|
+
allResults.push(result);
|
|
1147
|
+
}
|
|
1148
|
+
return {
|
|
1149
|
+
valid: allResults.every((r) => r.valid),
|
|
1150
|
+
errors: allResults.flatMap((r) => r.errors),
|
|
1151
|
+
warnings: allResults.flatMap((r) => r.warnings),
|
|
1152
|
+
contractsChecked: allResults.reduce((sum, r) => sum + (r.contractsChecked || 0), 0),
|
|
1153
|
+
componentsChecked: allResults.reduce((sum, r) => sum + (r.componentsChecked || 0), 0),
|
|
1154
|
+
typesGenerated: allResults.reduce((sum, r) => sum + (r.typesGenerated || 0), 0)
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
async function validateSchema(context, result) {
|
|
1158
|
+
const { manifest } = context;
|
|
1159
|
+
if (!manifest.name) {
|
|
1160
|
+
result.errors.push({
|
|
1161
|
+
type: "schema",
|
|
1162
|
+
message: "Missing required field: name",
|
|
1163
|
+
location: "plugin.yaml",
|
|
1164
|
+
suggestion: 'Add a "name" field with a kebab-case plugin name'
|
|
1165
|
+
});
|
|
1166
|
+
} else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(manifest.name)) {
|
|
1167
|
+
result.errors.push({
|
|
1168
|
+
type: "schema",
|
|
1169
|
+
message: `Invalid plugin name: "${manifest.name}". Must be kebab-case.`,
|
|
1170
|
+
location: "plugin.yaml",
|
|
1171
|
+
suggestion: 'Use lowercase letters, numbers, and hyphens only (e.g., "my-plugin")'
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
if (manifest.contracts) {
|
|
1175
|
+
if (!Array.isArray(manifest.contracts)) {
|
|
1176
|
+
result.errors.push({
|
|
1177
|
+
type: "schema",
|
|
1178
|
+
message: 'Field "contracts" must be an array',
|
|
1179
|
+
location: "plugin.yaml"
|
|
1180
|
+
});
|
|
1181
|
+
} else {
|
|
1182
|
+
manifest.contracts.forEach((contract, index) => {
|
|
1183
|
+
if (!contract.name) {
|
|
1184
|
+
result.errors.push({
|
|
1185
|
+
type: "schema",
|
|
1186
|
+
message: `Contract at index ${index} is missing "name" field`,
|
|
1187
|
+
location: "plugin.yaml"
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
if (!contract.contract) {
|
|
1191
|
+
result.errors.push({
|
|
1192
|
+
type: "schema",
|
|
1193
|
+
message: `Contract "${contract.name || index}" is missing "contract" field`,
|
|
1194
|
+
location: "plugin.yaml",
|
|
1195
|
+
suggestion: "Specify path to .jay-contract file"
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
if (!contract.component) {
|
|
1199
|
+
result.errors.push({
|
|
1200
|
+
type: "schema",
|
|
1201
|
+
message: `Contract "${contract.name || index}" is missing "component" field`,
|
|
1202
|
+
location: "plugin.yaml",
|
|
1203
|
+
suggestion: 'Specify the exported member name from the module (e.g., "moodTracker")'
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
if (manifest.dynamic_contracts) {
|
|
1210
|
+
if (!manifest.dynamic_contracts.component) {
|
|
1211
|
+
result.errors.push({
|
|
1212
|
+
type: "schema",
|
|
1213
|
+
message: 'dynamic_contracts is missing "component" field',
|
|
1214
|
+
location: "plugin.yaml",
|
|
1215
|
+
suggestion: "Specify path to shared component for dynamic contracts"
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
if (!manifest.dynamic_contracts.generator) {
|
|
1219
|
+
result.errors.push({
|
|
1220
|
+
type: "schema",
|
|
1221
|
+
message: 'dynamic_contracts is missing "generator" field',
|
|
1222
|
+
location: "plugin.yaml",
|
|
1223
|
+
suggestion: "Specify path to generator file"
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
if (!manifest.dynamic_contracts.prefix) {
|
|
1227
|
+
result.errors.push({
|
|
1228
|
+
type: "schema",
|
|
1229
|
+
message: 'dynamic_contracts is missing "prefix" field',
|
|
1230
|
+
location: "plugin.yaml",
|
|
1231
|
+
suggestion: 'Specify prefix for dynamic contract names (e.g., "cms")'
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
if (!manifest.contracts && !manifest.dynamic_contracts) {
|
|
1236
|
+
result.warnings.push({
|
|
1237
|
+
type: "schema",
|
|
1238
|
+
message: "Plugin has no contracts or dynamic_contracts defined",
|
|
1239
|
+
location: "plugin.yaml",
|
|
1240
|
+
suggestion: 'Add either "contracts" or "dynamic_contracts" to expose functionality'
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
async function validateContract(contract, index, context, generateTypes, result) {
|
|
1245
|
+
result.contractsChecked = (result.contractsChecked || 0) + 1;
|
|
1246
|
+
let contractPath;
|
|
1247
|
+
if (context.isNpmPackage) {
|
|
1248
|
+
const contractSpec = contract.contract;
|
|
1249
|
+
const possiblePaths = [
|
|
1250
|
+
path.join(context.pluginPath, "dist", contractSpec),
|
|
1251
|
+
path.join(context.pluginPath, "lib", contractSpec),
|
|
1252
|
+
path.join(context.pluginPath, contractSpec)
|
|
1253
|
+
];
|
|
1254
|
+
let found = false;
|
|
1255
|
+
for (const possiblePath of possiblePaths) {
|
|
1256
|
+
if (fs.existsSync(possiblePath)) {
|
|
1257
|
+
contractPath = possiblePath;
|
|
1258
|
+
found = true;
|
|
1259
|
+
break;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
if (!found) {
|
|
1263
|
+
result.errors.push({
|
|
1264
|
+
type: "file-missing",
|
|
1265
|
+
message: `Contract file not found: ${contractSpec}`,
|
|
1266
|
+
location: `plugin.yaml contracts[${index}]`,
|
|
1267
|
+
suggestion: `Ensure the contract file exists (looked in dist/, lib/, and root)`
|
|
1268
|
+
});
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
} else {
|
|
1272
|
+
contractPath = path.join(context.pluginPath, contract.contract);
|
|
1273
|
+
if (!fs.existsSync(contractPath)) {
|
|
1274
|
+
result.errors.push({
|
|
1275
|
+
type: "file-missing",
|
|
1276
|
+
message: `Contract file not found: ${contract.contract}`,
|
|
1277
|
+
location: `plugin.yaml contracts[${index}]`,
|
|
1278
|
+
suggestion: `Create the contract file at ${contractPath}`
|
|
1279
|
+
});
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
try {
|
|
1284
|
+
const contractContent = await fs.promises.readFile(contractPath, "utf-8");
|
|
1285
|
+
const parsedContract = YAML.parse(contractContent);
|
|
1286
|
+
if (!parsedContract.name) {
|
|
1287
|
+
result.errors.push({
|
|
1288
|
+
type: "contract-invalid",
|
|
1289
|
+
message: `Contract file ${contract.contract} is missing "name" field`,
|
|
1290
|
+
location: contractPath
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
if (!parsedContract.tags || !Array.isArray(parsedContract.tags)) {
|
|
1294
|
+
result.errors.push({
|
|
1295
|
+
type: "contract-invalid",
|
|
1296
|
+
message: `Contract file ${contract.contract} is missing "tags" array`,
|
|
1297
|
+
location: contractPath
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
} catch (error) {
|
|
1301
|
+
result.errors.push({
|
|
1302
|
+
type: "contract-invalid",
|
|
1303
|
+
message: `Invalid contract YAML: ${error.message}`,
|
|
1304
|
+
location: contractPath,
|
|
1305
|
+
suggestion: "Check YAML syntax and ensure it follows Jay contract format"
|
|
1306
|
+
});
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
if (generateTypes) {
|
|
1310
|
+
try {
|
|
1311
|
+
const { compileContractFile } = await import("@jay-framework/compiler-jay-html");
|
|
1312
|
+
const dtsPath = contractPath + ".d.ts";
|
|
1313
|
+
await compileContractFile(contractPath, dtsPath);
|
|
1314
|
+
result.typesGenerated = (result.typesGenerated || 0) + 1;
|
|
1315
|
+
} catch (error) {
|
|
1316
|
+
result.errors.push({
|
|
1317
|
+
type: "type-generation-failed",
|
|
1318
|
+
message: `Failed to generate types for ${contract.contract}: ${error.message}`,
|
|
1319
|
+
location: contractPath
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
async function validateComponent(contract, index, context, result) {
|
|
1325
|
+
result.componentsChecked = (result.componentsChecked || 0) + 1;
|
|
1326
|
+
if (typeof contract.component !== "string" || contract.component.length === 0) {
|
|
1327
|
+
result.errors.push({
|
|
1328
|
+
type: "schema",
|
|
1329
|
+
message: `Invalid component name: ${contract.component}`,
|
|
1330
|
+
location: `plugin.yaml contracts[${index}]`,
|
|
1331
|
+
suggestion: 'Component should be the exported member name (e.g., "moodTracker")'
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
if (contract.component.includes("/") || contract.component.includes(".")) {
|
|
1335
|
+
result.warnings.push({
|
|
1336
|
+
type: "schema",
|
|
1337
|
+
message: `Component "${contract.component}" looks like a path. Should it be an export name?`,
|
|
1338
|
+
location: `plugin.yaml contracts[${index}]`,
|
|
1339
|
+
suggestion: 'Component should be the exported member name (e.g., "moodTracker"), not a file path'
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
async function validatePackageJson(context, result) {
|
|
1344
|
+
const packageJsonPath = path.join(context.pluginPath, "package.json");
|
|
1345
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
1346
|
+
result.warnings.push({
|
|
1347
|
+
type: "file-missing",
|
|
1348
|
+
message: "package.json not found",
|
|
1349
|
+
location: context.pluginPath,
|
|
1350
|
+
suggestion: "Create a package.json file for NPM package distribution"
|
|
1351
|
+
});
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
try {
|
|
1355
|
+
const packageJson = JSON.parse(await fs.promises.readFile(packageJsonPath, "utf-8"));
|
|
1356
|
+
if (!packageJson.exports) {
|
|
1357
|
+
result.warnings.push({
|
|
1358
|
+
type: "export-mismatch",
|
|
1359
|
+
message: 'package.json missing "exports" field',
|
|
1360
|
+
location: packageJsonPath,
|
|
1361
|
+
suggestion: "Add exports field to define entry points for server/client builds"
|
|
1362
|
+
});
|
|
1363
|
+
} else {
|
|
1364
|
+
if (!packageJson.exports["."]) {
|
|
1365
|
+
result.warnings.push({
|
|
1366
|
+
type: "export-mismatch",
|
|
1367
|
+
message: 'package.json exports missing "." entry point',
|
|
1368
|
+
location: packageJsonPath,
|
|
1369
|
+
suggestion: 'Add "." export for the main module entry'
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
if (context.manifest.contracts) {
|
|
1373
|
+
for (const contract of context.manifest.contracts) {
|
|
1374
|
+
const contractExport = "./" + contract.contract;
|
|
1375
|
+
if (!packageJson.exports[contractExport]) {
|
|
1376
|
+
result.errors.push({
|
|
1377
|
+
type: "export-mismatch",
|
|
1378
|
+
message: `Contract "${contract.name}" not exported in package.json`,
|
|
1379
|
+
location: packageJsonPath,
|
|
1380
|
+
suggestion: `Add "${contractExport}": "./dist/${contract.contract}" to exports field`
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
if (!packageJson.exports["."]) {
|
|
1386
|
+
result.errors.push({
|
|
1387
|
+
type: "export-mismatch",
|
|
1388
|
+
message: 'NPM package missing "." export in package.json',
|
|
1389
|
+
location: packageJsonPath,
|
|
1390
|
+
suggestion: 'Add ".": "./dist/index.js" (or your main file) to exports field'
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
if (context.manifest.module) {
|
|
1394
|
+
const moduleName = context.manifest.module;
|
|
1395
|
+
result.warnings.push({
|
|
1396
|
+
type: "schema",
|
|
1397
|
+
message: 'NPM packages should omit the "module" field - the package main export will be used',
|
|
1398
|
+
location: "plugin.yaml",
|
|
1399
|
+
suggestion: 'Remove the "module" field from plugin.yaml'
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
if (!packageJson.exports || !packageJson.exports["./plugin.yaml"]) {
|
|
1404
|
+
result.errors.push({
|
|
1405
|
+
type: "export-mismatch",
|
|
1406
|
+
message: "plugin.yaml not exported in package.json (required for plugin resolution)",
|
|
1407
|
+
location: packageJsonPath,
|
|
1408
|
+
suggestion: 'Add "./plugin.yaml": "./plugin.yaml" to exports field'
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
} catch (error) {
|
|
1412
|
+
result.errors.push({
|
|
1413
|
+
type: "schema",
|
|
1414
|
+
message: `Invalid package.json: ${error.message}`,
|
|
1415
|
+
location: packageJsonPath
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
async function validateDynamicContracts(context, result) {
|
|
1420
|
+
const { dynamic_contracts } = context.manifest;
|
|
1421
|
+
if (!dynamic_contracts)
|
|
1422
|
+
return;
|
|
1423
|
+
if (dynamic_contracts.generator) {
|
|
1424
|
+
const generatorPath = path.join(context.pluginPath, dynamic_contracts.generator);
|
|
1425
|
+
const possibleExtensions = [".ts", ".js", "/index.ts", "/index.js"];
|
|
1426
|
+
let found = false;
|
|
1427
|
+
for (const ext of possibleExtensions) {
|
|
1428
|
+
if (fs.existsSync(generatorPath + ext)) {
|
|
1429
|
+
found = true;
|
|
1430
|
+
break;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
if (!found && !context.isNpmPackage) {
|
|
1434
|
+
result.errors.push({
|
|
1435
|
+
type: "file-missing",
|
|
1436
|
+
message: `Generator file not found: ${dynamic_contracts.generator}`,
|
|
1437
|
+
location: "plugin.yaml dynamic_contracts",
|
|
1438
|
+
suggestion: `Create generator file at ${generatorPath}.ts`
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
if (dynamic_contracts.component) {
|
|
1443
|
+
const componentPath = path.join(context.pluginPath, dynamic_contracts.component);
|
|
1444
|
+
const possibleExtensions = [".ts", ".js", "/index.ts", "/index.js"];
|
|
1445
|
+
let found = false;
|
|
1446
|
+
for (const ext of possibleExtensions) {
|
|
1447
|
+
if (fs.existsSync(componentPath + ext)) {
|
|
1448
|
+
found = true;
|
|
1449
|
+
break;
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
if (!found && !context.isNpmPackage) {
|
|
1453
|
+
result.errors.push({
|
|
1454
|
+
type: "file-missing",
|
|
1455
|
+
message: `Dynamic contracts component not found: ${dynamic_contracts.component}`,
|
|
1456
|
+
location: "plugin.yaml dynamic_contracts",
|
|
1457
|
+
suggestion: `Create component file at ${componentPath}.ts`
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
const program = new Command();
|
|
1463
|
+
program.name("jay-stack").description("Jay Stack CLI - Development server and plugin validation").version("0.9.0");
|
|
1464
|
+
program.command("dev [path]").description("Start the Jay Stack development server").action(async (path2, options) => {
|
|
1465
|
+
try {
|
|
1466
|
+
await startDevServer({
|
|
1467
|
+
projectPath: path2 || process.cwd()
|
|
1468
|
+
});
|
|
1469
|
+
} catch (error) {
|
|
1470
|
+
console.error(chalk.red("Error starting dev server:"), error.message);
|
|
1471
|
+
process.exit(1);
|
|
1472
|
+
}
|
|
1473
|
+
});
|
|
1474
|
+
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) => {
|
|
1475
|
+
try {
|
|
1476
|
+
const result = await validatePlugin({
|
|
1477
|
+
pluginPath: pluginPath || process.cwd(),
|
|
1478
|
+
local: options.local,
|
|
1479
|
+
verbose: options.verbose,
|
|
1480
|
+
strict: options.strict,
|
|
1481
|
+
generateTypes: options.generateTypes
|
|
1482
|
+
});
|
|
1483
|
+
printValidationResult(result, options.verbose);
|
|
1484
|
+
if (!result.valid || options.strict && result.warnings.length > 0) {
|
|
1485
|
+
process.exit(1);
|
|
1486
|
+
}
|
|
1487
|
+
} catch (error) {
|
|
1488
|
+
console.error(chalk.red("Validation error:"), error.message);
|
|
1489
|
+
process.exit(1);
|
|
1490
|
+
}
|
|
367
1491
|
});
|
|
1492
|
+
program.parse(process.argv);
|
|
1493
|
+
if (!process.argv.slice(2).length) {
|
|
1494
|
+
program.outputHelp();
|
|
1495
|
+
}
|
|
1496
|
+
function printValidationResult(result, verbose) {
|
|
1497
|
+
if (result.valid && result.warnings.length === 0) {
|
|
1498
|
+
console.log(chalk.green("✅ Plugin validation successful!\n"));
|
|
1499
|
+
if (verbose) {
|
|
1500
|
+
console.log("Plugin:", result.pluginName);
|
|
1501
|
+
console.log(" ✅ plugin.yaml valid");
|
|
1502
|
+
console.log(` ✅ ${result.contractsChecked} contracts validated`);
|
|
1503
|
+
if (result.typesGenerated) {
|
|
1504
|
+
console.log(` ✅ ${result.typesGenerated} type definitions generated`);
|
|
1505
|
+
}
|
|
1506
|
+
console.log(` ✅ ${result.componentsChecked} components validated`);
|
|
1507
|
+
if (result.packageJsonChecked) {
|
|
1508
|
+
console.log(" ✅ package.json valid");
|
|
1509
|
+
}
|
|
1510
|
+
console.log("\nNo errors found.");
|
|
1511
|
+
}
|
|
1512
|
+
} else if (result.valid && result.warnings.length > 0) {
|
|
1513
|
+
console.log(chalk.yellow("⚠️ Plugin validation passed with warnings\n"));
|
|
1514
|
+
console.log("Warnings:");
|
|
1515
|
+
result.warnings.forEach((warning) => {
|
|
1516
|
+
console.log(chalk.yellow(` ⚠️ ${warning.message}`));
|
|
1517
|
+
if (warning.location) {
|
|
1518
|
+
console.log(chalk.gray(` Location: ${warning.location}`));
|
|
1519
|
+
}
|
|
1520
|
+
if (warning.suggestion) {
|
|
1521
|
+
console.log(chalk.gray(` → ${warning.suggestion}`));
|
|
1522
|
+
}
|
|
1523
|
+
console.log();
|
|
1524
|
+
});
|
|
1525
|
+
console.log(chalk.gray("Use --strict to treat warnings as errors."));
|
|
1526
|
+
} else {
|
|
1527
|
+
console.log(chalk.red("❌ Plugin validation failed\n"));
|
|
1528
|
+
console.log("Errors:");
|
|
1529
|
+
result.errors.forEach((error) => {
|
|
1530
|
+
console.log(chalk.red(` ❌ ${error.message}`));
|
|
1531
|
+
if (error.location) {
|
|
1532
|
+
console.log(chalk.gray(` Location: ${error.location}`));
|
|
1533
|
+
}
|
|
1534
|
+
if (error.suggestion) {
|
|
1535
|
+
console.log(chalk.gray(` → ${error.suggestion}`));
|
|
1536
|
+
}
|
|
1537
|
+
console.log();
|
|
1538
|
+
});
|
|
1539
|
+
console.log(chalk.red(`${result.errors.length} errors found.`));
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
368
1542
|
export {
|
|
369
1543
|
createEditorHandlers,
|
|
370
1544
|
getConfigWithDefaults,
|
|
371
1545
|
loadConfig,
|
|
1546
|
+
startDevServer,
|
|
372
1547
|
updateConfig
|
|
373
1548
|
};
|