@jay-framework/jay-stack-cli 0.13.0 → 0.14.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.
@@ -26,7 +26,7 @@ There is no standalone "interactive" phase. Any tag with `type: interactive` (re
26
26
  ## Workflow
27
27
 
28
28
  1. **Read this file** for overview and workflow
29
- 2. **Discover plugins** — read `materialized-contracts/plugins-index.yaml` to see available plugins, contracts, and actions. Read `materialized-contracts/contracts-index.yaml` for the full contract list.
29
+ 2. **Discover plugins** — read `plugins-index.yaml` to see available plugins, contracts, and actions.
30
30
  3. **Read contracts** — read the `.jay-contract` files (paths from plugins-index) to understand data shapes, tag types, phases, and props.
31
31
  4. **Read actions** — read `.jay-action` files (paths from plugins-index) to see action descriptions, input schemas, and output schemas. This tells you what data each action accepts and returns.
32
32
  5. **Read references** — check `references/<plugin>/` for pre-generated discovery data (product catalogs, collection schemas, etc.). These are generated by `jay-stack agent-kit` and contain real data from the site.
@@ -50,7 +50,7 @@ There is no standalone "interactive" phase. Any tag with `type: interactive` (re
50
50
 
51
51
  ### 1. Discover plugins and contracts
52
52
 
53
- Read `materialized-contracts/plugins-index.yaml`:
53
+ Read `plugins-index.yaml`:
54
54
 
55
55
  ```yaml
56
56
  plugins:
@@ -50,8 +50,7 @@ jay-stack agent-kit --no-references
50
50
 
51
51
  Outputs:
52
52
 
53
- - `materialized-contracts/contracts-index.yaml`
54
- - `materialized-contracts/plugins-index.yaml`
53
+ - `plugins-index.yaml`
55
54
  - `materialized-contracts/<plugin>/*.jay-contract` (dynamic contracts)
56
55
  - `references/<plugin>/` — plugin reference data (product catalogs, collection schemas, etc.)
57
56
  - Documentation files (INSTRUCTIONS.md and reference docs)
@@ -2,10 +2,9 @@
2
2
 
3
3
  ## Discovery: Plugins Index
4
4
 
5
- After running `jay-stack agent-kit`, read `materialized-contracts/plugins-index.yaml`:
5
+ After running `jay-stack agent-kit`, read `plugins-index.yaml`:
6
6
 
7
7
  ```yaml
8
- materialized_at: '2026-02-09T...'
9
8
  jay_stack_version: '1.0.0'
10
9
  plugins:
11
10
  - name: wix-stores
@@ -37,18 +36,6 @@ Fields:
37
36
  - `actions[].description` — short description of what the action does
38
37
  - `actions[].path` — path to the `.jay-action` file with full input/output schemas
39
38
 
40
- ## Discovery: Contracts Index
41
-
42
- `materialized-contracts/contracts-index.yaml` lists all contracts across all plugins:
43
-
44
- ```yaml
45
- contracts:
46
- - plugin: wix-stores
47
- name: product-page
48
- type: static
49
- path: ./node_modules/@wix/stores/lib/contracts/product-page.jay-contract
50
- ```
51
-
52
39
  ## Reading plugin.yaml
53
40
 
54
41
  Each plugin has a `plugin.yaml` at its root (the `path` from plugins-index):
@@ -249,16 +236,16 @@ outputSchema:
249
236
 
250
237
  Schemas use a compact type notation:
251
238
 
252
- | Notation | Meaning |
253
- | --- | --- |
254
- | `propName: string` | Required string property |
255
- | `propName?: number` | Optional number property |
256
- | `propName: boolean` | Required boolean |
257
- | `propName: enum(a \| b \| c)` | Required enum |
258
- | `propName:` + nested block | Nested object |
259
- | `propName:` + `- childProp: type` | Array of objects (YAML list) |
260
- | `propName: importedName` | Type from `import:` block (references a `.jay-contract`) |
261
- | `- importedName` | Array of imported type |
239
+ | Notation | Meaning |
240
+ | --------------------------------- | -------------------------------------------------------- |
241
+ | `propName: string` | Required string property |
242
+ | `propName?: number` | Optional number property |
243
+ | `propName: boolean` | Required boolean |
244
+ | `propName: enum(a \| b \| c)` | Required enum |
245
+ | `propName:` + nested block | Nested object |
246
+ | `propName:` + `- childProp: type` | Array of objects (YAML list) |
247
+ | `propName: importedName` | Type from `import:` block (references a `.jay-contract`) |
248
+ | `- importedName` | Array of imported type |
262
249
 
263
250
  ### Using Action Metadata
264
251
 
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { LogLevel } from '@jay-framework/logger';
2
2
  import { PublishMessage, PublishResponse, SaveImageMessage, SaveImageResponse, HasImageMessage, HasImageResponse, GetProjectInfoMessage, GetProjectInfoResponse, ExportMessage, ExportResponse, ImportMessage, ImportResponse, ProjectPage, Plugin } from '@jay-framework/editor-protocol';
3
- export { ContractIndexEntry, ContractsIndex, MaterializeContractsOptions, MaterializeResult, listContracts, materializeContracts } from '@jay-framework/stack-server-runtime';
3
+ export { MaterializeContractsOptions, MaterializeResult, PluginContractEntry, PluginsIndex, PluginsIndexEntry, listContracts, materializeContracts } from '@jay-framework/stack-server-runtime';
4
4
 
5
5
  interface StartDevServerOptions {
6
6
  projectPath?: string;
package/dist/index.js CHANGED
@@ -2402,7 +2402,7 @@ async function startDevServer(options = {}) {
2402
2402
  const resolvedConfig = getConfigWithDefaults(config);
2403
2403
  const jayOptions = {
2404
2404
  tsConfigFilePath: "./tsconfig.json",
2405
- outputDir: "build/jay-runtime"
2405
+ outputDir: "build"
2406
2406
  };
2407
2407
  const app = express();
2408
2408
  const devServerPort = await getPort({ port: resolvedConfig.devServer.portRange });
@@ -2441,7 +2441,6 @@ async function startDevServer(options = {}) {
2441
2441
  pagesRootFolder: path.resolve(resolvedConfig.devServer.pagesBase),
2442
2442
  projectRootFolder: process.cwd(),
2443
2443
  publicBaseUrlPath: "/",
2444
- dontCacheSlowly: false,
2445
2444
  jayRollupConfig: jayOptions,
2446
2445
  logLevel: options.logLevel
2447
2446
  });
@@ -2934,13 +2933,206 @@ async function findJayFiles(dir) {
2934
2933
  async function findContractFiles(dir) {
2935
2934
  return await glob(`${dir}/**/*${JAY_CONTRACT_EXTENSION}`);
2936
2935
  }
2936
+ function flattenContractTags(tags, prefix) {
2937
+ const result = [];
2938
+ for (const tag of tags) {
2939
+ const tagPath = prefix ? `${prefix}.${tag.tag}` : tag.tag;
2940
+ result.push({ path: tagPath, required: tag.required === true });
2941
+ if (tag.tags) {
2942
+ result.push(...flattenContractTags(tag.tags, tagPath));
2943
+ }
2944
+ }
2945
+ return result;
2946
+ }
2947
+ function extractExpressions(text) {
2948
+ const results = [];
2949
+ const regex = /\{([^}]+)\}/g;
2950
+ let match;
2951
+ while ((match = regex.exec(text)) !== null) {
2952
+ results.push(match[1].trim());
2953
+ }
2954
+ return results;
2955
+ }
2956
+ function extractTagPath(expr) {
2957
+ let cleaned = expr.replace(/^!/, "").trim();
2958
+ cleaned = cleaned.split(/\s*[!=]==?\s*/)[0].trim();
2959
+ if (cleaned === "." || cleaned === "")
2960
+ return null;
2961
+ if (/^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*$/.test(cleaned)) {
2962
+ return cleaned;
2963
+ }
2964
+ return null;
2965
+ }
2966
+ const SKIP_ATTRS = /* @__PURE__ */ new Set([
2967
+ "forEach",
2968
+ "if",
2969
+ "ref",
2970
+ "trackBy",
2971
+ "slowForEach",
2972
+ "jayIndex",
2973
+ "jayTrackBy",
2974
+ "when-resolved",
2975
+ "when-loading",
2976
+ "when-rejected",
2977
+ "accessor"
2978
+ ]);
2979
+ function collectUsedTags(jayHtml) {
2980
+ const imports = jayHtml.headlessImports;
2981
+ const usedTags = /* @__PURE__ */ new Map();
2982
+ const keyMap = /* @__PURE__ */ new Map();
2983
+ for (let i = 0; i < imports.length; i++) {
2984
+ if (imports[i].contract) {
2985
+ usedTags.set(i, /* @__PURE__ */ new Set());
2986
+ if (imports[i].key) {
2987
+ keyMap.set(imports[i].key, i);
2988
+ }
2989
+ }
2990
+ }
2991
+ function markUsed(importIndex, tagPath) {
2992
+ usedTags.get(importIndex)?.add(tagPath);
2993
+ }
2994
+ function resolvePath(path2, scopes) {
2995
+ const dot = path2.indexOf(".");
2996
+ if (dot !== -1) {
2997
+ const key = path2.substring(0, dot);
2998
+ const idx = keyMap.get(key);
2999
+ if (idx !== void 0) {
3000
+ markUsed(idx, path2.substring(dot + 1));
3001
+ return;
3002
+ }
3003
+ }
3004
+ if (scopes.length > 0) {
3005
+ const scope = scopes[scopes.length - 1];
3006
+ const full = scope.prefix ? `${scope.prefix}.${path2}` : path2;
3007
+ markUsed(scope.importIndex, full);
3008
+ }
3009
+ }
3010
+ function walkElement(element, scopes) {
3011
+ const tagName = element.rawTagName?.toLowerCase();
3012
+ let childScopes = scopes;
3013
+ if (tagName?.startsWith("jay:")) {
3014
+ const contractName = tagName.substring(4);
3015
+ const idx = imports.findIndex(
3016
+ (imp) => imp.contractName === contractName && imp.contract
3017
+ );
3018
+ if (idx !== -1) {
3019
+ childScopes = [...scopes, { importIndex: idx, prefix: "" }];
3020
+ }
3021
+ }
3022
+ const forEachVal = element.getAttribute?.("forEach");
3023
+ if (forEachVal) {
3024
+ const fePath = extractTagPath(forEachVal);
3025
+ if (fePath) {
3026
+ resolvePath(fePath, childScopes);
3027
+ const dot = fePath.indexOf(".");
3028
+ if (dot !== -1) {
3029
+ const key = fePath.substring(0, dot);
3030
+ const idx = keyMap.get(key);
3031
+ if (idx !== void 0) {
3032
+ childScopes = [
3033
+ ...childScopes,
3034
+ { importIndex: idx, prefix: fePath.substring(dot + 1) }
3035
+ ];
3036
+ }
3037
+ } else if (childScopes.length > 0) {
3038
+ const scope = childScopes[childScopes.length - 1];
3039
+ const newPrefix = scope.prefix ? `${scope.prefix}.${fePath}` : fePath;
3040
+ childScopes = [
3041
+ ...childScopes,
3042
+ { importIndex: scope.importIndex, prefix: newPrefix }
3043
+ ];
3044
+ }
3045
+ }
3046
+ }
3047
+ if (tagName === "with-data") {
3048
+ const accessor = element.getAttribute?.("accessor");
3049
+ if (accessor && accessor !== "." && childScopes.length > 0) {
3050
+ resolvePath(accessor, childScopes);
3051
+ const scope = childScopes[childScopes.length - 1];
3052
+ const newPrefix = scope.prefix ? `${scope.prefix}.${accessor}` : accessor;
3053
+ childScopes = [
3054
+ ...childScopes,
3055
+ { importIndex: scope.importIndex, prefix: newPrefix }
3056
+ ];
3057
+ }
3058
+ }
3059
+ const ifVal = element.getAttribute?.("if");
3060
+ if (ifVal) {
3061
+ const ifPath = extractTagPath(ifVal);
3062
+ if (ifPath)
3063
+ resolvePath(ifPath, scopes);
3064
+ }
3065
+ const refVal = element.getAttribute?.("ref");
3066
+ if (refVal) {
3067
+ resolvePath(refVal, scopes);
3068
+ }
3069
+ const attrs = element.attributes ?? {};
3070
+ for (const [name, value] of Object.entries(attrs)) {
3071
+ if (SKIP_ATTRS.has(name))
3072
+ continue;
3073
+ for (const expr of extractExpressions(value)) {
3074
+ const p = extractTagPath(expr);
3075
+ if (p)
3076
+ resolvePath(p, scopes);
3077
+ }
3078
+ }
3079
+ for (const child of element.childNodes ?? []) {
3080
+ if (child.nodeType === 3) {
3081
+ const text = child.rawText ?? child.text ?? "";
3082
+ for (const expr of extractExpressions(text)) {
3083
+ const p = extractTagPath(expr);
3084
+ if (p)
3085
+ resolvePath(p, childScopes);
3086
+ }
3087
+ } else if (child.nodeType === 1) {
3088
+ walkElement(child, childScopes);
3089
+ }
3090
+ }
3091
+ }
3092
+ walkElement(jayHtml.body, []);
3093
+ return usedTags;
3094
+ }
3095
+ function analyzeTagCoverage(jayHtml, file) {
3096
+ const imports = jayHtml.headlessImports;
3097
+ const withContracts = imports.filter((imp) => imp.contract);
3098
+ if (withContracts.length === 0)
3099
+ return null;
3100
+ const usedTagsMap = collectUsedTags(jayHtml);
3101
+ const contracts = [];
3102
+ for (let i = 0; i < imports.length; i++) {
3103
+ const imp = imports[i];
3104
+ if (!imp.contract)
3105
+ continue;
3106
+ const allTags = flattenContractTags(imp.contract.tags);
3107
+ const usedSet = usedTagsMap.get(i) ?? /* @__PURE__ */ new Set();
3108
+ const expanded = new Set(usedSet);
3109
+ for (const usedPath of usedSet) {
3110
+ const segments = usedPath.split(".");
3111
+ for (let j = 1; j < segments.length; j++) {
3112
+ expanded.add(segments.slice(0, j).join("."));
3113
+ }
3114
+ }
3115
+ const unused = allTags.filter((t) => !expanded.has(t.path));
3116
+ const requiredUnused = unused.filter((t) => t.required);
3117
+ contracts.push({
3118
+ key: imp.key,
3119
+ contractName: imp.contractName,
3120
+ totalTags: allTags.length,
3121
+ usedTags: allTags.length - unused.length,
3122
+ unusedTags: unused.map((t) => t.path),
3123
+ requiredUnusedTags: requiredUnused.map((t) => t.path)
3124
+ });
3125
+ }
3126
+ return { file, contracts };
3127
+ }
2937
3128
  async function validateJayFiles(options = {}) {
2938
3129
  const config = loadConfig();
2939
3130
  const resolvedConfig = getConfigWithDefaults(config);
2940
- const projectRoot = process.cwd();
3131
+ const projectRoot = options.projectRoot ?? process.cwd();
2941
3132
  const scanDir = options.path ? path.resolve(options.path) : path.resolve(resolvedConfig.devServer.pagesBase);
2942
3133
  const errors = [];
2943
3134
  const warnings = [];
3135
+ const coverage = [];
2944
3136
  const jayHtmlFiles = await findJayFiles(scanDir);
2945
3137
  const contractFiles = await findContractFiles(scanDir);
2946
3138
  if (options.verbose) {
@@ -3006,6 +3198,10 @@ async function validateJayFiles(options = {}) {
3006
3198
  }
3007
3199
  continue;
3008
3200
  }
3201
+ const fileCoverage = analyzeTagCoverage(parsedFile.val, relativePath);
3202
+ if (fileCoverage) {
3203
+ coverage.push(fileCoverage);
3204
+ }
3009
3205
  const generatedFile = generateElementFile(
3010
3206
  parsedFile.val,
3011
3207
  RuntimeMode.MainTrusted,
@@ -3041,7 +3237,8 @@ async function validateJayFiles(options = {}) {
3041
3237
  jayHtmlFilesScanned: jayHtmlFiles.length,
3042
3238
  contractFilesScanned: contractFiles.length,
3043
3239
  errors,
3044
- warnings
3240
+ warnings,
3241
+ coverage
3045
3242
  };
3046
3243
  }
3047
3244
  function printJayValidationResult(result, options) {
@@ -3070,6 +3267,29 @@ function printJayValidationResult(result, options) {
3070
3267
  chalk.red(`${result.errors.length} error(s) found, ${validFiles} file(s) valid.`)
3071
3268
  );
3072
3269
  }
3270
+ if (result.coverage.length > 0) {
3271
+ logger.important("");
3272
+ logger.important("Tag Coverage:");
3273
+ for (const fileCov of result.coverage) {
3274
+ logger.important(` ${fileCov.file}`);
3275
+ for (const contract of fileCov.contracts) {
3276
+ const label = contract.key ? `${contract.key} (${contract.contractName})` : contract.contractName;
3277
+ logger.important(
3278
+ ` ${label}: ${contract.usedTags}/${contract.totalTags} tags used`
3279
+ );
3280
+ if (contract.unusedTags.length > 0) {
3281
+ logger.important(chalk.gray(` Unused: ${contract.unusedTags.join(", ")}`));
3282
+ }
3283
+ if (contract.requiredUnusedTags.length > 0) {
3284
+ logger.important(
3285
+ chalk.yellow(
3286
+ ` ⚠ Required unused: ${contract.requiredUnusedTags.join(", ")}`
3287
+ )
3288
+ );
3289
+ }
3290
+ }
3291
+ }
3292
+ }
3073
3293
  }
3074
3294
  async function initializeServicesForCli(projectRoot, viteServer) {
3075
3295
  const path2 = await import("node:path");
@@ -3553,12 +3773,14 @@ async function runMaterialize(projectRoot, options, defaultOutputRelative, keepV
3553
3773
  services
3554
3774
  );
3555
3775
  if (options.yaml) {
3556
- getLogger().important(YAML.stringify(result.index));
3776
+ getLogger().important(YAML.stringify(result.pluginsIndex));
3557
3777
  } else {
3558
- getLogger().important(
3559
- chalk.green(`
3560
- ✅ Materialized ${result.index.contracts.length} contracts`)
3778
+ const totalContracts = result.pluginsIndex.plugins.reduce(
3779
+ (sum, p) => sum + p.contracts.length,
3780
+ 0
3561
3781
  );
3782
+ getLogger().important(chalk.green(`
3783
+ ✅ Materialized ${totalContracts} contracts`));
3562
3784
  getLogger().important(` Static: ${result.staticCount}`);
3563
3785
  getLogger().important(` Dynamic: ${result.dynamicCount}`);
3564
3786
  getLogger().important(` Output: ${result.outputDir}`);
@@ -3670,21 +3892,15 @@ function printValidationResult(result, verbose) {
3670
3892
  function printContractList(index) {
3671
3893
  const logger = getLogger();
3672
3894
  logger.important("\nAvailable Contracts:\n");
3673
- const byPlugin = /* @__PURE__ */ new Map();
3674
- for (const contract of index.contracts) {
3675
- const existing = byPlugin.get(contract.plugin) || [];
3676
- existing.push(contract);
3677
- byPlugin.set(contract.plugin, existing);
3678
- }
3679
- for (const [plugin, contracts] of byPlugin) {
3680
- logger.important(chalk.bold(`📦 ${plugin}`));
3681
- for (const contract of contracts) {
3895
+ for (const plugin of index.plugins) {
3896
+ logger.important(chalk.bold(`📦 ${plugin.name}`));
3897
+ for (const contract of plugin.contracts) {
3682
3898
  const typeIcon = contract.type === "static" ? "📄" : "⚡";
3683
3899
  logger.important(` ${typeIcon} ${contract.name}`);
3684
3900
  }
3685
3901
  logger.important("");
3686
3902
  }
3687
- if (index.contracts.length === 0) {
3903
+ if (index.plugins.length === 0) {
3688
3904
  logger.important(chalk.gray("No contracts found."));
3689
3905
  }
3690
3906
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jay-framework/jay-stack-cli",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -24,14 +24,14 @@
24
24
  "test:watch": "vitest"
25
25
  },
26
26
  "dependencies": {
27
- "@jay-framework/compiler-jay-html": "^0.13.0",
28
- "@jay-framework/compiler-shared": "^0.13.0",
29
- "@jay-framework/dev-server": "^0.13.0",
30
- "@jay-framework/editor-server": "^0.13.0",
31
- "@jay-framework/fullstack-component": "^0.13.0",
32
- "@jay-framework/logger": "^0.13.0",
33
- "@jay-framework/plugin-validator": "^0.13.0",
34
- "@jay-framework/stack-server-runtime": "^0.13.0",
27
+ "@jay-framework/compiler-jay-html": "^0.14.0",
28
+ "@jay-framework/compiler-shared": "^0.14.0",
29
+ "@jay-framework/dev-server": "^0.14.0",
30
+ "@jay-framework/editor-server": "^0.14.0",
31
+ "@jay-framework/fullstack-component": "^0.14.0",
32
+ "@jay-framework/logger": "^0.14.0",
33
+ "@jay-framework/plugin-validator": "^0.14.0",
34
+ "@jay-framework/stack-server-runtime": "^0.14.0",
35
35
  "chalk": "^4.1.2",
36
36
  "commander": "^14.0.0",
37
37
  "express": "^5.0.1",
@@ -40,7 +40,7 @@
40
40
  "yaml": "^2.3.4"
41
41
  },
42
42
  "devDependencies": {
43
- "@jay-framework/dev-environment": "^0.13.0",
43
+ "@jay-framework/dev-environment": "^0.14.0",
44
44
  "@types/express": "^5.0.2",
45
45
  "@types/node": "^22.15.21",
46
46
  "nodemon": "^3.0.3",