@jay-framework/jay-stack-cli 0.13.0 → 0.15.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,12 +26,12 @@ 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.
33
33
  6. **Discover data** — run `jay-stack params <plugin>/<contract>` for SSG route params, `jay-stack action <plugin>/<action>` for data discovery. Use reference files (step 5) first when available — they're faster than running CLI commands.
34
- 7. **Create pages** — write `.jay-html` files under `src/pages/` following directory-based routing.
34
+ 7. **Create pages** — write `.jay-html` files under `src/pages/` following directory-based routing. For static override routes (a static page that overrides a dynamic route for a specific URL), declare params with `<script type="application/jay-params">`.
35
35
  8. **Validate** — run `jay-stack validate` to check for errors.
36
36
  9. **Test** — run `jay-stack dev --test-mode` and verify pages render.
37
37
 
@@ -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
 
@@ -10,9 +10,17 @@ A `.jay-html` file is standard HTML with jay-specific extensions.
10
10
  <!-- Page contract (optional — defines page-level data) -->
11
11
  <script type="application/jay-data" contract="./page.jay-contract"></script>
12
12
 
13
+ <!-- Explicit route params (for static override routes) -->
14
+ <script type="application/jay-params">
15
+ slug: ceramic-flower-vase
16
+ </script>
17
+
13
18
  <!-- Headless component imports -->
14
19
  <script type="application/jay-headless" plugin="..." contract="..." key="..."></script>
15
20
 
21
+ <!-- Headfull component imports -->
22
+ <script type="application/jay-headfull" src="..." names="..." contract="..."></script>
23
+
16
24
  <!-- Styles -->
17
25
  <style>
18
26
  /* inline CSS */
@@ -203,6 +211,35 @@ Use `<jay:contract-name>` tags with props:
203
211
 
204
212
  Inside `<jay:...>`, bindings resolve to **that instance's** contract tags (not the parent).
205
213
 
214
+ ## Headfull Full-Stack Components
215
+
216
+ Headfull components that own their UI can be made full-stack by adding a `contract` attribute:
217
+
218
+ ```html
219
+ <head>
220
+ <script
221
+ type="application/jay-headfull"
222
+ src="../components/shared-header"
223
+ names="SharedHeader"
224
+ contract="../components/shared-header/shared-header.jay-contract"
225
+ ></script>
226
+ </head>
227
+ ```
228
+
229
+ **Attributes:**
230
+
231
+ - `src` — Path to the component module
232
+ - `names` — Component name to import
233
+ - `contract` — Path to the contract file (makes the component full-stack with SSR)
234
+
235
+ **Usage** — same as client-only headfull, with props:
236
+
237
+ ```html
238
+ <jay:SharedHeader logoUrl="/logo.png" />
239
+ ```
240
+
241
+ Without `contract`, the component is client-only. With `contract`, it participates in slow/fast/interactive phases and is server-side rendered. Use headfull full-stack components for reusable UI with fixed layout that needs SSR (headers, footers, sidebars).
242
+
206
243
  ## Page-Level Contract
207
244
 
208
245
  A page can define its own data contract:
@@ -39,7 +39,9 @@ Static routes match before dynamic routes (most specific first):
39
39
  3. **`[[param]]`** — optional param
40
40
  4. **`[...param]`** — catch-all — lowest priority
41
41
 
42
- Static override alongside a dynamic route:
42
+ ## Static Route Overrides
43
+
44
+ A static route can override a dynamic route for a specific URL — giving one particular page a custom layout while the dynamic route handles everything else:
43
45
 
44
46
  ```
45
47
  src/pages/products/
@@ -47,6 +49,34 @@ src/pages/products/
47
49
  └── ceramic-flower-vase/page.jay-html # static override for this specific product
48
50
  ```
49
51
 
52
+ The static `ceramic-flower-vase/` route takes priority over `[slug]/` for that URL, but all other product URLs still use the dynamic route.
53
+
54
+ ### Static Override Params (`jay-params`)
55
+
56
+ Static override routes often use the same contract as the dynamic route they override. Since the static route has no dynamic directory segment, the params must be declared explicitly using `<script type="application/jay-params">`:
57
+
58
+ ```html
59
+ <!-- src/pages/products/ceramic-flower-vase/page.jay-html -->
60
+ <html>
61
+ <head>
62
+ <script type="application/jay-params">
63
+ slug: ceramic-flower-vase
64
+ </script>
65
+ <script
66
+ type="application/jay-headless"
67
+ plugin="wix-stores"
68
+ contract="product-page"
69
+ key="product"
70
+ ></script>
71
+ </head>
72
+ <body>
73
+ <h1>{product.productName}</h1>
74
+ </body>
75
+ </html>
76
+ ```
77
+
78
+ The script body is YAML. The declared params are passed to the component as if extracted from a dynamic URL segment. Without this, the component would receive no param values.
79
+
50
80
  ## Page Files
