@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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
28
|
-
"@jay-framework/compiler-shared": "^0.
|
|
29
|
-
"@jay-framework/dev-server": "^0.
|
|
30
|
-
"@jay-framework/editor-server": "^0.
|
|
31
|
-
"@jay-framework/fullstack-component": "^0.
|
|
32
|
-
"@jay-framework/logger": "^0.
|
|
33
|
-
"@jay-framework/plugin-validator": "^0.
|
|
34
|
-
"@jay-framework/stack-server-runtime": "^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.
|
|
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",
|