@jay-framework/plugin-validator 0.15.5 → 0.16.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 CHANGED
@@ -18,7 +18,7 @@ interface ValidationResult {
18
18
  typesGenerated?: number;
19
19
  }
20
20
  interface ValidationError {
21
- type: 'schema' | 'file-missing' | 'export-mismatch' | 'contract-invalid' | 'type-generation-failed';
21
+ type: 'schema' | 'file-missing' | 'export-mismatch' | 'contract-invalid' | 'component-contract-mismatch' | 'type-generation-failed';
22
22
  message: string;
23
23
  location?: string;
24
24
  suggestion?: string;
@@ -43,4 +43,40 @@ interface PluginContext {
43
43
  */
44
44
  declare function validatePlugin(options?: ValidatePluginOptions): Promise<ValidationResult>;
45
45
 
46
- export { type PluginContext, type ValidatePluginOptions, type ValidationError, type ValidationResult, type ValidationWarning, validatePlugin };
46
+ /**
47
+ * Component-contract consistency checker (Design Log #124, Phase 3).
48
+ *
49
+ * Single-file AST analysis: detects .withProps<T>() and .withLoadParams(fn)
50
+ * in a component's TypeScript source, then compares the type's properties
51
+ * against the contract's props/params declarations.
52
+ *
53
+ * Uses @jay-framework/typescript-bridge for AST parsing — same approach as
54
+ * source-file-binding-resolver.ts in the compiler package.
55
+ */
56
+
57
+ interface ContractPropsAndParams {
58
+ props?: Array<{
59
+ name: string;
60
+ required?: boolean;
61
+ }>;
62
+ params?: Array<{
63
+ name: string;
64
+ }>;
65
+ }
66
+ interface CheckResult {
67
+ errors: ValidationError[];
68
+ warnings: ValidationWarning[];
69
+ }
70
+ /**
71
+ * Check that a component's .withProps<T>() and .withLoadParams(fn) usage
72
+ * is consistent with its contract's props/params declarations.
73
+ *
74
+ * @param sourceCode - The component's TypeScript source code
75
+ * @param contract - The parsed contract (props/params from the .jay-contract file)
76
+ * @param contractName - The contract name (without .jay-contract suffix), used in error messages
77
+ * @param contractPath - Path to the contract file (for error location)
78
+ * @param sourcePath - Path to the component source (for error location)
79
+ */
80
+ declare function checkComponentPropsAndParams(sourceCode: string, contract: ContractPropsAndParams, contractName: string, contractPath: string, sourcePath: string): CheckResult;
81
+
82
+ export { type PluginContext, type ValidatePluginOptions, type ValidationError, type ValidationResult, type ValidationWarning, checkComponentPropsAndParams, validatePlugin };
package/dist/index.js CHANGED
@@ -2,6 +2,276 @@ import fs from "fs";
2
2
  import path from "path";
3
3
  import YAML from "yaml";
4
4
  import { loadPluginManifest } from "@jay-framework/compiler-shared";
5
+ import { parseContract } from "@jay-framework/compiler-jay-html";
6
+ import { ts } from "@jay-framework/typescript-bridge";
7
+ const FRAMEWORK_PROP_TYPES = /* @__PURE__ */ new Set(["PageProps", "RequestQuery"]);
8
+ function checkComponentPropsAndParams(sourceCode, contract, contractName, contractPath, sourcePath) {
9
+ const errors = [];
10
+ const warnings = [];
11
+ const sourceFile = ts.createSourceFile(
12
+ sourcePath,
13
+ sourceCode,
14
+ ts.ScriptTarget.Latest,
15
+ true,
16
+ ts.ScriptKind.TS
17
+ );
18
+ const localInterfaces = collectLocalInterfaces(sourceFile);
19
+ const contractImportedTypes = collectContractImportedTypes(sourceFile);
20
+ const builderInfo = analyzeBuilderChains(sourceFile);
21
+ for (const propsTypeName of builderInfo.propsTypeNames) {
22
+ checkPropsConsistency(
23
+ propsTypeName,
24
+ localInterfaces,
25
+ contractImportedTypes,
26
+ contract,
27
+ contractName,
28
+ contractPath,
29
+ sourcePath,
30
+ errors,
31
+ warnings
32
+ );
33
+ }
34
+ if (builderInfo.hasLoadParams) {
35
+ checkParamsConsistency(
36
+ builderInfo.paramsTypeNames,
37
+ localInterfaces,
38
+ contractImportedTypes,
39
+ contract,
40
+ contractName,
41
+ contractPath,
42
+ sourcePath,
43
+ errors,
44
+ warnings
45
+ );
46
+ }
47
+ return { errors, warnings };
48
+ }
49
+ function collectLocalInterfaces(sourceFile) {
50
+ const interfaces = /* @__PURE__ */ new Map();
51
+ for (const statement of sourceFile.statements) {
52
+ if (ts.isInterfaceDeclaration(statement)) {
53
+ const name = statement.name.text;
54
+ const properties = [];
55
+ for (const member of statement.members) {
56
+ if (ts.isPropertySignature(member) && member.name) {
57
+ if (ts.isIdentifier(member.name)) {
58
+ properties.push(member.name.text);
59
+ } else if (ts.isStringLiteral(member.name)) {
60
+ properties.push(member.name.text);
61
+ }
62
+ }
63
+ }
64
+ const extendsTypes = [];
65
+ if (statement.heritageClauses) {
66
+ for (const clause of statement.heritageClauses) {
67
+ for (const type of clause.types) {
68
+ if (ts.isIdentifier(type.expression)) {
69
+ extendsTypes.push(type.expression.text);
70
+ }
71
+ }
72
+ }
73
+ }
74
+ interfaces.set(name, { name, properties, extendsTypes });
75
+ }
76
+ if (ts.isTypeAliasDeclaration(statement)) {
77
+ const name = statement.name.text;
78
+ if (ts.isTypeLiteralNode(statement.type)) {
79
+ const properties = [];
80
+ for (const member of statement.type.members) {
81
+ if (ts.isPropertySignature(member) && member.name) {
82
+ if (ts.isIdentifier(member.name)) {
83
+ properties.push(member.name.text);
84
+ }
85
+ }
86
+ }
87
+ interfaces.set(name, { name, properties, extendsTypes: [] });
88
+ }
89
+ }
90
+ }
91
+ return interfaces;
92
+ }
93
+ function collectContractImportedTypes(sourceFile) {
94
+ const contractTypes = /* @__PURE__ */ new Set();
95
+ for (const statement of sourceFile.statements) {
96
+ if (!ts.isImportDeclaration(statement))
97
+ continue;
98
+ const moduleSpecifier = statement.moduleSpecifier;
99
+ if (!ts.isStringLiteral(moduleSpecifier))
100
+ continue;
101
+ const modulePath = moduleSpecifier.text;
102
+ if (!modulePath.includes(".jay-contract"))
103
+ continue;
104
+ const importClause = statement.importClause;
105
+ if (!importClause)
106
+ continue;
107
+ const namedBindings = importClause.namedBindings;
108
+ if (namedBindings && ts.isNamedImports(namedBindings)) {
109
+ for (const element of namedBindings.elements) {
110
+ contractTypes.add(element.name.text);
111
+ }
112
+ }
113
+ }
114
+ return contractTypes;
115
+ }
116
+ function analyzeBuilderChains(sourceFile) {
117
+ const result = {
118
+ propsTypeNames: [],
119
+ hasLoadParams: false,
120
+ paramsTypeNames: []
121
+ };
122
+ visitNode(sourceFile, result);
123
+ return result;
124
+ }
125
+ function visitNode(node, result) {
126
+ if (ts.isCallExpression(node)) {
127
+ const expr = node.expression;
128
+ if (ts.isPropertyAccessExpression(expr) && expr.name.text === "withProps") {
129
+ if (node.typeArguments && node.typeArguments.length > 0) {
130
+ const typeNames = extractTypeNames(node.typeArguments[0]);
131
+ result.propsTypeNames.push(...typeNames);
132
+ }
133
+ }
134
+ if (ts.isPropertyAccessExpression(expr) && expr.name.text === "withLoadParams") {
135
+ result.hasLoadParams = true;
136
+ if (node.typeArguments && node.typeArguments.length > 0) {
137
+ const typeNames = extractTypeNames(node.typeArguments[0]);
138
+ result.paramsTypeNames.push(...typeNames);
139
+ }
140
+ if (node.arguments.length > 0) {
141
+ const arg = node.arguments[0];
142
+ if (ts.isIdentifier(arg))
143
+ ;
144
+ }
145
+ }
146
+ }
147
+ ts.forEachChild(node, (child) => visitNode(child, result));
148
+ }
149
+ function extractTypeNames(typeNode) {
150
+ if (ts.isTypeReferenceNode(typeNode)) {
151
+ if (ts.isIdentifier(typeNode.typeName)) {
152
+ return [typeNode.typeName.text];
153
+ }
154
+ }
155
+ if (ts.isIntersectionTypeNode(typeNode)) {
156
+ const names = [];
157
+ for (const member of typeNode.types) {
158
+ names.push(...extractTypeNames(member));
159
+ }
160
+ return names;
161
+ }
162
+ return [];
163
+ }
164
+ function checkPropsConsistency(propsTypeName, localInterfaces, contractImportedTypes, contract, contractName, contractPath, sourcePath, errors, warnings) {
165
+ if (FRAMEWORK_PROP_TYPES.has(propsTypeName))
166
+ return;
167
+ if (contractImportedTypes.has(propsTypeName))
168
+ return;
169
+ const prefix = `[${contractName}]`;
170
+ const iface = localInterfaces.get(propsTypeName);
171
+ if (!iface) {
172
+ if (!contract.props || contract.props.length === 0) {
173
+ errors.push({
174
+ type: "contract-invalid",
175
+ message: `${prefix} component uses .withProps<${propsTypeName}>() but the contract does not declare any props`,
176
+ location: contractPath,
177
+ suggestion: `Add a props section to the contract`
178
+ });
179
+ }
180
+ return;
181
+ }
182
+ const ownProperties = iface.properties;
183
+ if (ownProperties.length === 0)
184
+ return;
185
+ if (!contract.props || contract.props.length === 0) {
186
+ errors.push({
187
+ type: "contract-invalid",
188
+ message: `${prefix} component uses .withProps<${propsTypeName}>() with properties [${ownProperties.join(", ")}] but the contract does not declare any props`,
189
+ location: contractPath,
190
+ suggestion: `Add to contract: props:
191
+ ` + ownProperties.map((p) => ` - name: ${p}
192
+ type: string`).join("\n")
193
+ });
194
+ return;
195
+ }
196
+ const contractPropNames = new Set(contract.props.map((p) => p.name));
197
+ for (const prop of ownProperties) {
198
+ if (!contractPropNames.has(prop)) {
199
+ errors.push({
200
+ type: "contract-invalid",
201
+ message: `${prefix} component prop "${prop}" (from ${propsTypeName}) is not declared in the contract`,
202
+ location: contractPath,
203
+ suggestion: `Add to contract props: - name: ${prop}`
204
+ });
205
+ }
206
+ }
207
+ for (const contractProp of contract.props) {
208
+ if (!ownProperties.includes(contractProp.name)) {
209
+ warnings.push({
210
+ type: "contract-invalid",
211
+ message: `${prefix} contract declares prop "${contractProp.name}" but the component's ${propsTypeName} interface does not include it`,
212
+ location: sourcePath,
213
+ suggestion: `Add "${contractProp.name}" to the ${propsTypeName} interface, or remove it from the contract`
214
+ });
215
+ }
216
+ }
217
+ }
218
+ function checkParamsConsistency(paramsTypeNames, localInterfaces, contractImportedTypes, contract, contractName, contractPath, sourcePath, errors, warnings) {
219
+ const prefix = `[${contractName}]`;
220
+ if (!contract.params || contract.params.length === 0) {
221
+ errors.push({
222
+ type: "contract-invalid",
223
+ message: `${prefix} component uses .withLoadParams() but the contract does not declare any params`,
224
+ location: contractPath,
225
+ suggestion: `Add a params section to the contract (e.g., params: { slug: string })`
226
+ });
227
+ for (const typeName of paramsTypeNames) {
228
+ if (FRAMEWORK_PROP_TYPES.has(typeName))
229
+ continue;
230
+ if (contractImportedTypes.has(typeName))
231
+ continue;
232
+ const iface = localInterfaces.get(typeName);
233
+ if (iface) {
234
+ const ownProps = iface.properties;
235
+ if (ownProps.length > 0) {
236
+ errors[errors.length - 1].suggestion = `Add to contract: params:
237
+ ` + ownProps.map((p) => ` ${p}: string`).join("\n");
238
+ }
239
+ }
240
+ }
241
+ return;
242
+ }
243
+ for (const typeName of paramsTypeNames) {
244
+ if (FRAMEWORK_PROP_TYPES.has(typeName))
245
+ continue;
246
+ if (contractImportedTypes.has(typeName))
247
+ continue;
248
+ const iface = localInterfaces.get(typeName);
249
+ if (!iface)
250
+ continue;
251
+ const ownProperties = iface.properties;
252
+ const contractParamNames = new Set(contract.params.map((p) => p.name));
253
+ for (const prop of ownProperties) {
254
+ if (!contractParamNames.has(prop)) {
255
+ errors.push({
256
+ type: "contract-invalid",
257
+ message: `${prefix} component param "${prop}" (from ${typeName}) is not declared in the contract`,
258
+ location: contractPath,
259
+ suggestion: `Add to contract params: ${prop}: string`
260
+ });
261
+ }
262
+ }
263
+ for (const contractParam of contract.params) {
264
+ if (!ownProperties.includes(contractParam.name)) {
265
+ warnings.push({
266
+ type: "contract-invalid",
267
+ message: `${prefix} contract declares param "${contractParam.name}" but the component's ${typeName} interface does not include it`,
268
+ location: sourcePath,
269
+ suggestion: `Add "${contractParam.name}" to the ${typeName} interface, or remove it from the contract`
270
+ });
271
+ }
272
+ }
273
+ }
274
+ }
5
275
  async function validatePlugin(options = {}) {
6
276
  const pluginPath = options.pluginPath || process.cwd();
7
277
  if (options.local) {
@@ -103,6 +373,36 @@ async function validateLocalPlugins(projectPath, options) {
103
373
  typesGenerated: allResults.reduce((sum, r) => sum + (r.typesGenerated || 0), 0)
104
374
  };
105
375
  }
376
+ function validateDocFile(docPath, label, context, result) {
377
+ const resolvedPath = path.join(context.pluginPath, docPath);
378
+ if (!fs.existsSync(resolvedPath)) {
379
+ result.errors.push({
380
+ type: "file-missing",
381
+ message: `Doc file for ${label} not found: ${docPath}`,
382
+ location: "plugin.yaml",
383
+ suggestion: `Create the documentation file at ${resolvedPath}`
384
+ });
385
+ return;
386
+ }
387
+ if (context.isNpmPackage) {
388
+ const packageJsonPath = path.join(context.pluginPath, "package.json");
389
+ try {
390
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
391
+ if (packageJson.exports) {
392
+ const exportKey = "./" + docPath.replace(/^\.\//, "");
393
+ if (!packageJson.exports[exportKey]) {
394
+ result.errors.push({
395
+ type: "export-mismatch",
396
+ message: `Doc file for ${label} is not exported in package.json: ${docPath}`,
397
+ location: packageJsonPath,
398
+ suggestion: `Add "${exportKey}": "${docPath}" to the exports field`
399
+ });
400
+ }
401
+ }
402
+ } catch {
403
+ }
404
+ }
405
+ }
106
406
  async function validateSchema(context, result) {
107
407
  const { manifest } = context;
108
408
  if (!manifest.name) {
@@ -193,46 +493,157 @@ async function validateSchema(context, result) {
193
493
  suggestion: 'Add either "contracts" or "dynamic_contracts" to expose functionality'
194
494
  });
195
495
  }
196
- }
197
- async function validateContract(contract, index, context, generateTypes, result) {
198
- result.contractsChecked = (result.contractsChecked || 0) + 1;
199
- let contractPath;
200
- if (context.isNpmPackage) {
201
- const contractSpec = contract.contract;
202
- const possiblePaths = [
203
- path.join(context.pluginPath, "dist", contractSpec),
204
- path.join(context.pluginPath, "lib", contractSpec),
205
- path.join(context.pluginPath, contractSpec)
206
- ];
207
- let found = false;
208
- for (const possiblePath of possiblePaths) {
209
- if (fs.existsSync(possiblePath)) {
210
- contractPath = possiblePath;
211
- found = true;
212
- break;
213
- }
496
+ if (manifest.services) {
497
+ if (!Array.isArray(manifest.services)) {
498
+ result.errors.push({
499
+ type: "schema",
500
+ message: 'Field "services" must be an array',
501
+ location: "plugin.yaml"
502
+ });
503
+ } else {
504
+ manifest.services.forEach((service, index) => {
505
+ if (!service.name) {
506
+ result.errors.push({
507
+ type: "schema",
508
+ message: `Service at index ${index} is missing "name" field`,
509
+ location: "plugin.yaml"
510
+ });
511
+ }
512
+ if (!service.marker) {
513
+ result.errors.push({
514
+ type: "schema",
515
+ message: `Service "${service.name || index}" is missing "marker" field`,
516
+ location: "plugin.yaml",
517
+ suggestion: "Specify the exported service marker constant name"
518
+ });
519
+ }
520
+ if (service.doc) {
521
+ validateDocFile(service.doc, `service "${service.name}"`, context, result);
522
+ }
523
+ });
214
524
  }
215
- if (!found) {
525
+ }
526
+ if (manifest.contexts) {
527
+ if (!Array.isArray(manifest.contexts)) {
216
528
  result.errors.push({
217
- type: "file-missing",
218
- message: `Contract file not found: ${contractSpec}`,
219
- location: `plugin.yaml contracts[${index}]`,
220
- suggestion: `Ensure the contract file exists (looked in dist/, lib/, and root)`
529
+ type: "schema",
530
+ message: 'Field "contexts" must be an array',
531
+ location: "plugin.yaml"
532
+ });
533
+ } else {
534
+ manifest.contexts.forEach((ctx, index) => {
535
+ if (!ctx.name) {
536
+ result.errors.push({
537
+ type: "schema",
538
+ message: `Context at index ${index} is missing "name" field`,
539
+ location: "plugin.yaml"
540
+ });
541
+ }
542
+ if (!ctx.marker) {
543
+ result.errors.push({
544
+ type: "schema",
545
+ message: `Context "${ctx.name || index}" is missing "marker" field`,
546
+ location: "plugin.yaml",
547
+ suggestion: "Specify the exported context marker constant name"
548
+ });
549
+ }
550
+ if (ctx.doc) {
551
+ validateDocFile(ctx.doc, `context "${ctx.name}"`, context, result);
552
+ }
221
553
  });
222
- return;
223
554
  }
224
- } else {
225
- contractPath = path.join(context.pluginPath, contract.contract);
226
- if (!fs.existsSync(contractPath)) {
555
+ }
556
+ if (manifest.routes) {
557
+ if (!Array.isArray(manifest.routes)) {
227
558
  result.errors.push({
228
- type: "file-missing",
229
- message: `Contract file not found: ${contract.contract}`,
230
- location: `plugin.yaml contracts[${index}]`,
231
- suggestion: `Create the contract file at ${contractPath}`
559
+ type: "schema",
560
+ message: 'Field "routes" must be an array',
561
+ location: "plugin.yaml"
562
+ });
563
+ } else {
564
+ manifest.routes.forEach((route, index) => {
565
+ if (!route.path) {
566
+ result.errors.push({
567
+ type: "schema",
568
+ message: `Route at index ${index} is missing "path" field`,
569
+ location: "plugin.yaml"
570
+ });
571
+ }
572
+ if (!route.jayHtml) {
573
+ result.errors.push({
574
+ type: "schema",
575
+ message: `Route "${route.path || index}" is missing "jayHtml" field`,
576
+ location: "plugin.yaml",
577
+ suggestion: "Specify the export subpath for the jay-html file"
578
+ });
579
+ }
580
+ if (!route.component) {
581
+ result.errors.push({
582
+ type: "schema",
583
+ message: `Route "${route.path || index}" is missing "component" field`,
584
+ location: "plugin.yaml",
585
+ suggestion: "Specify the exported member name for the page component"
586
+ });
587
+ }
588
+ if (route.jayHtml) {
589
+ validateDocFile(
590
+ route.jayHtml,
591
+ `route "${route.path}" jayHtml`,
592
+ context,
593
+ result
594
+ );
595
+ }
596
+ if (route.css) {
597
+ validateDocFile(route.css, `route "${route.path}" css`, context, result);
598
+ }
232
599
  });
233
- return;
234
600
  }
235
601
  }
602
+ }
603
+ function resolveContractFile(contractSpec, context) {
604
+ if (context.isNpmPackage) {
605
+ const packageJsonPath = path.join(context.pluginPath, "package.json");
606
+ if (fs.existsSync(packageJsonPath)) {
607
+ try {
608
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
609
+ if (packageJson.exports) {
610
+ const exportKey = "./" + contractSpec;
611
+ const exportValue = packageJson.exports[exportKey];
612
+ if (exportValue) {
613
+ const resolvedPath = typeof exportValue === "string" ? exportValue : exportValue.default || exportValue.import || exportValue.require;
614
+ if (resolvedPath) {
615
+ const fullPath = path.join(context.pluginPath, resolvedPath);
616
+ if (fs.existsSync(fullPath))
617
+ return fullPath;
618
+ }
619
+ }
620
+ }
621
+ } catch {
622
+ }
623
+ }
624
+ for (const dir of ["dist", "lib", ""]) {
625
+ const candidate = path.join(context.pluginPath, dir, contractSpec);
626
+ if (fs.existsSync(candidate))
627
+ return candidate;
628
+ }
629
+ return void 0;
630
+ } else {
631
+ const candidate = path.join(context.pluginPath, contractSpec);
632
+ return fs.existsSync(candidate) ? candidate : void 0;
633
+ }
634
+ }
635
+ async function validateContract(contract, index, context, generateTypes, result) {
636
+ result.contractsChecked = (result.contractsChecked || 0) + 1;
637
+ const contractPath = resolveContractFile(contract.contract, context);
638
+ if (!contractPath) {
639
+ result.errors.push({
640
+ type: "file-missing",
641
+ message: `Contract file not found: ${contract.contract}`,
642
+ location: `plugin.yaml contracts[${index}]`,
643
+ suggestion: context.isNpmPackage ? `Ensure the contract is exported in package.json and the file exists` : `Create the contract file at ${path.join(context.pluginPath, contract.contract)}`
644
+ });
645
+ return;
646
+ }
236
647
  try {
237
648
  const contractContent = await fs.promises.readFile(contractPath, "utf-8");
238
649
  const parsedContract = YAML.parse(contractContent);
@@ -283,6 +694,7 @@ async function validateComponent(contract, index, context, result) {
283
694
  location: `plugin.yaml contracts[${index}]`,
284
695
  suggestion: 'Component should be the exported member name (e.g., "moodTracker")'
285
696
  });
697
+ return;
286
698
  }
287
699
  if (contract.component.includes("/") || contract.component.includes(".")) {
288
700
  result.warnings.push({
@@ -292,6 +704,143 @@ async function validateComponent(contract, index, context, result) {
292
704
  suggestion: 'Component should be the exported member name (e.g., "moodTracker"), not a file path'
293
705
  });
294
706
  }
707
+ await checkComponentContractConsistency(contract, context, result);
708
+ }
709
+ function hasExportModifier(node) {
710
+ return node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
711
+ }
712
+ function resolveModulePath(basePath) {
713
+ for (const ext of ["", ".ts", ".js", "/index.ts", "/index.js"]) {
714
+ const candidate = basePath + ext;
715
+ if (fs.existsSync(candidate))
716
+ return candidate;
717
+ }
718
+ return void 0;
719
+ }
720
+ function resolveComponentSourcePath(componentName, context) {
721
+ const modulePath = context.manifest.module || "index";
722
+ const entryBase = path.join(context.pluginPath, modulePath);
723
+ const entryFile = resolveModulePath(entryBase);
724
+ const libEntryFile = !entryFile ? resolveModulePath(path.join(context.pluginPath, "lib", modulePath)) : void 0;
725
+ const sourceEntry = entryFile || libEntryFile;
726
+ if (!sourceEntry)
727
+ return void 0;
728
+ if (!sourceEntry.endsWith(".ts"))
729
+ return void 0;
730
+ let sourceCode;
731
+ try {
732
+ sourceCode = fs.readFileSync(sourceEntry, "utf-8");
733
+ } catch {
734
+ return void 0;
735
+ }
736
+ const sourceFile = ts.createSourceFile(
737
+ sourceEntry,
738
+ sourceCode,
739
+ ts.ScriptTarget.Latest,
740
+ true,
741
+ ts.ScriptKind.TS
742
+ );
743
+ const starReexportModules = [];
744
+ for (const statement of sourceFile.statements) {
745
+ if (!ts.isExportDeclaration(statement))
746
+ continue;
747
+ if (!statement.moduleSpecifier)
748
+ continue;
749
+ if (!ts.isStringLiteral(statement.moduleSpecifier))
750
+ continue;
751
+ const moduleSpec = statement.moduleSpecifier.text;
752
+ const exportClause = statement.exportClause;
753
+ if (!exportClause) {
754
+ starReexportModules.push(moduleSpec);
755
+ continue;
756
+ }
757
+ if (ts.isNamedExports(exportClause)) {
758
+ for (const element of exportClause.elements) {
759
+ const exportedName = element.name.text;
760
+ if (exportedName === componentName) {
761
+ const resolvedBase = path.resolve(path.dirname(sourceEntry), moduleSpec);
762
+ return resolveModulePath(resolvedBase);
763
+ }
764
+ }
765
+ }
766
+ }
767
+ for (const moduleSpec of starReexportModules) {
768
+ if (!moduleSpec.startsWith("."))
769
+ continue;
770
+ const resolvedBase = path.resolve(path.dirname(sourceEntry), moduleSpec);
771
+ const resolvedPath = resolveModulePath(resolvedBase);
772
+ if (!resolvedPath || !resolvedPath.endsWith(".ts"))
773
+ continue;
774
+ try {
775
+ const modSource = fs.readFileSync(resolvedPath, "utf-8");
776
+ const modFile = ts.createSourceFile(
777
+ resolvedPath,
778
+ modSource,
779
+ ts.ScriptTarget.Latest,
780
+ true,
781
+ ts.ScriptKind.TS
782
+ );
783
+ for (const stmt of modFile.statements) {
784
+ if (ts.isVariableStatement(stmt) && hasExportModifier(stmt)) {
785
+ for (const decl of stmt.declarationList.declarations) {
786
+ if (ts.isIdentifier(decl.name) && decl.name.text === componentName) {
787
+ return resolvedPath;
788
+ }
789
+ }
790
+ }
791
+ if (ts.isFunctionDeclaration(stmt) && hasExportModifier(stmt) && stmt.name?.text === componentName) {
792
+ return resolvedPath;
793
+ }
794
+ }
795
+ } catch {
796
+ continue;
797
+ }
798
+ }
799
+ return sourceEntry;
800
+ }
801
+ function resolveContractPath(contract, context) {
802
+ return resolveContractFile(contract.contract, context);
803
+ }
804
+ async function checkComponentContractConsistency(contract, context, result) {
805
+ const componentName = contract.component;
806
+ if (!componentName)
807
+ return;
808
+ const sourcePath = resolveComponentSourcePath(componentName, context);
809
+ if (!sourcePath)
810
+ return;
811
+ if (!sourcePath.endsWith(".ts"))
812
+ return;
813
+ const contractPath = resolveContractPath(contract, context);
814
+ if (!contractPath)
815
+ return;
816
+ let contractContent;
817
+ try {
818
+ contractContent = await fs.promises.readFile(contractPath, "utf-8");
819
+ } catch {
820
+ return;
821
+ }
822
+ const parsed = parseContract(contractContent, path.basename(contractPath));
823
+ if (parsed.validations.length > 0)
824
+ return;
825
+ let sourceCode;
826
+ try {
827
+ sourceCode = await fs.promises.readFile(sourcePath, "utf-8");
828
+ } catch {
829
+ return;
830
+ }
831
+ const contractName = contract.contract.replace(/\.jay-contract$/, "");
832
+ const checkResult = checkComponentPropsAndParams(
833
+ sourceCode,
834
+ {
835
+ props: parsed.val?.props,
836
+ params: parsed.val?.params
837
+ },
838
+ contractName,
839
+ contractPath,
840
+ sourcePath
841
+ );
842
+ result.errors.push(...checkResult.errors);
843
+ result.warnings.push(...checkResult.warnings);
295
844
  }
296
845
  async function validatePackageJson(context, result) {
297
846
  const packageJsonPath = path.join(context.pluginPath, "package.json");
@@ -423,5 +972,6 @@ async function validateDynamicContracts(context, result) {
423
972
  }
424
973
  }
425
974
  export {
975
+ checkComponentPropsAndParams,
426
976
  validatePlugin
427
977
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jay-framework/plugin-validator",
3
- "version": "0.15.5",
3
+ "version": "0.16.0",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "description": "Validation tool for Jay Stack plugins",
@@ -25,13 +25,15 @@
25
25
  "test:watch": ":"
26
26
  },
27
27
  "dependencies": {
28
- "@jay-framework/compiler-jay-html": "^0.15.5",
29
- "@jay-framework/editor-protocol": "^0.15.5",
28
+ "@jay-framework/compiler-jay-html": "^0.16.0",
29
+ "@jay-framework/compiler-shared": "^0.16.0",
30
+ "@jay-framework/editor-protocol": "^0.16.0",
31
+ "@jay-framework/typescript-bridge": "^0.16.0",
30
32
  "chalk": "^4.1.2",
31
33
  "yaml": "^2.3.4"
32
34
  },
33
35
  "devDependencies": {
34
- "@jay-framework/dev-environment": "^0.15.5",
36
+ "@jay-framework/dev-environment": "^0.16.0",
35
37
  "@types/node": "^22.15.21",
36
38
  "rimraf": "^5.0.5",
37
39
  "tsup": "^8.0.1",