@jay-framework/jay-stack-cli 0.14.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.
@@ -31,7 +31,7 @@ There is no standalone "interactive" phase. Any tag with `type: interactive` (re
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
 
@@ -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.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],
@@ -3125,6 +3126,82 @@ function analyzeTagCoverage(jayHtml, file) {
3125
3126
  }
3126
3127
  return { file, contracts };
3127
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
+ }
3128
3205
  async function validateJayFiles(options = {}) {
3129
3206
  const config = loadConfig();
3130
3207
  const resolvedConfig = getConfigWithDefaults(config);
@@ -3198,6 +3275,10 @@ async function validateJayFiles(options = {}) {
3198
3275
  }
3199
3276
  continue;
3200
3277
  }
3278
+ const routeParamWarnings = checkRouteParams(parsedFile.val, jayFile, scanDir, content);
3279
+ for (const msg of routeParamWarnings) {
3280
+ warnings.push({ file: relativePath, message: msg });
3281
+ }
3201
3282
  const fileCoverage = analyzeTagCoverage(parsedFile.val, relativePath);
3202
3283
  if (fileCoverage) {
3203
3284
  coverage.push(fileCoverage);
@@ -3267,6 +3348,15 @@ function printJayValidationResult(result, options) {
3267
3348
  chalk.red(`${result.errors.length} error(s) found, ${validFiles} file(s) valid.`)
3268
3349
  );
3269
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
+ }
3270
3360
  if (result.coverage.length > 0) {
3271
3361
  logger.important("");
3272
3362
  logger.important("Tag Coverage:");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jay-framework/jay-stack-cli",
3
- "version": "0.14.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.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",
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.14.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",