51
81
 
52
82
  Each page directory can contain:
@@ -82,9 +112,9 @@ tags:
82
112
 
83
113
  ## Dynamic Routes and Contract Params
84
114
 
85
- When a contract declares `params`, it means the component expects those URL parameters to be provided by the route. This tells you that the page using this contract **should be placed in a matching dynamic route directory**.
115
+ When a component on the page whether the page contract, a headless component, or a headfull full-stack component declares `params`, the page should be placed in a dynamic route directory that provides those params.
86
116
 
87
- For example, if a contract declares:
117
+ For example, if a headless component's contract declares:
88
118
 
89
119
  ```yaml
90
120
  name: product-page
@@ -94,12 +124,14 @@ tags:
94
124
  - ...
95
125
  ```
96
126
 
97
- Then the page using this contract should live at a route that provides a `slug` param:
127
+ Then the page using this component should live at a route that provides a `slug` param:
98
128
 
99
129
  ```
100
130
  src/pages/products/[slug]/page.jay-html
101
131
  ```
102
132
 
133
+ Multiple components on the same page can each declare params. The route directory must provide all required params across all components. For example, if the page contract requires `lang` and a headless component requires `slug`, the page should live at `src/pages/[lang]/products/[slug]/page.jay-html`.
134
+
103
135
  ### Discovering Param Values
104
136
 
