@jay-framework/plugin-validator 0.15.4 → 0.15.6

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,111 @@ 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
- }
214
- }
215
- if (!found) {
496
+ if (manifest.services) {
497
+ if (!Array.isArray(manifest.services)) {
216
498
  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)`
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
+ }
221
523
  });
222
- return;
223
524
  }
224
- } else {
225
- contractPath = path.join(context.pluginPath, contract.contract);
226
- if (!fs.existsSync(contractPath)) {
525
+ }
526
+ if (manifest.contexts) {
527
+ if (!Array.isArray(manifest.contexts)) {
227
528
  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}`
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
+ }
232
553
  });
233
- return;
234
554
  }
235
555
  }
556
+ }
557
+ function resolveContractFile(contractSpec, context) {
558
+ if (context.isNpmPackage) {
559
+ const packageJsonPath = path.join(context.pluginPath, "package.json");
560
+ if (fs.existsSync(packageJsonPath)) {
561
+ try {
562
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
563
+ if (packageJson.exports) {
564
+ const exportKey = "./" + contractSpec;
565
+ const exportValue = packageJson.exports[exportKey];
566
+ if (exportValue) {
567
+ const resolvedPath = typeof exportValue === "string" ? exportValue : exportValue.default || exportValue.import || exportValue.require;
568
+ if (resolvedPath) {
569
+ const fullPath = path.join(context.pluginPath, resolvedPath);
570
+ if (fs.existsSync(fullPath))
571
+ return fullPath;
572
+ }
573
+ }
574
+ }
575
+ } catch {
576
+ }
577
+ }
578
+ for (const dir of ["dist", "lib", ""]) {
579
+ const candidate = path.join(context.pluginPath, dir, contractSpec);
580
+ if (fs.existsSync(candidate))
581
+ return candidate;
582
+ }
583
+ return void 0;
584
+ } else {
585
+ const candidate = path.join(context.pluginPath, contractSpec);
586
+ return fs.existsSync(candidate) ? candidate : void 0;
587
+ }
588
+ }
589
+ async function validateContract(contract, index, context, generateTypes, result) {
590
+ result.contractsChecked = (result.contractsChecked || 0) + 1;
591
+ const contractPath = resolveContractFile(contract.contract, context);
592
+ if (!contractPath) {
593
+ result.errors.push({
594
+ type: "file-missing",
595
+ message: `Contract file not found: ${contract.contract}`,
596
+ location: `plugin.yaml contracts[${index}]`,
597
+ 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)}`
598
+ });
599
+ return;
600
+ }
236
601
  try {
237
602
  const contractContent = await fs.promises.readFile(contractPath, "utf-8");
238
603
  const parsedContract = YAML.parse(contractContent);
@@ -283,6 +648,7 @@ async function validateComponent(contract, index, context, result) {
283
648
  location: `plugin.yaml contracts[${index}]`,
284
649
  suggestion: 'Component should be the exported member name (e.g., "moodTracker")'
285
650
  });
651
+ return;
286
652
  }
287
653
  if (contract.component.includes("/") || contract.component.includes(".")) {
288
654
  result.warnings.push({
@@ -292,6 +658,143 @@ async function validateComponent(contract, index, context, result) {
292
658
  suggestion: 'Component should be the exported member name (e.g., "moodTracker"), not a file path'
293
659
  });
294
660
  }
661
+ await checkComponentContractConsistency(contract, context, result);
662
+ }
663
+ function hasExportModifier(node) {
664
+ return node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
665
+ }
666
+ function resolveModulePath(basePath) {
667
+ for (const ext of ["", ".ts", ".js", "/index.ts", "/index.js"]) {
668
+ const candidate = basePath + ext;
669
+ if (fs.existsSync(candidate))
670
+ return candidate;
671
+ }
672
+ return void 0;
673
+ }
674
+ function resolveComponentSourcePath(componentName, context) {
675
+ const modulePath = context.manifest.module || "index";
676
+ const entryBase = path.join(context.pluginPath, modulePath);
677
+ const entryFile = resolveModulePath(entryBase);
678
+ const libEntryFile = !entryFile ? resolveModulePath(path.join(context.pluginPath, "lib", modulePath)) : void 0;
679
+ const sourceEntry = entryFile || libEntryFile;
680
+ if (!sourceEntry)
681
+ return void 0;
682
+ if (!sourceEntry.endsWith(".ts"))
683
+ return void 0;
684
+ let sourceCode;
685
+ try {
686
+ sourceCode = fs.readFileSync(sourceEntry, "utf-8");
687
+ } catch {
688
+ return void 0;
689
+ }
690
+ const sourceFile = ts.createSourceFile(
691
+ sourceEntry,
692
+ sourceCode,
693
+ ts.ScriptTarget.Latest,
694
+ true,
695
+ ts.ScriptKind.TS
696
+ );
697
+ const starReexportModules = [];
698
+ for (const statement of sourceFile.statements) {
699
+ if (!ts.isExportDeclaration(statement))
700
+ continue;
701
+ if (!statement.moduleSpecifier)
702
+ continue;
703
+ if (!ts.isStringLiteral(statement.moduleSpecifier))
704
+ continue;
705
+ const moduleSpec = statement.moduleSpecifier.text;
706
+ const exportClause = statement.exportClause;
707
+ if (!exportClause) {
708
+ starReexportModules.push(moduleSpec);
709
+ continue;
710
+ }
711
+ if (ts.isNamedExports(exportClause)) {
712
+ for (const element of exportClause.elements) {
713
+ const exportedName = element.name.text;
714
+ if (exportedName === componentName) {
715
+ const resolvedBase = path.resolve(path.dirname(sourceEntry), moduleSpec);
716
+ return resolveModulePath(resolvedBase);
717
+ }
718
+ }
719
+ }
720
+ }
721
+ for (const moduleSpec of starReexportModules) {
722
+ if (!moduleSpec.startsWith("."))
723
+ continue;
724
+ const resolvedBase = path.resolve(path.dirname(sourceEntry), moduleSpec);
725
+ const resolvedPath = resolveModulePath(resolvedBase);
726
+ if (!resolvedPath || !resolvedPath.endsWith(".ts"))
727
+ continue;
728
+ try {
729
+ const modSource = fs.readFileSync(resolvedPath, "utf-8");
730
+ const modFile = ts.createSourceFile(
731
+ resolvedPath,
732
+ modSource,
733
+ ts.ScriptTarget.Latest,
734
+ true,
735
+ ts.ScriptKind.TS
736
+ );
737
+ for (const stmt of modFile.statements) {
738
+ if (ts.isVariableStatement(stmt) && hasExportModifier(stmt)) {
739
+ for (const decl of stmt.declarationList.declarations) {
740
+ if (ts.isIdentifier(decl.name) && decl.name.text === componentName) {
741
+ return resolvedPath;
742
+ }
743
+ }
744
+ }
745
+ if (ts.isFunctionDeclaration(stmt) && hasExportModifier(stmt) && stmt.name?.text === componentName) {
746
+ return resolvedPath;
747
+ }
748
+ }
749
+ } catch {
750
+ continue;
751
+ }
752
+ }
753
+ return sourceEntry;
754
+ }
755
+ function resolveContractPath(contract, context) {
756
+ return resolveContractFile(contract.contract, context);
757
+ }
758
+ async function checkComponentContractConsistency(contract, context, result) {
759
+ const componentName = contract.component;
760
+ if (!componentName)
761
+ return;
762
+ const sourcePath = resolveComponentSourcePath(componentName, context);
763
+ if (!sourcePath)
764
+ return;
765
+ if (!sourcePath.endsWith(".ts"))
766
+ return;
767
+ const contractPath = resolveContractPath(contract, context);
768
+ if (!contractPath)
769
+ return;
770
+ let contractContent;
771
+ try {
772
+ contractContent = await fs.promises.readFile(contractPath, "utf-8");
773
+ } catch {
774
+ return;
775
+ }
776
+ const parsed = parseContract(contractContent, path.basename(contractPath));
777
+ if (parsed.validations.length > 0)
778
+ return;
779
+ let sourceCode;
780
+ try {
781
+ sourceCode = await fs.promises.readFile(sourcePath, "utf-8");
782
+ } catch {
783
+ return;
784
+ }
785
+ const contractName = contract.contract.replace(/\.jay-contract$/, "");
786
+ const checkResult = checkComponentPropsAndParams(
787
+ sourceCode,
788
+ {
789
+ props: parsed.val?.props,
790
+ params: parsed.val?.params
791
+ },
792
+ contractName,
793
+ contractPath,
794
+ sourcePath
795
+ );
796
+ result.errors.push(...checkResult.errors);
797
+ result.warnings.push(...checkResult.warnings);
295
798
  }
296
799
  async function validatePackageJson(context, result) {
297
800
  const packageJsonPath = path.join(context.pluginPath, "package.json");
@@ -423,5 +926,6 @@ async function validateDynamicContracts(context, result) {
423
926
  }
424
927
  }
425
928
  export {
929
+ checkComponentPropsAndParams,
426
930
  validatePlugin
427
931
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jay-framework/plugin-validator",
3
- "version": "0.15.4",
3
+ "version": "0.15.6",
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.4",
29
- "@jay-framework/editor-protocol": "^0.15.4",
28
+ "@jay-framework/compiler-jay-html": "^0.15.6",
29
+ "@jay-framework/compiler-shared": "^0.15.6",
30
+ "@jay-framework/editor-protocol": "^0.15.6",
31
+ "@jay-framework/typescript-bridge": "^0.15.6",
30
32
  "chalk": "^4.1.2",
31
33
  "yaml": "^2.3.4"
32
34
  },
33
35
  "devDependencies": {
34
- "@jay-framework/dev-environment": "^0.15.4",
36
+ "@jay-framework/dev-environment": "^0.15.6",
35
37
  "@types/node": "^22.15.21",
36
38
  "rimraf": "^5.0.5",
37
39
  "tsup": "^8.0.1",