@jay-framework/jay-stack-cli 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/agent-kit-template/{INSTRUCTIONS.md → designer/INSTRUCTIONS.md} +11 -8
- package/agent-kit-template/{contracts-and-plugins.md → designer/contracts-and-plugins.md} +1 -0
- package/agent-kit-template/{jay-html-syntax.md → designer/jay-html-components.md} +89 -158
- package/agent-kit-template/designer/jay-html-styling.md +97 -0
- package/agent-kit-template/designer/jay-html-syntax.md +44 -0
- package/agent-kit-template/designer/jay-html-template-syntax.md +203 -0
- package/agent-kit-template/developer/INSTRUCTIONS.md +34 -0
- package/agent-kit-template/developer/cli-commands.md +228 -0
- package/agent-kit-template/developer/component-data.md +109 -0
- package/agent-kit-template/developer/component-refs.md +117 -0
- package/agent-kit-template/developer/component-state.md +140 -0
- package/agent-kit-template/developer/configuration.md +76 -0
- package/agent-kit-template/developer/page-components.md +103 -0
- package/agent-kit-template/developer/page-contracts.md +114 -0
- package/agent-kit-template/developer/project-structure.md +242 -0
- package/agent-kit-template/developer/render-results.md +112 -0
- package/agent-kit-template/developer/routing.md +161 -0
- package/agent-kit-template/developer/seo-guide.md +93 -0
- package/agent-kit-template/plugin/INSTRUCTIONS.md +40 -0
- package/agent-kit-template/plugin/actions-guide.md +125 -0
- package/agent-kit-template/plugin/component-context.md +103 -0
- package/agent-kit-template/plugin/component-data.md +109 -0
- package/agent-kit-template/plugin/component-refs.md +117 -0
- package/agent-kit-template/plugin/component-state.md +140 -0
- package/agent-kit-template/plugin/component-structure.md +174 -0
- package/agent-kit-template/plugin/contracts-guide.md +193 -0
- package/agent-kit-template/plugin/plugin-structure.md +194 -0
- package/agent-kit-template/plugin/render-results.md +112 -0
- package/agent-kit-template/plugin/seo-guide.md +93 -0
- package/agent-kit-template/plugin/services-guide.md +116 -0
- package/agent-kit-template/plugin/validation.md +101 -0
- package/dist/index.js +805 -61
- package/package.json +10 -10
- /package/agent-kit-template/{cli-commands.md → designer/cli-commands.md} +0 -0
- /package/agent-kit-template/{project-structure.md → designer/project-structure.md} +0 -0
- /package/agent-kit-template/{routing.md → designer/routing.md} +0 -0
package/dist/index.js
CHANGED
|
@@ -8,12 +8,13 @@ import path from "path";
|
|
|
8
8
|
import fs, { promises } from "fs";
|
|
9
9
|
import YAML from "yaml";
|
|
10
10
|
import { getLogger, createDevLogger, setDevLogger } from "@jay-framework/logger";
|
|
11
|
-
import { parseJayFile, JAY_IMPORT_RESOLVER, generateElementDefinitionFile, ContractTagType, parseContract, generateElementFile } from "@jay-framework/compiler-jay-html";
|
|
11
|
+
import { parseJayFile, JAY_IMPORT_RESOLVER, generateElementDefinitionFile, ContractTagType, parseContract, generateElementFile, htmlElementTagNameMap } from "@jay-framework/compiler-jay-html";
|
|
12
12
|
import { JAY_CONTRACT_EXTENSION, JAY_EXTENSION, resolvePluginManifest, LOCAL_PLUGIN_PATH, JayAtomicType, JayEnumType, loadPluginManifest, RuntimeMode, GenerateTarget } from "@jay-framework/compiler-shared";
|
|
13
13
|
import { listContracts, materializeContracts } from "@jay-framework/stack-server-runtime";
|
|
14
14
|
import { listContracts as listContracts2, materializeContracts as materializeContracts2 } from "@jay-framework/stack-server-runtime";
|
|
15
15
|
import { Command } from "commander";
|
|
16
16
|
import chalk from "chalk";
|
|
17
|
+
import { createRequire } from "module";
|
|
17
18
|
import { glob } from "glob";
|
|
18
19
|
import { parse } from "node-html-parser";
|
|
19
20
|
const DEFAULT_CONFIG = {
|
|
@@ -244,7 +245,7 @@ function getCommonStyles(node) {
|
|
|
244
245
|
transformStyles.push(`rotate(${node.rotation}deg)`);
|
|
245
246
|
}
|
|
246
247
|
if (node.effects && Array.isArray(node.effects) && node.effects.length > 0) {
|
|
247
|
-
const visibleEffects = node.effects.filter((
|
|
248
|
+
const visibleEffects = node.effects.filter((e2) => e2.visible !== false).reverse();
|
|
248
249
|
const filterFunctions = [];
|
|
249
250
|
const boxShadows = [];
|
|
250
251
|
for (const effect of visibleEffects) {
|
|
@@ -410,7 +411,7 @@ function getStrokeStyles(node) {
|
|
|
410
411
|
if (!node.strokes || node.strokes.length === 0) {
|
|
411
412
|
return "";
|
|
412
413
|
}
|
|
413
|
-
const visibleStrokes = node.strokes.filter((
|
|
414
|
+
const visibleStrokes = node.strokes.filter((s2) => s2.visible !== false);
|
|
414
415
|
if (visibleStrokes.length === 0) {
|
|
415
416
|
return "";
|
|
416
417
|
}
|
|
@@ -1588,9 +1589,9 @@ function convertContractToProtocol(contract) {
|
|
|
1588
1589
|
}
|
|
1589
1590
|
async function isPageDirectory(dirPath) {
|
|
1590
1591
|
const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
|
|
1591
|
-
const hasPageHtml = entries.some((
|
|
1592
|
-
const hasPageContract = entries.some((
|
|
1593
|
-
const hasPageConfig = entries.some((
|
|
1592
|
+
const hasPageHtml = entries.some((e2) => e2.name === PAGE_FILENAME);
|
|
1593
|
+
const hasPageContract = entries.some((e2) => e2.name === PAGE_CONTRACT_FILENAME);
|
|
1594
|
+
const hasPageConfig = entries.some((e2) => e2.name === PAGE_CONFIG_FILENAME);
|
|
1594
1595
|
const isPage = hasPageHtml || hasPageContract || hasPageConfig;
|
|
1595
1596
|
return { isPage, hasPageHtml, hasPageContract, hasPageConfig };
|
|
1596
1597
|
}
|
|
@@ -2155,7 +2156,7 @@ function createEditorHandlers(config, tsConfigPath, projectRoot) {
|
|
|
2155
2156
|
}
|
|
2156
2157
|
return {
|
|
2157
2158
|
type: "publish",
|
|
2158
|
-
success: status.every((
|
|
2159
|
+
success: status.every((s2) => s2.success),
|
|
2159
2160
|
status
|
|
2160
2161
|
};
|
|
2161
2162
|
};
|
|
@@ -2511,6 +2512,280 @@ async function startDevServer(options = {}) {
|
|
|
2511
2512
|
process.on("SIGTERM", shutdown);
|
|
2512
2513
|
process.on("SIGINT", shutdown);
|
|
2513
2514
|
}
|
|
2515
|
+
const s = createRequire(import.meta.url), e = s("typescript"), u = e;
|
|
2516
|
+
new Proxy(e, {
|
|
2517
|
+
get(t, r) {
|
|
2518
|
+
return t[r];
|
|
2519
|
+
}
|
|
2520
|
+
});
|
|
2521
|
+
const FRAMEWORK_PROP_TYPES = /* @__PURE__ */ new Set(["PageProps", "RequestQuery"]);
|
|
2522
|
+
function checkComponentPropsAndParams(sourceCode, contract, contractName, contractPath, sourcePath) {
|
|
2523
|
+
const errors = [];
|
|
2524
|
+
const warnings = [];
|
|
2525
|
+
const sourceFile = u.createSourceFile(
|
|
2526
|
+
sourcePath,
|
|
2527
|
+
sourceCode,
|
|
2528
|
+
u.ScriptTarget.Latest,
|
|
2529
|
+
true,
|
|
2530
|
+
u.ScriptKind.TS
|
|
2531
|
+
);
|
|
2532
|
+
const localInterfaces = collectLocalInterfaces(sourceFile);
|
|
2533
|
+
const contractImportedTypes = collectContractImportedTypes(sourceFile);
|
|
2534
|
+
const builderInfo = analyzeBuilderChains(sourceFile);
|
|
2535
|
+
for (const propsTypeName of builderInfo.propsTypeNames) {
|
|
2536
|
+
checkPropsConsistency(
|
|
2537
|
+
propsTypeName,
|
|
2538
|
+
localInterfaces,
|
|
2539
|
+
contractImportedTypes,
|
|
2540
|
+
contract,
|
|
2541
|
+
contractName,
|
|
2542
|
+
contractPath,
|
|
2543
|
+
sourcePath,
|
|
2544
|
+
errors,
|
|
2545
|
+
warnings
|
|
2546
|
+
);
|
|
2547
|
+
}
|
|
2548
|
+
if (builderInfo.hasLoadParams) {
|
|
2549
|
+
checkParamsConsistency(
|
|
2550
|
+
builderInfo.paramsTypeNames,
|
|
2551
|
+
localInterfaces,
|
|
2552
|
+
contractImportedTypes,
|
|
2553
|
+
contract,
|
|
2554
|
+
contractName,
|
|
2555
|
+
contractPath,
|
|
2556
|
+
sourcePath,
|
|
2557
|
+
errors,
|
|
2558
|
+
warnings
|
|
2559
|
+
);
|
|
2560
|
+
}
|
|
2561
|
+
return { errors, warnings };
|
|
2562
|
+
}
|
|
2563
|
+
function collectLocalInterfaces(sourceFile) {
|
|
2564
|
+
const interfaces = /* @__PURE__ */ new Map();
|
|
2565
|
+
for (const statement of sourceFile.statements) {
|
|
2566
|
+
if (u.isInterfaceDeclaration(statement)) {
|
|
2567
|
+
const name = statement.name.text;
|
|
2568
|
+
const properties = [];
|
|
2569
|
+
for (const member of statement.members) {
|
|
2570
|
+
if (u.isPropertySignature(member) && member.name) {
|
|
2571
|
+
if (u.isIdentifier(member.name)) {
|
|
2572
|
+
properties.push(member.name.text);
|
|
2573
|
+
} else if (u.isStringLiteral(member.name)) {
|
|
2574
|
+
properties.push(member.name.text);
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
const extendsTypes = [];
|
|
2579
|
+
if (statement.heritageClauses) {
|
|
2580
|
+
for (const clause of statement.heritageClauses) {
|
|
2581
|
+
for (const type of clause.types) {
|
|
2582
|
+
if (u.isIdentifier(type.expression)) {
|
|
2583
|
+
extendsTypes.push(type.expression.text);
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
interfaces.set(name, { name, properties, extendsTypes });
|
|
2589
|
+
}
|
|
2590
|
+
if (u.isTypeAliasDeclaration(statement)) {
|
|
2591
|
+
const name = statement.name.text;
|
|
2592
|
+
if (u.isTypeLiteralNode(statement.type)) {
|
|
2593
|
+
const properties = [];
|
|
2594
|
+
for (const member of statement.type.members) {
|
|
2595
|
+
if (u.isPropertySignature(member) && member.name) {
|
|
2596
|
+
if (u.isIdentifier(member.name)) {
|
|
2597
|
+
properties.push(member.name.text);
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
interfaces.set(name, { name, properties, extendsTypes: [] });
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
return interfaces;
|
|
2606
|
+
}
|
|
2607
|
+
function collectContractImportedTypes(sourceFile) {
|
|
2608
|
+
const contractTypes = /* @__PURE__ */ new Set();
|
|
2609
|
+
for (const statement of sourceFile.statements) {
|
|
2610
|
+
if (!u.isImportDeclaration(statement))
|
|
2611
|
+
continue;
|
|
2612
|
+
const moduleSpecifier = statement.moduleSpecifier;
|
|
2613
|
+
if (!u.isStringLiteral(moduleSpecifier))
|
|
2614
|
+
continue;
|
|
2615
|
+
const modulePath = moduleSpecifier.text;
|
|
2616
|
+
if (!modulePath.includes(".jay-contract"))
|
|
2617
|
+
continue;
|
|
2618
|
+
const importClause = statement.importClause;
|
|
2619
|
+
if (!importClause)
|
|
2620
|
+
continue;
|
|
2621
|
+
const namedBindings = importClause.namedBindings;
|
|
2622
|
+
if (namedBindings && u.isNamedImports(namedBindings)) {
|
|
2623
|
+
for (const element of namedBindings.elements) {
|
|
2624
|
+
contractTypes.add(element.name.text);
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
return contractTypes;
|
|
2629
|
+
}
|
|
2630
|
+
function analyzeBuilderChains(sourceFile) {
|
|
2631
|
+
const result = {
|
|
2632
|
+
propsTypeNames: [],
|
|
2633
|
+
hasLoadParams: false,
|
|
2634
|
+
paramsTypeNames: []
|
|
2635
|
+
};
|
|
2636
|
+
visitNode(sourceFile, result);
|
|
2637
|
+
return result;
|
|
2638
|
+
}
|
|
2639
|
+
function visitNode(node, result) {
|
|
2640
|
+
if (u.isCallExpression(node)) {
|
|
2641
|
+
const expr = node.expression;
|
|
2642
|
+
if (u.isPropertyAccessExpression(expr) && expr.name.text === "withProps") {
|
|
2643
|
+
if (node.typeArguments && node.typeArguments.length > 0) {
|
|
2644
|
+
const typeNames = extractTypeNames(node.typeArguments[0]);
|
|
2645
|
+
result.propsTypeNames.push(...typeNames);
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
if (u.isPropertyAccessExpression(expr) && expr.name.text === "withLoadParams") {
|
|
2649
|
+
result.hasLoadParams = true;
|
|
2650
|
+
if (node.typeArguments && node.typeArguments.length > 0) {
|
|
2651
|
+
const typeNames = extractTypeNames(node.typeArguments[0]);
|
|
2652
|
+
result.paramsTypeNames.push(...typeNames);
|
|
2653
|
+
}
|
|
2654
|
+
if (node.arguments.length > 0) {
|
|
2655
|
+
const arg = node.arguments[0];
|
|
2656
|
+
if (u.isIdentifier(arg))
|
|
2657
|
+
;
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
u.forEachChild(node, (child) => visitNode(child, result));
|
|
2662
|
+
}
|
|
2663
|
+
function extractTypeNames(typeNode) {
|
|
2664
|
+
if (u.isTypeReferenceNode(typeNode)) {
|
|
2665
|
+
if (u.isIdentifier(typeNode.typeName)) {
|
|
2666
|
+
return [typeNode.typeName.text];
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
if (u.isIntersectionTypeNode(typeNode)) {
|
|
2670
|
+
const names = [];
|
|
2671
|
+
for (const member of typeNode.types) {
|
|
2672
|
+
names.push(...extractTypeNames(member));
|
|
2673
|
+
}
|
|
2674
|
+
return names;
|
|
2675
|
+
}
|
|
2676
|
+
return [];
|
|
2677
|
+
}
|
|
2678
|
+
function checkPropsConsistency(propsTypeName, localInterfaces, contractImportedTypes, contract, contractName, contractPath, sourcePath, errors, warnings) {
|
|
2679
|
+
if (FRAMEWORK_PROP_TYPES.has(propsTypeName))
|
|
2680
|
+
return;
|
|
2681
|
+
if (contractImportedTypes.has(propsTypeName))
|
|
2682
|
+
return;
|
|
2683
|
+
const prefix = `[${contractName}]`;
|
|
2684
|
+
const iface = localInterfaces.get(propsTypeName);
|
|
2685
|
+
if (!iface) {
|
|
2686
|
+
if (!contract.props || contract.props.length === 0) {
|
|
2687
|
+
errors.push({
|
|
2688
|
+
type: "contract-invalid",
|
|
2689
|
+
message: `${prefix} component uses .withProps<${propsTypeName}>() but the contract does not declare any props`,
|
|
2690
|
+
location: contractPath,
|
|
2691
|
+
suggestion: `Add a props section to the contract`
|
|
2692
|
+
});
|
|
2693
|
+
}
|
|
2694
|
+
return;
|
|
2695
|
+
}
|
|
2696
|
+
const ownProperties = iface.properties;
|
|
2697
|
+
if (ownProperties.length === 0)
|
|
2698
|
+
return;
|
|
2699
|
+
if (!contract.props || contract.props.length === 0) {
|
|
2700
|
+
errors.push({
|
|
2701
|
+
type: "contract-invalid",
|
|
2702
|
+
message: `${prefix} component uses .withProps<${propsTypeName}>() with properties [${ownProperties.join(", ")}] but the contract does not declare any props`,
|
|
2703
|
+
location: contractPath,
|
|
2704
|
+
suggestion: `Add to contract: props:
|
|
2705
|
+
` + ownProperties.map((p) => ` - name: ${p}
|
|
2706
|
+
type: string`).join("\n")
|
|
2707
|
+
});
|
|
2708
|
+
return;
|
|
2709
|
+
}
|
|
2710
|
+
const contractPropNames = new Set(contract.props.map((p) => p.name));
|
|
2711
|
+
for (const prop of ownProperties) {
|
|
2712
|
+
if (!contractPropNames.has(prop)) {
|
|
2713
|
+
errors.push({
|
|
2714
|
+
type: "contract-invalid",
|
|
2715
|
+
message: `${prefix} component prop "${prop}" (from ${propsTypeName}) is not declared in the contract`,
|
|
2716
|
+
location: contractPath,
|
|
2717
|
+
suggestion: `Add to contract props: - name: ${prop}`
|
|
2718
|
+
});
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
for (const contractProp of contract.props) {
|
|
2722
|
+
if (!ownProperties.includes(contractProp.name)) {
|
|
2723
|
+
warnings.push({
|
|
2724
|
+
type: "contract-invalid",
|
|
2725
|
+
message: `${prefix} contract declares prop "${contractProp.name}" but the component's ${propsTypeName} interface does not include it`,
|
|
2726
|
+
location: sourcePath,
|
|
2727
|
+
suggestion: `Add "${contractProp.name}" to the ${propsTypeName} interface, or remove it from the contract`
|
|
2728
|
+
});
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
function checkParamsConsistency(paramsTypeNames, localInterfaces, contractImportedTypes, contract, contractName, contractPath, sourcePath, errors, warnings) {
|
|
2733
|
+
const prefix = `[${contractName}]`;
|
|
2734
|
+
if (!contract.params || contract.params.length === 0) {
|
|
2735
|
+
errors.push({
|
|
2736
|
+
type: "contract-invalid",
|
|
2737
|
+
message: `${prefix} component uses .withLoadParams() but the contract does not declare any params`,
|
|
2738
|
+
location: contractPath,
|
|
2739
|
+
suggestion: `Add a params section to the contract (e.g., params: { slug: string })`
|
|
2740
|
+
});
|
|
2741
|
+
for (const typeName of paramsTypeNames) {
|
|
2742
|
+
if (FRAMEWORK_PROP_TYPES.has(typeName))
|
|
2743
|
+
continue;
|
|
2744
|
+
if (contractImportedTypes.has(typeName))
|
|
2745
|
+
continue;
|
|
2746
|
+
const iface = localInterfaces.get(typeName);
|
|
2747
|
+
if (iface) {
|
|
2748
|
+
const ownProps = iface.properties;
|
|
2749
|
+
if (ownProps.length > 0) {
|
|
2750
|
+
errors[errors.length - 1].suggestion = `Add to contract: params:
|
|
2751
|
+
` + ownProps.map((p) => ` ${p}: string`).join("\n");
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
return;
|
|
2756
|
+
}
|
|
2757
|
+
for (const typeName of paramsTypeNames) {
|
|
2758
|
+
if (FRAMEWORK_PROP_TYPES.has(typeName))
|
|
2759
|
+
continue;
|
|
2760
|
+
if (contractImportedTypes.has(typeName))
|
|
2761
|
+
continue;
|
|
2762
|
+
const iface = localInterfaces.get(typeName);
|
|
2763
|
+
if (!iface)
|
|
2764
|
+
continue;
|
|
2765
|
+
const ownProperties = iface.properties;
|
|
2766
|
+
const contractParamNames = new Set(contract.params.map((p) => p.name));
|
|
2767
|
+
for (const prop of ownProperties) {
|
|
2768
|
+
if (!contractParamNames.has(prop)) {
|
|
2769
|
+
errors.push({
|
|
2770
|
+
type: "contract-invalid",
|
|
2771
|
+
message: `${prefix} component param "${prop}" (from ${typeName}) is not declared in the contract`,
|
|
2772
|
+
location: contractPath,
|
|
2773
|
+
suggestion: `Add to contract params: ${prop}: string`
|
|
2774
|
+
});
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
for (const contractParam of contract.params) {
|
|
2778
|
+
if (!ownProperties.includes(contractParam.name)) {
|
|
2779
|
+
warnings.push({
|
|
2780
|
+
type: "contract-invalid",
|
|
2781
|
+
message: `${prefix} contract declares param "${contractParam.name}" but the component's ${typeName} interface does not include it`,
|
|
2782
|
+
location: sourcePath,
|
|
2783
|
+
suggestion: `Add "${contractParam.name}" to the ${typeName} interface, or remove it from the contract`
|
|
2784
|
+
});
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2514
2789
|
async function validatePlugin(options = {}) {
|
|
2515
2790
|
const pluginPath = options.pluginPath || process.cwd();
|
|
2516
2791
|
if (options.local) {
|
|
@@ -2612,6 +2887,36 @@ async function validateLocalPlugins(projectPath, options) {
|
|
|
2612
2887
|
typesGenerated: allResults.reduce((sum, r) => sum + (r.typesGenerated || 0), 0)
|
|
2613
2888
|
};
|
|
2614
2889
|
}
|
|
2890
|
+
function validateDocFile(docPath, label, context, result) {
|
|
2891
|
+
const resolvedPath = path.join(context.pluginPath, docPath);
|
|
2892
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
2893
|
+
result.errors.push({
|
|
2894
|
+
type: "file-missing",
|
|
2895
|
+
message: `Doc file for ${label} not found: ${docPath}`,
|
|
2896
|
+
location: "plugin.yaml",
|
|
2897
|
+
suggestion: `Create the documentation file at ${resolvedPath}`
|
|
2898
|
+
});
|
|
2899
|
+
return;
|
|
2900
|
+
}
|
|
2901
|
+
if (context.isNpmPackage) {
|
|
2902
|
+
const packageJsonPath = path.join(context.pluginPath, "package.json");
|
|
2903
|
+
try {
|
|
2904
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
2905
|
+
if (packageJson.exports) {
|
|
2906
|
+
const exportKey = "./" + docPath.replace(/^\.\//, "");
|
|
2907
|
+
if (!packageJson.exports[exportKey]) {
|
|
2908
|
+
result.errors.push({
|
|
2909
|
+
type: "export-mismatch",
|
|
2910
|
+
message: `Doc file for ${label} is not exported in package.json: ${docPath}`,
|
|
2911
|
+
location: packageJsonPath,
|
|
2912
|
+
suggestion: `Add "${exportKey}": "${docPath}" to the exports field`
|
|
2913
|
+
});
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
} catch {
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2615
2920
|
async function validateSchema(context, result) {
|
|
2616
2921
|
const { manifest } = context;
|
|
2617
2922
|
if (!manifest.name) {
|
|
@@ -2702,45 +3007,110 @@ async function validateSchema(context, result) {
|
|
|
2702
3007
|
suggestion: 'Add either "contracts" or "dynamic_contracts" to expose functionality'
|
|
2703
3008
|
});
|
|
2704
3009
|
}
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
result.contractsChecked = (result.contractsChecked || 0) + 1;
|
|
2708
|
-
let contractPath;
|
|
2709
|
-
if (context.isNpmPackage) {
|
|
2710
|
-
const contractSpec = contract.contract;
|
|
2711
|
-
const possiblePaths = [
|
|
2712
|
-
path.join(context.pluginPath, "dist", contractSpec),
|
|
2713
|
-
path.join(context.pluginPath, "lib", contractSpec),
|
|
2714
|
-
path.join(context.pluginPath, contractSpec)
|
|
2715
|
-
];
|
|
2716
|
-
let found = false;
|
|
2717
|
-
for (const possiblePath of possiblePaths) {
|
|
2718
|
-
if (fs.existsSync(possiblePath)) {
|
|
2719
|
-
contractPath = possiblePath;
|
|
2720
|
-
found = true;
|
|
2721
|
-
break;
|
|
2722
|
-
}
|
|
2723
|
-
}
|
|
2724
|
-
if (!found) {
|
|
3010
|
+
if (manifest.services) {
|
|
3011
|
+
if (!Array.isArray(manifest.services)) {
|
|
2725
3012
|
result.errors.push({
|
|
2726
|
-
type: "
|
|
2727
|
-
message:
|
|
2728
|
-
location:
|
|
2729
|
-
|
|
3013
|
+
type: "schema",
|
|
3014
|
+
message: 'Field "services" must be an array',
|
|
3015
|
+
location: "plugin.yaml"
|
|
3016
|
+
});
|
|
3017
|
+
} else {
|
|
3018
|
+
manifest.services.forEach((service, index) => {
|
|
3019
|
+
if (!service.name) {
|
|
3020
|
+
result.errors.push({
|
|
3021
|
+
type: "schema",
|
|
3022
|
+
message: `Service at index ${index} is missing "name" field`,
|
|
3023
|
+
location: "plugin.yaml"
|
|
3024
|
+
});
|
|
3025
|
+
}
|
|
3026
|
+
if (!service.marker) {
|
|
3027
|
+
result.errors.push({
|
|
3028
|
+
type: "schema",
|
|
3029
|
+
message: `Service "${service.name || index}" is missing "marker" field`,
|
|
3030
|
+
location: "plugin.yaml",
|
|
3031
|
+
suggestion: "Specify the exported service marker constant name"
|
|
3032
|
+
});
|
|
3033
|
+
}
|
|
3034
|
+
if (service.doc) {
|
|
3035
|
+
validateDocFile(service.doc, `service "${service.name}"`, context, result);
|
|
3036
|
+
}
|
|
2730
3037
|
});
|
|
2731
|
-
return;
|
|
2732
3038
|
}
|
|
2733
|
-
}
|
|
2734
|
-
|
|
2735
|
-
if (!
|
|
3039
|
+
}
|
|
3040
|
+
if (manifest.contexts) {
|
|
3041
|
+
if (!Array.isArray(manifest.contexts)) {
|
|
2736
3042
|
result.errors.push({
|
|
2737
|
-
type: "
|
|
2738
|
-
message:
|
|
2739
|
-
location:
|
|
2740
|
-
suggestion: `Create the contract file at ${contractPath}`
|
|
3043
|
+
type: "schema",
|
|
3044
|
+
message: 'Field "contexts" must be an array',
|
|
3045
|
+
location: "plugin.yaml"
|
|
2741
3046
|
});
|
|
2742
|
-
|
|
3047
|
+
} else {
|
|
3048
|
+
manifest.contexts.forEach((ctx, index) => {
|
|
3049
|
+
if (!ctx.name) {
|
|
3050
|
+
result.errors.push({
|
|
3051
|
+
type: "schema",
|
|
3052
|
+
message: `Context at index ${index} is missing "name" field`,
|
|
3053
|
+
location: "plugin.yaml"
|
|
3054
|
+
});
|
|
3055
|
+
}
|
|
3056
|
+
if (!ctx.marker) {
|
|
3057
|
+
result.errors.push({
|
|
3058
|
+
type: "schema",
|
|
3059
|
+
message: `Context "${ctx.name || index}" is missing "marker" field`,
|
|
3060
|
+
location: "plugin.yaml",
|
|
3061
|
+
suggestion: "Specify the exported context marker constant name"
|
|
3062
|
+
});
|
|
3063
|
+
}
|
|
3064
|
+
if (ctx.doc) {
|
|
3065
|
+
validateDocFile(ctx.doc, `context "${ctx.name}"`, context, result);
|
|
3066
|
+
}
|
|
3067
|
+
});
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
function resolveContractFile(contractSpec, context) {
|
|
3072
|
+
if (context.isNpmPackage) {
|
|
3073
|
+
const packageJsonPath = path.join(context.pluginPath, "package.json");
|
|
3074
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
3075
|
+
try {
|
|
3076
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
3077
|
+
if (packageJson.exports) {
|
|
3078
|
+
const exportKey = "./" + contractSpec;
|
|
3079
|
+
const exportValue = packageJson.exports[exportKey];
|
|
3080
|
+
if (exportValue) {
|
|
3081
|
+
const resolvedPath = typeof exportValue === "string" ? exportValue : exportValue.default || exportValue.import || exportValue.require;
|
|
3082
|
+
if (resolvedPath) {
|
|
3083
|
+
const fullPath = path.join(context.pluginPath, resolvedPath);
|
|
3084
|
+
if (fs.existsSync(fullPath))
|
|
3085
|
+
return fullPath;
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
} catch {
|
|
3090
|
+
}
|
|
2743
3091
|
}
|
|
3092
|
+
for (const dir of ["dist", "lib", ""]) {
|
|
3093
|
+
const candidate = path.join(context.pluginPath, dir, contractSpec);
|
|
3094
|
+
if (fs.existsSync(candidate))
|
|
3095
|
+
return candidate;
|
|
3096
|
+
}
|
|
3097
|
+
return void 0;
|
|
3098
|
+
} else {
|
|
3099
|
+
const candidate = path.join(context.pluginPath, contractSpec);
|
|
3100
|
+
return fs.existsSync(candidate) ? candidate : void 0;
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
async function validateContract(contract, index, context, generateTypes, result) {
|
|
3104
|
+
result.contractsChecked = (result.contractsChecked || 0) + 1;
|
|
3105
|
+
const contractPath = resolveContractFile(contract.contract, context);
|
|
3106
|
+
if (!contractPath) {
|
|
3107
|
+
result.errors.push({
|
|
3108
|
+
type: "file-missing",
|
|
3109
|
+
message: `Contract file not found: ${contract.contract}`,
|
|
3110
|
+
location: `plugin.yaml contracts[${index}]`,
|
|
3111
|
+
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)}`
|
|
3112
|
+
});
|
|
3113
|
+
return;
|
|
2744
3114
|
}
|
|
2745
3115
|
try {
|
|
2746
3116
|
const contractContent = await fs.promises.readFile(contractPath, "utf-8");
|
|
@@ -2792,6 +3162,7 @@ async function validateComponent(contract, index, context, result) {
|
|
|
2792
3162
|
location: `plugin.yaml contracts[${index}]`,
|
|
2793
3163
|
suggestion: 'Component should be the exported member name (e.g., "moodTracker")'
|
|
2794
3164
|
});
|
|
3165
|
+
return;
|
|
2795
3166
|
}
|
|
2796
3167
|
if (contract.component.includes("/") || contract.component.includes(".")) {
|
|
2797
3168
|
result.warnings.push({
|
|
@@ -2801,6 +3172,143 @@ async function validateComponent(contract, index, context, result) {
|
|
|
2801
3172
|
suggestion: 'Component should be the exported member name (e.g., "moodTracker"), not a file path'
|
|
2802
3173
|
});
|
|
2803
3174
|
}
|
|
3175
|
+
await checkComponentContractConsistency(contract, context, result);
|
|
3176
|
+
}
|
|
3177
|
+
function hasExportModifier(node) {
|
|
3178
|
+
return node.modifiers?.some((m) => m.kind === u.SyntaxKind.ExportKeyword) ?? false;
|
|
3179
|
+
}
|
|
3180
|
+
function resolveModulePath(basePath) {
|
|
3181
|
+
for (const ext of ["", ".ts", ".js", "/index.ts", "/index.js"]) {
|
|
3182
|
+
const candidate = basePath + ext;
|
|
3183
|
+
if (fs.existsSync(candidate))
|
|
3184
|
+
return candidate;
|
|
3185
|
+
}
|
|
3186
|
+
return void 0;
|
|
3187
|
+
}
|
|
3188
|
+
function resolveComponentSourcePath(componentName, context) {
|
|
3189
|
+
const modulePath = context.manifest.module || "index";
|
|
3190
|
+
const entryBase = path.join(context.pluginPath, modulePath);
|
|
3191
|
+
const entryFile = resolveModulePath(entryBase);
|
|
3192
|
+
const libEntryFile = !entryFile ? resolveModulePath(path.join(context.pluginPath, "lib", modulePath)) : void 0;
|
|
3193
|
+
const sourceEntry = entryFile || libEntryFile;
|
|
3194
|
+
if (!sourceEntry)
|
|
3195
|
+
return void 0;
|
|
3196
|
+
if (!sourceEntry.endsWith(".ts"))
|
|
3197
|
+
return void 0;
|
|
3198
|
+
let sourceCode;
|
|
3199
|
+
try {
|
|
3200
|
+
sourceCode = fs.readFileSync(sourceEntry, "utf-8");
|
|
3201
|
+
} catch {
|
|
3202
|
+
return void 0;
|
|
3203
|
+
}
|
|
3204
|
+
const sourceFile = u.createSourceFile(
|
|
3205
|
+
sourceEntry,
|
|
3206
|
+
sourceCode,
|
|
3207
|
+
u.ScriptTarget.Latest,
|
|
3208
|
+
true,
|
|
3209
|
+
u.ScriptKind.TS
|
|
3210
|
+
);
|
|
3211
|
+
const starReexportModules = [];
|
|
3212
|
+
for (const statement of sourceFile.statements) {
|
|
3213
|
+
if (!u.isExportDeclaration(statement))
|
|
3214
|
+
continue;
|
|
3215
|
+
if (!statement.moduleSpecifier)
|
|
3216
|
+
continue;
|
|
3217
|
+
if (!u.isStringLiteral(statement.moduleSpecifier))
|
|
3218
|
+
continue;
|
|
3219
|
+
const moduleSpec = statement.moduleSpecifier.text;
|
|
3220
|
+
const exportClause = statement.exportClause;
|
|
3221
|
+
if (!exportClause) {
|
|
3222
|
+
starReexportModules.push(moduleSpec);
|
|
3223
|
+
continue;
|
|
3224
|
+
}
|
|
3225
|
+
if (u.isNamedExports(exportClause)) {
|
|
3226
|
+
for (const element of exportClause.elements) {
|
|
3227
|
+
const exportedName = element.name.text;
|
|
3228
|
+
if (exportedName === componentName) {
|
|
3229
|
+
const resolvedBase = path.resolve(path.dirname(sourceEntry), moduleSpec);
|
|
3230
|
+
return resolveModulePath(resolvedBase);
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
for (const moduleSpec of starReexportModules) {
|
|
3236
|
+
if (!moduleSpec.startsWith("."))
|
|
3237
|
+
continue;
|
|
3238
|
+
const resolvedBase = path.resolve(path.dirname(sourceEntry), moduleSpec);
|
|
3239
|
+
const resolvedPath = resolveModulePath(resolvedBase);
|
|
3240
|
+
if (!resolvedPath || !resolvedPath.endsWith(".ts"))
|
|
3241
|
+
continue;
|
|
3242
|
+
try {
|
|
3243
|
+
const modSource = fs.readFileSync(resolvedPath, "utf-8");
|
|
3244
|
+
const modFile = u.createSourceFile(
|
|
3245
|
+
resolvedPath,
|
|
3246
|
+
modSource,
|
|
3247
|
+
u.ScriptTarget.Latest,
|
|
3248
|
+
true,
|
|
3249
|
+
u.ScriptKind.TS
|
|
3250
|
+
);
|
|
3251
|
+
for (const stmt of modFile.statements) {
|
|
3252
|
+
if (u.isVariableStatement(stmt) && hasExportModifier(stmt)) {
|
|
3253
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
3254
|
+
if (u.isIdentifier(decl.name) && decl.name.text === componentName) {
|
|
3255
|
+
return resolvedPath;
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
if (u.isFunctionDeclaration(stmt) && hasExportModifier(stmt) && stmt.name?.text === componentName) {
|
|
3260
|
+
return resolvedPath;
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
} catch {
|
|
3264
|
+
continue;
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
return sourceEntry;
|
|
3268
|
+
}
|
|
3269
|
+
function resolveContractPath(contract, context) {
|
|
3270
|
+
return resolveContractFile(contract.contract, context);
|
|
3271
|
+
}
|
|
3272
|
+
async function checkComponentContractConsistency(contract, context, result) {
|
|
3273
|
+
const componentName = contract.component;
|
|
3274
|
+
if (!componentName)
|
|
3275
|
+
return;
|
|
3276
|
+
const sourcePath = resolveComponentSourcePath(componentName, context);
|
|
3277
|
+
if (!sourcePath)
|
|
3278
|
+
return;
|
|
3279
|
+
if (!sourcePath.endsWith(".ts"))
|
|
3280
|
+
return;
|
|
3281
|
+
const contractPath = resolveContractPath(contract, context);
|
|
3282
|
+
if (!contractPath)
|
|
3283
|
+
return;
|
|
3284
|
+
let contractContent;
|
|
3285
|
+
try {
|
|
3286
|
+
contractContent = await fs.promises.readFile(contractPath, "utf-8");
|
|
3287
|
+
} catch {
|
|
3288
|
+
return;
|
|
3289
|
+
}
|
|
3290
|
+
const parsed = parseContract(contractContent, path.basename(contractPath));
|
|
3291
|
+
if (parsed.validations.length > 0)
|
|
3292
|
+
return;
|
|
3293
|
+
let sourceCode;
|
|
3294
|
+
try {
|
|
3295
|
+
sourceCode = await fs.promises.readFile(sourcePath, "utf-8");
|
|
3296
|
+
} catch {
|
|
3297
|
+
return;
|
|
3298
|
+
}
|
|
3299
|
+
const contractName = contract.contract.replace(/\.jay-contract$/, "");
|
|
3300
|
+
const checkResult = checkComponentPropsAndParams(
|
|
3301
|
+
sourceCode,
|
|
3302
|
+
{
|
|
3303
|
+
props: parsed.val?.props,
|
|
3304
|
+
params: parsed.val?.params
|
|
3305
|
+
},
|
|
3306
|
+
contractName,
|
|
3307
|
+
contractPath,
|
|
3308
|
+
sourcePath
|
|
3309
|
+
);
|
|
3310
|
+
result.errors.push(...checkResult.errors);
|
|
3311
|
+
result.warnings.push(...checkResult.warnings);
|
|
2804
3312
|
}
|
|
2805
3313
|
async function validatePackageJson(context, result) {
|
|
2806
3314
|
const packageJsonPath = path.join(context.pluginPath, "package.json");
|
|
@@ -3129,6 +3637,128 @@ function analyzeTagCoverage(jayHtml, file) {
|
|
|
3129
3637
|
}
|
|
3130
3638
|
return { file, contracts };
|
|
3131
3639
|
}
|
|
3640
|
+
const htmlTagNameMap = htmlElementTagNameMap;
|
|
3641
|
+
function resolveContractTag(contract, tagPath) {
|
|
3642
|
+
const segments = tagPath.split(".");
|
|
3643
|
+
let tags = contract.tags;
|
|
3644
|
+
for (let i = 0; i < segments.length; i++) {
|
|
3645
|
+
const tag = tags.find((t) => t.tag === segments[i]);
|
|
3646
|
+
if (!tag)
|
|
3647
|
+
return void 0;
|
|
3648
|
+
if (i === segments.length - 1)
|
|
3649
|
+
return tag;
|
|
3650
|
+
if (!tag.tags)
|
|
3651
|
+
return void 0;
|
|
3652
|
+
tags = tag.tags;
|
|
3653
|
+
}
|
|
3654
|
+
return void 0;
|
|
3655
|
+
}
|
|
3656
|
+
function checkRefElementTypes(jayHtml, file) {
|
|
3657
|
+
const imports = jayHtml.headlessImports;
|
|
3658
|
+
const keyMap = /* @__PURE__ */ new Map();
|
|
3659
|
+
for (let i = 0; i < imports.length; i++) {
|
|
3660
|
+
if (imports[i].key) {
|
|
3661
|
+
keyMap.set(imports[i].key, i);
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
const warnings = [];
|
|
3665
|
+
function checkRef(ref, scopes) {
|
|
3666
|
+
const refPath = ref.refPath;
|
|
3667
|
+
const dot = refPath.indexOf(".");
|
|
3668
|
+
let importIndex;
|
|
3669
|
+
let tagPath;
|
|
3670
|
+
if (dot !== -1) {
|
|
3671
|
+
const key = refPath.substring(0, dot);
|
|
3672
|
+
const idx = keyMap.get(key);
|
|
3673
|
+
if (idx !== void 0) {
|
|
3674
|
+
importIndex = idx;
|
|
3675
|
+
tagPath = refPath.substring(dot + 1);
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
if (importIndex === void 0 && scopes.length > 0) {
|
|
3679
|
+
const scope = scopes[scopes.length - 1];
|
|
3680
|
+
importIndex = scope.importIndex;
|
|
3681
|
+
tagPath = scope.prefix ? `${scope.prefix}.${refPath}` : refPath;
|
|
3682
|
+
}
|
|
3683
|
+
if (importIndex === void 0)
|
|
3684
|
+
return;
|
|
3685
|
+
const imp = imports[importIndex];
|
|
3686
|
+
if (!imp.contract)
|
|
3687
|
+
return;
|
|
3688
|
+
const contractTag = resolveContractTag(imp.contract, tagPath);
|
|
3689
|
+
if (!contractTag || !contractTag.elementType)
|
|
3690
|
+
return;
|
|
3691
|
+
const contractTypes = contractTag.elementType;
|
|
3692
|
+
if (contractTypes.includes("HTMLElement"))
|
|
3693
|
+
return;
|
|
3694
|
+
if (!contractTypes.includes(ref.actualType)) {
|
|
3695
|
+
const label = imp.key ? `${imp.key}.${tagPath}` : tagPath;
|
|
3696
|
+
warnings.push(
|
|
3697
|
+
`Ref "${label}" is on a <${ref.htmlTag}> (${ref.actualType}) but the contract declares ${contractTypes.join(" | ")}`
|
|
3698
|
+
);
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
3701
|
+
function walkElement(element, scopes) {
|
|
3702
|
+
const tagName = element.rawTagName?.toLowerCase();
|
|
3703
|
+
let childScopes = scopes;
|
|
3704
|
+
if (tagName?.startsWith("jay:")) {
|
|
3705
|
+
const contractName = tagName.substring(4);
|
|
3706
|
+
const idx = imports.findIndex(
|
|
3707
|
+
(imp) => imp.contractName === contractName && imp.contract
|
|
3708
|
+
);
|
|
3709
|
+
if (idx !== -1) {
|
|
3710
|
+
childScopes = [...scopes, { importIndex: idx, prefix: "" }];
|
|
3711
|
+
}
|
|
3712
|
+
}
|
|
3713
|
+
const forEachVal = element.getAttribute?.("forEach");
|
|
3714
|
+
if (forEachVal) {
|
|
3715
|
+
const fePath = extractTagPath(forEachVal);
|
|
3716
|
+
if (fePath) {
|
|
3717
|
+
const dot = fePath.indexOf(".");
|
|
3718
|
+
if (dot !== -1) {
|
|
3719
|
+
const key = fePath.substring(0, dot);
|
|
3720
|
+
const idx = keyMap.get(key);
|
|
3721
|
+
if (idx !== void 0) {
|
|
3722
|
+
childScopes = [
|
|
3723
|
+
...childScopes,
|
|
3724
|
+
{ importIndex: idx, prefix: fePath.substring(dot + 1) }
|
|
3725
|
+
];
|
|
3726
|
+
}
|
|
3727
|
+
} else if (childScopes.length > 0) {
|
|
3728
|
+
const scope = childScopes[childScopes.length - 1];
|
|
3729
|
+
const newPrefix = scope.prefix ? `${scope.prefix}.${fePath}` : fePath;
|
|
3730
|
+
childScopes = [
|
|
3731
|
+
...childScopes,
|
|
3732
|
+
{ importIndex: scope.importIndex, prefix: newPrefix }
|
|
3733
|
+
];
|
|
3734
|
+
}
|
|
3735
|
+
}
|
|
3736
|
+
}
|
|
3737
|
+
if (tagName === "with-data") {
|
|
3738
|
+
const accessor = element.getAttribute?.("accessor");
|
|
3739
|
+
if (accessor && accessor !== "." && childScopes.length > 0) {
|
|
3740
|
+
const scope = childScopes[childScopes.length - 1];
|
|
3741
|
+
const newPrefix = scope.prefix ? `${scope.prefix}.${accessor}` : accessor;
|
|
3742
|
+
childScopes = [
|
|
3743
|
+
...childScopes,
|
|
3744
|
+
{ importIndex: scope.importIndex, prefix: newPrefix }
|
|
3745
|
+
];
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
const refVal = element.getAttribute?.("ref");
|
|
3749
|
+
if (refVal && tagName) {
|
|
3750
|
+
const actualType = htmlTagNameMap[tagName] ?? "HTMLElement";
|
|
3751
|
+
checkRef({ refPath: refVal, htmlTag: tagName, actualType }, childScopes);
|
|
3752
|
+
}
|
|
3753
|
+
for (const child of element.childNodes ?? []) {
|
|
3754
|
+
if (child.nodeType === 1) {
|
|
3755
|
+
walkElement(child, childScopes);
|
|
3756
|
+
}
|
|
3757
|
+
}
|
|
3758
|
+
}
|
|
3759
|
+
walkElement(jayHtml.body, []);
|
|
3760
|
+
return warnings;
|
|
3761
|
+
}
|
|
3132
3762
|
const PARSE_PARAM = /^\[(\[)?(\.\.\.)?([^\]]+)\]?\]$/;
|
|
3133
3763
|
function extractRouteParams(filePath, pagesBase) {
|
|
3134
3764
|
const relative = path.relative(pagesBase, filePath);
|
|
@@ -3205,6 +3835,101 @@ function checkRouteParams(parsedFile, filePath, pagesBase, jayHtmlContent) {
|
|
|
3205
3835
|
}
|
|
3206
3836
|
return warnings;
|
|
3207
3837
|
}
|
|
3838
|
+
function checkRouteToContractParams(parsedFile, filePath, pagesBase) {
|
|
3839
|
+
const routeParams = extractRouteParams(filePath, pagesBase);
|
|
3840
|
+
if (routeParams.size === 0)
|
|
3841
|
+
return [];
|
|
3842
|
+
const hasAnyContract = !!parsedFile.contract || parsedFile.headlessImports.some((imp) => !!imp.contract);
|
|
3843
|
+
if (!hasAnyContract)
|
|
3844
|
+
return [];
|
|
3845
|
+
const declaredParams = /* @__PURE__ */ new Set();
|
|
3846
|
+
if (parsedFile.contract?.params) {
|
|
3847
|
+
for (const p of parsedFile.contract.params) {
|
|
3848
|
+
declaredParams.add(p.name);
|
|
3849
|
+
}
|
|
3850
|
+
}
|
|
3851
|
+
for (const imp of parsedFile.headlessImports) {
|
|
3852
|
+
if (imp.contract?.params) {
|
|
3853
|
+
for (const p of imp.contract.params) {
|
|
3854
|
+
declaredParams.add(p.name);
|
|
3855
|
+
}
|
|
3856
|
+
}
|
|
3857
|
+
}
|
|
3858
|
+
const warnings = [];
|
|
3859
|
+
for (const routeParam of routeParams) {
|
|
3860
|
+
if (!declaredParams.has(routeParam)) {
|
|
3861
|
+
warnings.push(
|
|
3862
|
+
`Route provides param "${routeParam}" but no contract on this page declares it. Add params: { ${routeParam}: string } to the appropriate contract.`
|
|
3863
|
+
);
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3866
|
+
return warnings;
|
|
3867
|
+
}
|
|
3868
|
+
const HEADLESS_SKIP_ATTRS = /* @__PURE__ */ new Set([
|
|
3869
|
+
"foreach",
|
|
3870
|
+
"if",
|
|
3871
|
+
"ref",
|
|
3872
|
+
"trackby",
|
|
3873
|
+
"slowforeach",
|
|
3874
|
+
"jayindex",
|
|
3875
|
+
"jaytrackby",
|
|
3876
|
+
"when-resolved",
|
|
3877
|
+
"when-loading",
|
|
3878
|
+
"when-rejected",
|
|
3879
|
+
"accessor",
|
|
3880
|
+
"props",
|
|
3881
|
+
"key",
|
|
3882
|
+
"jay-coordinate-base",
|
|
3883
|
+
"jay-scope"
|
|
3884
|
+
]);
|
|
3885
|
+
function checkHeadlessInstanceProps(jayHtml, file) {
|
|
3886
|
+
const imports = jayHtml.headlessImports;
|
|
3887
|
+
const warnings = [];
|
|
3888
|
+
function walkElement(element) {
|
|
3889
|
+
const tagName = element.rawTagName?.toLowerCase();
|
|
3890
|
+
if (tagName?.startsWith("jay:")) {
|
|
3891
|
+
const contractName = tagName.substring(4);
|
|
3892
|
+
const imp = imports.find((i) => i.contractName === contractName && i.contract);
|
|
3893
|
+
if (imp?.contract) {
|
|
3894
|
+
const contract = imp.contract;
|
|
3895
|
+
const attrs = element.attributes ?? {};
|
|
3896
|
+
const passedProps = /* @__PURE__ */ new Set();
|
|
3897
|
+
for (const attrName of Object.keys(attrs)) {
|
|
3898
|
+
if (!HEADLESS_SKIP_ATTRS.has(attrName.toLowerCase())) {
|
|
3899
|
+
passedProps.add(attrName);
|
|
3900
|
+
}
|
|
3901
|
+
}
|
|
3902
|
+
if (passedProps.size > 0) {
|
|
3903
|
+
const contractPropNames = new Set((contract.props || []).map((p) => p.name));
|
|
3904
|
+
for (const prop of passedProps) {
|
|
3905
|
+
if (!contractPropNames.has(prop)) {
|
|
3906
|
+
imp.key ? `${imp.key} (${contractName})` : contractName;
|
|
3907
|
+
warnings.push(
|
|
3908
|
+
`<jay:${contractName}> passes attribute "${prop}" but the "${contract.name}" contract does not declare it as a prop. Add to ${contractName}.jay-contract: props: [{ name: ${prop}, type: string }]`
|
|
3909
|
+
);
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3912
|
+
}
|
|
3913
|
+
if (contract.props) {
|
|
3914
|
+
for (const contractProp of contract.props) {
|
|
3915
|
+
if (contractProp.required && !passedProps.has(contractProp.name)) {
|
|
3916
|
+
warnings.push(
|
|
3917
|
+
`<jay:${contractName}> is missing required prop "${contractProp.name}" declared in the "${contract.name}" contract.`
|
|
3918
|
+
);
|
|
3919
|
+
}
|
|
3920
|
+
}
|
|
3921
|
+
}
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3924
|
+
for (const child of element.childNodes ?? []) {
|
|
3925
|
+
if (child.nodeType === 1) {
|
|
3926
|
+
walkElement(child);
|
|
3927
|
+
}
|
|
3928
|
+
}
|
|
3929
|
+
}
|
|
3930
|
+
walkElement(jayHtml.body);
|
|
3931
|
+
return warnings;
|
|
3932
|
+
}
|
|
3208
3933
|
async function validateJayFiles(options = {}) {
|
|
3209
3934
|
const config = loadConfig();
|
|
3210
3935
|
const resolvedConfig = getConfigWithDefaults(config);
|
|
@@ -3282,6 +4007,22 @@ async function validateJayFiles(options = {}) {
|
|
|
3282
4007
|
for (const msg of routeParamWarnings) {
|
|
3283
4008
|
warnings.push({ file: relativePath, message: msg });
|
|
3284
4009
|
}
|
|
4010
|
+
const routeToContractWarnings = checkRouteToContractParams(
|
|
4011
|
+
parsedFile.val,
|
|
4012
|
+
jayFile,
|
|
4013
|
+
scanDir
|
|
4014
|
+
);
|
|
4015
|
+
for (const msg of routeToContractWarnings) {
|
|
4016
|
+
warnings.push({ file: relativePath, message: msg });
|
|
4017
|
+
}
|
|
4018
|
+
const refTypeErrors = checkRefElementTypes(parsedFile.val, relativePath);
|
|
4019
|
+
for (const msg of refTypeErrors) {
|
|
4020
|
+
errors.push({ file: relativePath, message: msg, stage: "generate" });
|
|
4021
|
+
}
|
|
4022
|
+
const headlessPropWarnings = checkHeadlessInstanceProps(parsedFile.val, relativePath);
|
|
4023
|
+
for (const msg of headlessPropWarnings) {
|
|
4024
|
+
warnings.push({ file: relativePath, message: msg });
|
|
4025
|
+
}
|
|
3285
4026
|
const fileCoverage = analyzeTagCoverage(parsedFile.val, relativePath);
|
|
3286
4027
|
if (fileCoverage) {
|
|
3287
4028
|
coverage.push(fileCoverage);
|
|
@@ -3752,32 +4493,32 @@ program.command("validate-plugin [path]").description("Validate a Jay Stack plug
|
|
|
3752
4493
|
process.exit(1);
|
|
3753
4494
|
}
|
|
3754
4495
|
});
|
|
3755
|
-
|
|
4496
|
+
const ALL_ROLES = ["designer", "developer", "plugin"];
|
|
4497
|
+
async function ensureAgentKitDocs(projectRoot, _force, mode) {
|
|
3756
4498
|
const path2 = await import("node:path");
|
|
3757
4499
|
const fs2 = await import("node:fs/promises");
|
|
3758
4500
|
const { fileURLToPath } = await import("node:url");
|
|
3759
4501
|
const agentKitDir = path2.join(projectRoot, "agent-kit");
|
|
3760
|
-
await fs2.mkdir(agentKitDir, { recursive: true });
|
|
3761
4502
|
const thisDir = path2.dirname(fileURLToPath(import.meta.url));
|
|
3762
4503
|
const templateDir = path2.resolve(thisDir, "..", "agent-kit-template");
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
4504
|
+
const roles = mode && ALL_ROLES.includes(mode) ? [mode] : ALL_ROLES;
|
|
4505
|
+
for (const role of roles) {
|
|
4506
|
+
const roleTemplateDir = path2.join(templateDir, role);
|
|
4507
|
+
const roleOutputDir = path2.join(agentKitDir, role);
|
|
4508
|
+
let files;
|
|
4509
|
+
try {
|
|
4510
|
+
files = (await fs2.readdir(roleTemplateDir)).filter((f) => f.endsWith(".md"));
|
|
4511
|
+
} catch {
|
|
4512
|
+
continue;
|
|
4513
|
+
}
|
|
4514
|
+
await fs2.mkdir(roleOutputDir, { recursive: true });
|
|
4515
|
+
for (const filename of files) {
|
|
4516
|
+
await fs2.copyFile(
|
|
4517
|
+
path2.join(roleTemplateDir, filename),
|
|
4518
|
+
path2.join(roleOutputDir, filename)
|
|
4519
|
+
);
|
|
4520
|
+
getLogger().info(chalk.gray(` Created agent-kit/${role}/${filename}`));
|
|
3778
4521
|
}
|
|
3779
|
-
await fs2.copyFile(path2.join(templateDir, filename), destPath);
|
|
3780
|
-
getLogger().info(chalk.gray(` Created agent-kit/${filename}`));
|
|
3781
4522
|
}
|
|
3782
4523
|
}
|
|
3783
4524
|
async function generatePluginReferences(projectRoot, options, initErrors, viteServer) {
|
|
@@ -3899,7 +4640,10 @@ program.command("setup [plugin]").description(
|
|
|
3899
4640
|
});
|
|
3900
4641
|
program.command("agent-kit").description(
|
|
3901
4642
|
"Prepare the agent kit: materialize contracts, generate references, and write docs to agent-kit/"
|
|
3902
|
-
).option("-o, --output <dir>", "Output directory (default: agent-kit/materialized-contracts)").option("--yaml", "Output contract index as YAML to stdout").option("--list", "List contracts without writing files").option("--plugin <name>", "Filter to specific plugin").option("--dynamic-only", "Only process dynamic contracts").option("--force", "Force re-materialization").option("--no-references", "Skip reference data generation").option(
|
|
4643
|
+
).option("-o, --output <dir>", "Output directory (default: agent-kit/materialized-contracts)").option("--yaml", "Output contract index as YAML to stdout").option("--list", "List contracts without writing files").option("--plugin <name>", "Filter to specific plugin").option("--dynamic-only", "Only process dynamic contracts").option("--force", "Force re-materialization").option("--no-references", "Skip reference data generation").option(
|
|
4644
|
+
"-m, --mode <role>",
|
|
4645
|
+
"Generate guides for a specific role: designer, developer, or plugin (default: all)"
|
|
4646
|
+
).option("-v, --verbose", "Show detailed output").action(async (options) => {
|
|
3903
4647
|
const projectRoot = process.cwd();
|
|
3904
4648
|
const { initErrors, viteServer } = await runMaterialize(
|
|
3905
4649
|
projectRoot,
|
|
@@ -3910,7 +4654,7 @@ program.command("agent-kit").description(
|
|
|
3910
4654
|
);
|
|
3911
4655
|
try {
|
|
3912
4656
|
if (!options.list) {
|
|
3913
|
-
await ensureAgentKitDocs(projectRoot, options.force);
|
|
4657
|
+
await ensureAgentKitDocs(projectRoot, options.force, options.mode);
|
|
3914
4658
|
if (options.references !== false) {
|
|
3915
4659
|
await generatePluginReferences(projectRoot, options, initErrors, viteServer);
|
|
3916
4660
|
}
|