105
137
  For SSG with dynamic routes, the plugin component provides a `loadParams` generator that yields all valid param combinations. Use it to discover what routes will be generated:
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
@@ -14,6 +14,7 @@ import { listContracts as listContracts2, materializeContracts as materializeCon
14
14
  import { Command } from "commander";
15
15
  import chalk from "chalk";
16
16
  import { glob } from "glob";
17
+ import { parse } from "node-html-parser";
17
18
  const DEFAULT_CONFIG = {
18
19
  devServer: {
19
20
  portRange: [3e3, 3100],
@@ -2402,7 +2403,7 @@ async function startDevServer(options = {}) {
2402
2403
  const resolvedConfig = getConfigWithDefaults(config);
2403
2404
  const jayOptions = {
2404
2405
  tsConfigFilePath: "./tsconfig.json",
2405
- outputDir: "build/jay-runtime"
2406
+ outputDir: "build"
2406
2407
  };
2407
2408
  const app = express();
2408
2409
  const devServerPort = await getPort({ port: resolvedConfig.devServer.portRange });
@@ -2441,7 +2442,6 @@ async function startDevServer(options = {}) {
2441
2442
  pagesRootFolder: path.resolve(resolvedConfig.devServer.pagesBase),
2442
2443
  projectRootFolder: process.cwd(),
2443
2444
  publicBaseUrlPath: "/",
2444
- dontCacheSlowly: false,
2445
2445
  jayRollupConfig: jayOptions,
2446
2446
  logLevel: options.logLevel
2447
2447
  });
@@ -2934,13 +2934,282 @@ async function findJayFiles(dir) {
2934
2934
  async function findContractFiles(dir) {
2935
2935
  return await glob(`${dir}/**/*${JAY_CONTRACT_EXTENSION}`);
2936
2936
  }
2937
+ function flattenContractTags(tags, prefix) {
2938
+ const result = [];
2939
+ for (const tag of tags) {
2940
+ const tagPath = prefix ? `${prefix}.${tag.tag}` : tag.tag;
2941
+ result.push({ path: tagPath, required: tag.required === true });
2942
+ if (tag.tags) {
2943
+ result.push(...flattenContractTags(tag.tags, tagPath));
2944
+ }
2945
+ }
2946
+ return result;
2947
+ }
2948
+ function extractExpressions(text) {
2949
+ const results = [];
2950
+ const regex = /\{([^}]+)\}/g;
2951
+ let match;
2952
+ while ((match = regex.exec(text)) !== null) {
2953
+ results.push(match[1].trim());
2954
+ }
2955
+ return results;
2956
+ }
2957
+ function extractTagPath(expr) {
2958
+ let cleaned = expr.replace(/^!/, "").trim();
2959
+ cleaned = cleaned.split(/\s*[!=]==?\s*/)[0].trim();
2960
+ if (cleaned === "." || cleaned === "")
2961
+ return null;
2962
+ if (/^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*$/.test(cleaned)) {
2963
+ return cleaned;
2964
+ }
2965
+ return null;
2966
+ }
2967
+ const SKIP_ATTRS = /* @__PURE__ */ new Set([
2968
+ "forEach",
2969
+ "if",
2970
+ "ref",
2971
+ "trackBy",
2972
+ "slowForEach",
2973
+ "jayIndex",
2974
+ "jayTrackBy",
2975
+ "when-resolved",
2976
+ "when-loading",
2977
+ "when-rejected",
2978
+ "accessor"
2979
+ ]);
2980
+ function collectUsedTags(jayHtml) {
2981
+ const imports = jayHtml.headlessImports;
2982
+ const usedTags = /* @__PURE__ */ new Map();
2983
+ const keyMap = /* @__PURE__ */ new Map();
2984
+ for (let i = 0; i < imports.length; i++) {
2985
+ if (imports[i].contract) {
2986
+ usedTags.set(i, /* @__PURE__ */ new Set());
2987
+ if (imports[i].key) {
2988
+ keyMap.set(imports[i].key, i);
2989
+ }
2990
+ }
2991
+ }
2992
+ function markUsed(importIndex, tagPath) {
2993
+ usedTags.get(importIndex)?.add(tagPath);
2994
+ }
2995
+ function resolvePath(path2, scopes) {
2996
+ const dot = path2.indexOf(".");
2997
+ if (dot !== -1) {
2998
+ const key = path2.substring(0, dot);
2999
+ const idx = keyMap.get(key);
3000
+ if (idx !== void 0) {
3001
+ markUsed(idx, path2.substring(dot + 1));
3002
+ return;
3003
+ }
3004
+ }
3005
+ if (scopes.length > 0) {
3006
+ const scope = scopes[scopes.length - 1];
3007
+ const full = scope.prefix ? `${scope.prefix}.${path2}` : path2;
3008
+ markUsed(scope.importIndex, full);
3009
+ }
3010
+ }
3011
+ function walkElement(element, scopes) {
3012
+ const tagName = element.rawTagName?.toLowerCase();
3013
+ let childScopes = scopes;
3014
+ if (tagName?.startsWith("jay:")) {
3015
+ const contractName = tagName.substring(4);
3016
+ const idx = imports.findIndex(
3017
+ (imp) => imp.contractName === contractName && imp.contract
3018
+ );
3019
+ if (idx !== -1) {
3020
+ childScopes = [...scopes, { importIndex: idx, prefix: "" }];
3021
+ }
3022
+ }
3023
+ const forEachVal = element.getAttribute?.("forEach");
3024
+ if (forEachVal) {
3025
+ const fePath = extractTagPath(forEachVal);
3026
+ if (fePath) {
3027
+ resolvePath(fePath, childScopes);
3028
+ const dot = fePath.indexOf(".");
3029
+ if (dot !== -1) {
3030
+ const key = fePath.substring(0, dot);
3031
+ const idx = keyMap.get(key);
3032
+ if (idx !== void 0) {
3033
+ childScopes = [
3034
+ ...childScopes,
3035
+ { importIndex: idx, prefix: fePath.substring(dot + 1) }
3036
+ ];
3037
+ }
3038
+ } else if (childScopes.length > 0) {
3039
+ const scope = childScopes[childScopes.length - 1];
3040
+ const newPrefix = scope.prefix ? `${scope.prefix}.${fePath}` : fePath;
3041
+ childScopes = [
3042
+ ...childScopes,
3043
+ { importIndex: scope.importIndex, prefix: newPrefix }
3044
+ ];
3045
+ }
3046
+ }
3047
+ }
3048
+ if (tagName === "with-data") {
3049
+ const accessor = element.getAttribute?.("accessor");
3050
+ if (accessor && accessor !== "." && childScopes.length > 0) {
3051
+ resolvePath(accessor, childScopes);
3052
+ const scope = childScopes[childScopes.length - 1];
3053
+ const newPrefix = scope.prefix ? `${scope.prefix}.${accessor}` : accessor;
3054
+ childScopes = [
3055
+ ...childScopes,
3056
+ { importIndex: scope.importIndex, prefix: newPrefix }
3057
+ ];
3058
+ }
3059
+ }
3060
+ const ifVal = element.getAttribute?.("if");
3061
+ if (ifVal) {
3062
+ const ifPath = extractTagPath(ifVal);
3063
+ if (ifPath)
3064
+ resolvePath(ifPath, scopes);
3065
+ }
3066
+ const refVal = element.getAttribute?.("ref");
3067
+ if (refVal) {
3068
+ resolvePath(refVal, scopes);
3069
+ }
3070
+ const attrs = element.attributes ?? {};
3071
+ for (const [name, value] of Object.entries(attrs)) {
3072
+ if (SKIP_ATTRS.has(name))
3073
+ continue;
3074
+ for (const expr of extractExpressions(value)) {
3075
+ const p = extractTagPath(expr);
3076
+ if (p)
3077
+ resolvePath(p, scopes);
3078
+ }
3079
+ }
3080
+ for (const child of element.childNodes ?? []) {
3081
+ if (child.nodeType === 3) {
3082
+ const text = child.rawText ?? child.text ?? "";
3083
+ for (const expr of extractExpressions(text)) {
3084
+ const p = extractTagPath(expr);
3085
+ if (p)
3086
+ resolvePath(p, childScopes);
3087
+ }
3088
+ } else if (child.nodeType === 1) {
3089
+ walkElement(child, childScopes);
3090
+ }
3091
+ }
3092
+ }
3093
+ walkElement(jayHtml.body, []);
3094
+ return usedTags;
3095
+ }
3096
+ function analyzeTagCoverage(jayHtml, file) {
3097
+ const imports = jayHtml.headlessImports;
3098
+ const withContracts = imports.filter((imp) => imp.contract);
3099
+ if (withContracts.length === 0)
3100
+ return null;
3101
+ const usedTagsMap = collectUsedTags(jayHtml);
3102
+ const contracts = [];
3103
+ for (let i = 0; i < imports.length; i++) {
3104
+ const imp = imports[i];
3105
+ if (!imp.contract)
3106
+ continue;
3107
+ const allTags = flattenContractTags(imp.contract.tags);
3108
+ const usedSet = usedTagsMap.get(i) ?? /* @__PURE__ */ new Set();
3109
+ const expanded = new Set(usedSet);
3110
+ for (const usedPath of usedSet) {
3111
+ const segments = usedPath.split(".");
3112
+ for (let j = 1; j < segments.length; j++) {
3113
+ expanded.add(segments.slice(0, j).join("."));
3114
+ }
3115
+ }
3116
+ const unused = allTags.filter((t) => !expanded.has(t.path));
3117
+ const requiredUnused = unused.filter((t) => t.required);
3118
+ contracts.push({
3119
+ key: imp.key,
3120
+ contractName: imp.contractName,
3121
+ totalTags: allTags.length,
3122
+ usedTags: allTags.length - unused.length,
3123
+ unusedTags: unused.map((t) => t.path),
3124
+ requiredUnusedTags: requiredUnused.map((t) => t.path)
3125
+ });
3126
+ }
3127
+ return { file, contracts };
3128
+ }
3129
+ const PARSE_PARAM = /^\[(\[)?(\.\.\.)?([^\]]+)\]?\]$/;
3130
+ function extractRouteParams(filePath, pagesBase) {
3131
+ const relative = path.relative(pagesBase, filePath);
3132
+ const segments = relative.split(path.sep);
3133
+ const params = /* @__PURE__ */ new Set();
3134
+ for (const segment of segments) {
3135
+ const match = PARSE_PARAM.exec(segment);
3136
+ if (match) {
3137
+ params.add(match[3]);
3138
+ }
3139
+ }
3140
+ return params;
3141
+ }
3142
+ function dedentYaml(text) {
3143
+ const lines = text.split("\n").filter((l) => l.trim().length > 0);
3144
+ if (lines.length === 0)
3145
+ return "";
3146
+ const minIndent = Math.min(...lines.map((l) => l.match(/^\s*/)?.[0].length ?? 0));
3147
+ return lines.map((l) => l.slice(minIndent)).join("\n");
3148
+ }
3149
+ function extractJayParams(content) {
3150
+ const root = parse(content, {
3151
+ comment: true,
3152
+ blockTextElements: { script: true, style: true }
3153
+ });
3154
+ const head = root.querySelector("head");
3155
+ if (!head)
3156
+ return /* @__PURE__ */ new Set();
3157
+ const paramScripts = head.querySelectorAll('script[type="application/jay-params"]');
3158
+ if (paramScripts.length !== 1)
3159
+ return /* @__PURE__ */ new Set();
3160
+ const body = dedentYaml(paramScripts[0].textContent ?? "");
3161
+ if (!body)
3162
+ return /* @__PURE__ */ new Set();
3163
+ try {
3164
+ const parsed = YAML.parse(body);
3165
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
3166
+ return new Set(Object.keys(parsed));
3167
+ }
3168
+ return /* @__PURE__ */ new Set();
3169
+ } catch {
3170
+ return /* @__PURE__ */ new Set();
3171
+ }
3172
+ }
3173
+ function checkRouteParams(parsedFile, filePath, pagesBase, jayHtmlContent) {
3174
+ const requiredParams = /* @__PURE__ */ new Set();
3175
+ function collectParams(params) {
3176
+ for (const p of params) {
3177
+ if (p.kind !== "optional") {
3178
+ requiredParams.add(p.name);
3179
+ }
3180
+ }
3181
+ }
3182
+ if (parsedFile.contract?.params) {
3183
+ collectParams(parsedFile.contract.params);
3184
+ }
3185
+ for (const imp of parsedFile.headlessImports) {
3186
+ if (imp.contract?.params) {
3187
+ collectParams(imp.contract.params);
3188
+ }
3189
+ }
3190
+ if (requiredParams.size === 0)
3191
+ return [];
3192
+ const routeParams = extractRouteParams(filePath, pagesBase);
3193
+ const jayParams = extractJayParams(jayHtmlContent);
3194
+ const availableParams = /* @__PURE__ */ new Set([...routeParams, ...jayParams]);
3195
+ const warnings = [];
3196
+ for (const param of requiredParams) {
3197
+ if (!availableParams.has(param)) {
3198
+ warnings.push(
3199
+ `Contract requires param "${param}" but the route does not provide it. Add a dynamic segment [${param}] to the route path or declare it in <script type="application/jay-params">.`
3200
+ );
3201
+ }
3202
+ }
3203
+ return warnings;
3204
+ }
2937
3205
  async function validateJayFiles(options = {}) {
2938
3206
  const config = loadConfig();
2939
3207
  const resolvedConfig = getConfigWithDefaults(config);
2940
- const projectRoot = process.cwd();
3208
+ const projectRoot = options.projectRoot ?? process.cwd();
2941
3209
  const scanDir = options.path ? path.resolve(options.path) : path.resolve(resolvedConfig.devServer.pagesBase);
2942
3210
  const errors = [];
2943
3211
  const warnings = [];
3212
+ const coverage = [];
2944
3213
  const jayHtmlFiles = await findJayFiles(scanDir);
2945
3214
  const contractFiles = await findContractFiles(scanDir);
2946
3215
  if (options.verbose) {
@@ -3006,6 +3275,14 @@ async function validateJayFiles(options = {}) {
3006
3275
  }
3007
3276
  continue;
3008
3277
  }
3278
+ const routeParamWarnings = checkRouteParams(parsedFile.val, jayFile, scanDir, content);
3279
+ for (const msg of routeParamWarnings) {
3280
+ warnings.push({ file: relativePath, message: msg });
3281
+ }
3282
+ const fileCoverage = analyzeTagCoverage(parsedFile.val, relativePath);
3283
+ if (fileCoverage) {
3284
+ coverage.push(fileCoverage);
3285
+ }
3009
3286
  const generatedFile = generateElementFile(
3010
3287
  parsedFile.val,
3011
3288
  RuntimeMode.MainTrusted,
@@ -3041,7 +3318,8 @@ async function validateJayFiles(options = {}) {
3041
3318
  jayHtmlFilesScanned: jayHtmlFiles.length,
3042
3319
  contractFilesScanned: contractFiles.length,
3043
3320
  errors,
3044
- warnings
3321
+ warnings,
3322
+ coverage
3045
3323
  };
3046
3324
  }
3047
3325
  function printJayValidationResult(result, options) {
@@ -3070,6 +3348,38 @@ function printJayValidationResult(result, options) {
3070
3348
  chalk.red(`${result.errors.length} error(s) found, ${validFiles} file(s) valid.`)
3071
3349
  );
3072
3350
  }
3351
+ if (result.warnings.length > 0) {
3352
+ logger.important("");
3353
+ logger.important(chalk.yellow("Warnings:"));
3354
+ for (const warning of result.warnings) {
3355
+ logger.important(chalk.yellow(` ⚠ ${warning.file}`));
3356
+ logger.important(chalk.gray(` ${warning.message}`));
3357
+ logger.important("");
3358
+ }
3359
+ }
3360
+ if (result.coverage.length > 0) {
3361
+ logger.important("");
3362
+ logger.important("Tag Coverage:");
3363
+ for (const fileCov of result.coverage) {
3364
+ logger.important(` ${fileCov.file}`);
3365
+ for (const contract of fileCov.contracts) {
3366
+ const label = contract.key ? `${contract.key} (${contract.contractName})` : contract.contractName;
3367
+ logger.important(
3368
+ ` ${label}: ${contract.usedTags}/${contract.totalTags} tags used`
3369
+ );
3370
+ if (contract.unusedTags.length > 0) {
3371
+ logger.important(chalk.gray(` Unused: ${contract.unusedTags.join(", ")}`));
3372
+ }
3373
+ if (contract.requiredUnusedTags.length > 0) {
3374
+ logger.important(
3375
+ chalk.yellow(
3376
+ ` ⚠ Required unused: ${contract.requiredUnusedTags.join(", ")}`
3377
+ )
3378
+ );
3379
+ }
3380
+ }
3381
+ }
3382
+ }
3073
3383
  }
3074
3384
  async function initializeServicesForCli(projectRoot, viteServer) {
3075
3385
  const path2 = await import("node:path");
@@ -3553,12 +3863,14 @@ async function runMaterialize(projectRoot, options, defaultOutputRelative, keepV
3553
3863
  services
3554
3864
  );
3555
3865
  if (options.yaml) {
3556
- getLogger().important(YAML.stringify(result.index));
3866
+ getLogger().important(YAML.stringify(result.pluginsIndex));
3557
3867
  } else {
3558
- getLogger().important(
3559
- chalk.green(`
3560
- ✅ Materialized ${result.index.contracts.length} contracts`)
3868
+ const totalContracts = result.pluginsIndex.plugins.reduce(
3869
+ (sum, p) => sum + p.contracts.length,
3870
+ 0
3561
3871
  );
3872
+ getLogger().important(chalk.green(`
3873
+ ✅ Materialized ${totalContracts} contracts`));
3562
3874
  getLogger().important(` Static: ${result.staticCount}`);
3563
3875
  getLogger().important(` Dynamic: ${result.dynamicCount}`);
3564
3876
  getLogger().important(` Output: ${result.outputDir}`);
@@ -3670,21 +3982,15 @@ function printValidationResult(result, verbose) {
3670
3982
  function printContractList(index) {
3671
3983
  const logger = getLogger();
3672
3984
  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) {
3985
+ for (const plugin of index.plugins) {
3986
+ logger.important(chalk.bold(`📦 ${plugin.name}`));
3987
+ for (const contract of plugin.contracts) {
3682
3988
  const typeIcon = contract.type === "static" ? "📄" : "⚡";
3683
3989
  logger.important(` ${typeIcon} ${contract.name}`);
3684
3990
  }
3685
3991
  logger.important("");
3686
3992
  }
3687
- if (index.contracts.length === 0) {
3993
+ if (index.plugins.length === 0) {
3688
3994
  logger.important(chalk.gray("No contracts found."));
3689
3995
  }
3690
3996
  }
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.15.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -24,23 +24,24 @@
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.15.0",
28
+ "@jay-framework/compiler-shared": "^0.15.0",
29
+ "@jay-framework/dev-server": "^0.15.0",
30
+ "@jay-framework/editor-server": "^0.15.0",
31
+ "@jay-framework/fullstack-component": "^0.15.0",
32
+ "@jay-framework/logger": "^0.15.0",
33
+ "@jay-framework/plugin-validator": "^0.15.0",
34
+ "@jay-framework/stack-server-runtime": "^0.15.0",
35
35
  "chalk": "^4.1.2",
36
36
  "commander": "^14.0.0",
37
37
  "express": "^5.0.1",
38
38
  "glob": "^10.3.10",
39
+ "node-html-parser": "^6.1.12",
39
40
  "vite": "^5.0.11",
40
41
  "yaml": "^2.3.4"
41
42
  },
42
43
  "devDependencies": {
43
- "@jay-framework/dev-environment": "^0.13.0",
44
+ "@jay-framework/dev-environment": "^0.15.0",
44
45
  "@types/express": "^5.0.2",
45
46
  "@types/node": "^22.15.21",
46
47
  "nodemon": "^3.0.3",