@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.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 { parseJayFile, JAY_IMPORT_RESOLVER, generateElementDefinitionFile } from "@jay-framework/compiler-jay-html";
10
- import { JAY_CONTRACT_EXTENSION, JAY_EXTENSION } from "@jay-framework/compiler-shared";
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(config2) {
50
+ function getConfigWithDefaults(config) {
46
51
  return {
47
52
  devServer: {
48
- portRange: config2.devServer?.portRange || DEFAULT_CONFIG.devServer.portRange,
49
- pagesBase: config2.devServer?.pagesBase || DEFAULT_CONFIG.devServer.pagesBase,
50
- componentsBase: config2.devServer?.componentsBase || DEFAULT_CONFIG.devServer.componentsBase,
51
- publicFolder: config2.devServer?.publicFolder || DEFAULT_CONFIG.devServer.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: config2.editorServer?.portRange || DEFAULT_CONFIG.editorServer.portRange,
55
- editorId: config2.editorServer?.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
- async function handlePagePublish(resolvedConfig2, page) {
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 pagesBasePath = path.resolve(resolvedConfig2.devServer.pagesBase);
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(resolvedConfig2, component) {
762
+ async function handleComponentPublish(resolvedConfig, component) {
123
763
  try {
124
- const dirname = path.resolve(resolvedConfig2.devServer.componentsBase);
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(config2, tsConfigPath) {
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(config2, page);
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
- config2,
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(config2.devServer.publicFolder), "images");
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(config2.devServer.publicFolder),
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
- const config = loadConfig();
302
- const resolvedConfig = getConfigWithDefaults(config);
303
- const jayOptions = {
304
- tsConfigFilePath: "./tsconfig.json",
305
- outputDir: "build/jay-runtime"
306
- };
307
- const app = express();
308
- async function initApp() {
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(resolvedConfig, jayOptions.tsConfigFilePath);
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
- initApp().catch((error) => {
365
- console.error("Failed to start servers:", error);
366
- process.exit(1);
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
  };