@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.
- package/agent-kit-template/INSTRUCTIONS.md +3 -3
- package/agent-kit-template/cli-commands.md +1 -2
- package/agent-kit-template/contracts-and-plugins.md +11 -24
- package/agent-kit-template/jay-html-syntax.md +37 -0
- package/agent-kit-template/routing.md +36 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +324 -18
- package/package.json +11 -10
|
@@ -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 `
|
|
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 `
|
|
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
|
-
- `
|
|
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 `
|
|
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
|
|
253
|
-
|
|
|
254
|
-
| `propName: string`
|
|
255
|
-
| `propName?: number`
|
|
256
|
-
| `propName: boolean`
|
|
257
|
-
| `propName: enum(a \| b \| c)`
|
|
258
|
-
| `propName:` + nested block
|
|
259
|
-
| `propName:` + `- childProp: type` | Array of objects (YAML list)
|
|
260
|
-
| `propName: importedName`
|
|
261
|
-
| `- importedName`
|
|
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
|
|
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.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 {
|
|
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
|
|
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.
|
|
3866
|
+
getLogger().important(YAML.stringify(result.pluginsIndex));
|
|
3557
3867
|
} else {
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
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
|
|
3674
|
-
|
|
3675
|
-
const
|
|
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.
|
|
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.
|
|
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",
|