@jay-framework/jay-stack-cli 0.24.7 → 0.25.1
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/contracts/GUIDE.md +8 -8
- package/agent-kit-template/contracts/linked-contracts.md +1 -1
- package/agent-kit-template/designer/cli-commands.md +7 -0
- package/agent-kit-template/designer/contracts-and-plugins.md +1 -1
- package/agent-kit-template/designer/design-system-guide.md +43 -31
- package/agent-kit-template/designer/jay-html-styling.md +5 -4
- package/agent-kit-template/designer/jay-html-template-syntax.md +1 -1
- package/agent-kit-template/designer/validation-guide.md +59 -2
- package/agent-kit-template/designer/validation-tier-2-guide.md +64 -0
- package/dist/{index-l6QVpvnY.js → index-BQbkAEhi.js} +108 -99
- package/dist/index.js +773 -81
- package/package.json +14 -13
|
@@ -60,11 +60,11 @@ See [syntax.md](syntax.md) for the full YAML format: tag types, phases, data typ
|
|
|
60
60
|
|
|
61
61
|
Start with the simplest example that matches your use case, then add complexity as needed:
|
|
62
62
|
|
|
63
|
-
| Example
|
|
64
|
-
|
|
|
65
|
-
| [mini-cart](examples/mini-cart.md) | Trivial | Variant + interactive refs |
|
|
66
|
-
| [cart-indicator](examples/cart-indicator.md) | Simple | Flat data, variants, phase choices |
|
|
67
|
-
| [category-list](examples/category-list.md) | Medium | Props, repeated sub-contracts |
|
|
68
|
-
| [product-card](examples/product-card.md) | Medium-complex | Linked sub-contracts, variants, multiple ref types |
|
|
69
|
-
| [product-page](examples/product-page.md) | Complex | Params, nested repeated sub-contracts, linked sub-contracts |
|
|
70
|
-
| [composing-contracts](examples/composing-contracts.md) | Pattern | How contracts link together into a hierarchy |
|
|
63
|
+
| Example | Complexity | Key patterns |
|
|
64
|
+
| --------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | ----------------------------------------------------------- |
|
|
65
|
+
| [mini-cart](https://github.com/jay-framework/jay/blob/main/packages/jay-stack/stack-cli/agent-kit-template/contracts/examples/mini-cart.md) | Trivial | Variant + interactive refs |
|
|
66
|
+
| [cart-indicator](https://github.com/jay-framework/jay/blob/main/packages/jay-stack/stack-cli/agent-kit-template/contracts/examples/cart-indicator.md) | Simple | Flat data, variants, phase choices |
|
|
67
|
+
| [category-list](https://github.com/jay-framework/jay/blob/main/packages/jay-stack/stack-cli/agent-kit-template/contracts/examples/category-list.md) | Medium | Props, repeated sub-contracts |
|
|
68
|
+
| [product-card](https://github.com/jay-framework/jay/blob/main/packages/jay-stack/stack-cli/agent-kit-template/contracts/examples/product-card.md) | Medium-complex | Linked sub-contracts, variants, multiple ref types |
|
|
69
|
+
| [product-page](https://github.com/jay-framework/jay/blob/main/packages/jay-stack/stack-cli/agent-kit-template/contracts/examples/product-page.md) | Complex | Params, nested repeated sub-contracts, linked sub-contracts |
|
|
70
|
+
| [composing-contracts](https://github.com/jay-framework/jay/blob/main/packages/jay-stack/stack-cli/agent-kit-template/contracts/examples/composing-contracts.md) | Pattern | How contracts link together into a hierarchy |
|
|
@@ -72,4 +72,4 @@ The linked contract must have a tag matching the `trackBy` field.
|
|
|
72
72
|
|
|
73
73
|
## See Also
|
|
74
74
|
|
|
75
|
-
- [composing-contracts example](examples/composing-contracts.md) — Real-world composition hierarchy from wix-stores
|
|
75
|
+
- [composing-contracts example](https://github.com/jay-framework/jay/blob/main/packages/jay-stack/stack-cli/agent-kit-template/contracts/examples/composing-contracts.md) — Real-world composition hierarchy from wix-stores
|
|
@@ -69,8 +69,15 @@ jay-stack validate -v
|
|
|
69
69
|
|
|
70
70
|
# JSON output
|
|
71
71
|
jay-stack validate --json
|
|
72
|
+
|
|
73
|
+
# Tier 2: deeper checks against an existing build (needs `jay-stack build` first)
|
|
74
|
+
jay-stack validate --tier-2
|
|
72
75
|
```
|
|
73
76
|
|
|
77
|
+
The default run is **Tier 1** (fast, template-only). `--tier-2` adds deep checks against a build's output
|
|
78
|
+
(dynamic-slug links, rendered-content links, per-instance meta) — see
|
|
79
|
+
[validation-tier-2-guide.md](validation-tier-2-guide.md).
|
|
80
|
+
|
|
74
81
|
Example output:
|
|
75
82
|
|
|
76
83
|
```
|
|
@@ -327,4 +327,4 @@ For repeated lists, add a **boolean variant** (e.g. `hasItems`, `hasCategories`)
|
|
|
327
327
|
<div if="hasCategories" forEach="categories" trackBy="_id">...</div>
|
|
328
328
|
```
|
|
329
329
|
|
|
330
|
-
Alternatively, expose a **number** data tag (`itemCount`) and use numeric comparison: `if="itemCount===0"`. See [jay-html-template-syntax.md](jay-html-template-syntax.md#expression-limits-important) and [contracts/examples/category-list.md](
|
|
330
|
+
Alternatively, expose a **number** data tag (`itemCount`) and use numeric comparison: `if="itemCount===0"`. See [jay-html-template-syntax.md](jay-html-template-syntax.md#expression-limits-important) and [contracts/examples/category-list.md](https://github.com/jay-framework/jay/blob/main/packages/jay-stack/stack-cli/agent-kit-template/contracts/examples/category-list.md).
|
|
@@ -148,8 +148,8 @@ component's **own** tags, and an optional `<style>` of **plain component CSS**.
|
|
|
148
148
|
```
|
|
149
149
|
|
|
150
150
|
> **Author plain CSS here, not `@scope`.** In the template you write ordinary class rules
|
|
151
|
-
> (`.ds-card { … }`). When a page flattens the element, `sync` copies
|
|
152
|
-
>
|
|
151
|
+
> (`.ds-card { … }`). When a page flattens the element, `sync` copies these rules into the page verbatim and
|
|
152
|
+
> wraps them in a canonical `@scope (.<ref>)` block for you — that scoped wrapper is a _page-side_ concern
|
|
153
153
|
> (see [The shape of a region's CSS](#the-shape-of-a-regions-css)); you don't write it in the template.
|
|
154
154
|
|
|
155
155
|
### 4. Choose the right axis of variation
|
|
@@ -217,7 +217,7 @@ the page's `<style>`, `@scope`-wrapped by the region's `ref`:
|
|
|
217
217
|
|
|
218
218
|
```html
|
|
219
219
|
<jay:card ref="promo" heading="{title}">
|
|
220
|
-
<div class="card
|
|
220
|
+
<div class="card">
|
|
221
221
|
<h3 class="card-heading">{heading}</h3>
|
|
222
222
|
<p class="card-body">Default body</p>
|
|
223
223
|
</div>
|
|
@@ -226,7 +226,7 @@ the page's `<style>`, `@scope`-wrapped by the region's `ref`:
|
|
|
226
226
|
|
|
227
227
|
```css
|
|
228
228
|
@scope (.promo) {
|
|
229
|
-
|
|
229
|
+
.card {
|
|
230
230
|
border: 1px solid #ccc;
|
|
231
231
|
padding: 16px;
|
|
232
232
|
}
|
|
@@ -236,8 +236,9 @@ the page's `<style>`, `@scope`-wrapped by the region's `ref`:
|
|
|
236
236
|
}
|
|
237
237
|
```
|
|
238
238
|
|
|
239
|
-
|
|
240
|
-
|
|
239
|
+
The flattened body is the template body **verbatim** — no synthetic class on the root — and the CSS is the
|
|
240
|
+
component's own plain rules wrapped in one `@scope (.<ref>)` block. **Write region CSS in this shape
|
|
241
|
+
yourself**; the rules are below in [The shape of a region's CSS](#the-shape-of-a-regions-css).
|
|
241
242
|
|
|
242
243
|
> A bare `<jay:X>` with **no** flattened body is a hard error at build time — flatten it with `jay-stack
|
|
243
244
|
sync` (first-fill is just the no-edits case of sync; there is no separate `add` command).
|
|
@@ -292,9 +293,9 @@ the second import its own region tag with `as=`:
|
|
|
292
293
|
|
|
293
294
|
## The shape of a region's CSS
|
|
294
295
|
|
|
295
|
-
A region's CSS is the component's own CSS, copied into the page `<style>` and
|
|
296
|
-
|
|
297
|
-
|
|
296
|
+
A region's CSS is the component's own CSS, copied into the page `<style>` and wrapped in one `@scope` block.
|
|
297
|
+
`sync` produces this form and `validate` enforces it — but when you **hand-write or edit** a region's CSS,
|
|
298
|
+
write it this way directly:
|
|
298
299
|
|
|
299
300
|
### 1. Wrap the component's rules in `@scope (.<ref>)`
|
|
300
301
|
|
|
@@ -302,52 +303,47 @@ All of a region's rules live inside one `@scope` block keyed by the region's `re
|
|
|
302
303
|
|
|
303
304
|
```css
|
|
304
305
|
@scope (.promo) {
|
|
305
|
-
/* …the card's rules… */
|
|
306
|
+
/* …the card's rules, as plain class selectors… */
|
|
306
307
|
}
|
|
307
308
|
```
|
|
308
309
|
|
|
309
310
|
This isolates the component's styles to that region — rules inside never leak out, and page rules outside
|
|
310
311
|
never bleed in.
|
|
311
312
|
|
|
312
|
-
### 2.
|
|
313
|
-
|
|
314
|
-
A jay `ref` is not emitted to the DOM, so `@scope (.promo)` would have nothing to match. The region root
|
|
315
|
-
therefore carries the ref **as an actual class** — `<div class="card promo">` above. Keep that class on the
|
|
316
|
-
root when you edit markup; it is what anchors the scope. (validate's drift check ignores this synthetic
|
|
317
|
-
class, so it never shows up as drift.)
|
|
313
|
+
### 2. Write plain class selectors — the ref is _not_ a class on your markup
|
|
318
314
|
|
|
319
|
-
|
|
315
|
+
A jay `ref` is not emitted to the DOM, so `@scope (.promo)` would have nothing to match. The compiler
|
|
316
|
+
therefore synthesizes a `<div class="promo" style="display: contents">` **scope-anchor wrapper** around the
|
|
317
|
+
region body at build time (DL#206). It is layout-transparent (`display:contents` generates no box) and you
|
|
318
|
+
**never write it, see it in source, or style it** — it exists only so `.promo` has a real element to root at.
|
|
320
319
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
320
|
+
Because the wrapper is the scope root, your real roots are ordinary **descendants** of it. So every rule —
|
|
321
|
+
including the component's own root element — is a **plain class selector**, exactly as authored in the
|
|
322
|
+
template. Keep the template's classes on your markup unchanged; add **no** ref class:
|
|
324
323
|
|
|
325
324
|
```css
|
|
326
325
|
@scope (.promo) {
|
|
327
|
-
:scope {
|
|
328
|
-
border: 1px solid #ccc;
|
|
329
|
-
} /* ✅ styles the region root (<div class="card promo">) */
|
|
330
326
|
.card {
|
|
331
327
|
border: 1px solid #ccc;
|
|
332
|
-
} /*
|
|
328
|
+
} /* ✅ the region root — a plain class selector, matched as a descendant of the wrapper */
|
|
333
329
|
.card-heading {
|
|
334
330
|
color: black;
|
|
335
|
-
} /* ✅ descendant — plain class selector
|
|
331
|
+
} /* ✅ descendant — also a plain class selector */
|
|
336
332
|
}
|
|
337
333
|
```
|
|
338
334
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
335
|
+
> **No `:scope` rewrite.** In the old model the root rule was rewritten to `:scope` and the ref was stamped
|
|
336
|
+
> onto the root as a class. DL#206 removed both: page region CSS is now byte-for-byte the template's plain
|
|
337
|
+
> rules, just wrapped in `@scope (.<ref>)`. `:scope` would now refer to the invisible wrapper — don't use it.
|
|
342
338
|
|
|
343
|
-
###
|
|
339
|
+
### 3. Instances of the same component coalesce into one block
|
|
344
340
|
|
|
345
341
|
When a page holds several regions flattened from the _same_ template, they share one `@scope` block with a
|
|
346
342
|
selector list — never one duplicated block per `ref`:
|
|
347
343
|
|
|
348
344
|
```css
|
|
349
345
|
@scope (.cardStarter, .cardPro) {
|
|
350
|
-
|
|
346
|
+
.card {
|
|
351
347
|
border: 1px solid #ccc;
|
|
352
348
|
}
|
|
353
349
|
.card-heading {
|
|
@@ -359,6 +355,21 @@ selector list — never one duplicated block per `ref`:
|
|
|
359
355
|
Regions from _different_ templates stay in separate blocks. If you add a second instance by hand, fold its
|
|
360
356
|
ref into the existing block's selector list rather than copying the block.
|
|
361
357
|
|
|
358
|
+
### 4. A CSS-shipping region must have a `ref` (error)
|
|
359
|
+
|
|
360
|
+
The `@scope (.<ref>)` anchor is derived from the region's `ref=`. A region whose `template=` ships CSS but
|
|
361
|
+
whose `<jay:X>` tag has **no `ref`** has nothing to scope to — the materialiser cannot wrap the CSS and
|
|
362
|
+
`jay-stack sync` silently drops it. `validate` catches this as a **hard error** (`REGION-CSS-NO-REF`, DL#209):
|
|
363
|
+
|
|
364
|
+
```
|
|
365
|
+
<jay:card> flattens template="./components/card/card.jay-html" which ships CSS, but the region has no ref=
|
|
366
|
+
— so its CSS cannot be scoped and `jay-stack sync` will silently drop it.
|
|
367
|
+
→ Add a ref= to the <jay:card> so sync can scope its CSS as an @scope (.<ref>) block and preserve it.
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Fix it by adding a `ref` — there is no suppression, because the alternative (keeping the CSS unscoped) would
|
|
371
|
+
leak the region's styles across the whole page.
|
|
372
|
+
|
|
362
373
|
## Drift: what `jay-stack validate` tells you
|
|
363
374
|
|
|
364
375
|
Because the copy has a known source (its `template=`), `jay-stack validate` diffs the two and reports each
|
|
@@ -490,7 +501,8 @@ CSS reconciles the same way as markup — drift is overwritten, marked facets ar
|
|
|
490
501
|
never rewritten.
|
|
491
502
|
- A block that **diverges without any pragma** is unmarked CSS drift: `sync` overwrites it back to the
|
|
492
503
|
canonical form, exactly as it re-flattens unmarked markup. (This is what lets `sync` _migrate_ a page to a
|
|
493
|
-
new canonical form — e.g. an older
|
|
504
|
+
new canonical form — e.g. an older `:scope { … }` / ref-on-root block is rewritten to the DL#206 form:
|
|
505
|
+
plain class rules wrapped in `@scope (.<ref>)`.)
|
|
494
506
|
|
|
495
507
|
So after one `sync` a page's CSS is in canonical form, and a `validate`-clean page is one `sync` leaves
|
|
496
508
|
unchanged. To keep a hand-edit through `sync`, mark it with a `jay:override` pragma.
|
|
@@ -19,12 +19,13 @@ Add `<style>` blocks in `<head>`:
|
|
|
19
19
|
</head>
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
## Styling a design-system region (`@scope`
|
|
22
|
+
## Styling a design-system region (`@scope`)
|
|
23
23
|
|
|
24
24
|
CSS for a `<jay:X>` **design-system element** (a region with `template=` provenance) is not written as plain
|
|
25
|
-
page rules — it lives in an `@scope (.<ref>)` block
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
page rules — it lives in an `@scope (.<ref>)` block keyed by the region's `ref`. Inside that block you write
|
|
26
|
+
the component's **plain class rules** exactly as authored in the template (`.ds-card { … }`); the compiler
|
|
27
|
+
synthesizes a `display:contents` scope-anchor so `.<ref>` has something to match — you never write it or use
|
|
28
|
+
`:scope`. This has its own rules and is covered in full in
|
|
28
29
|
[design-system-guide.md → The shape of a region's CSS](design-system-guide.md#the-shape-of-a-regions-css).
|
|
29
30
|
The plain-`<style>` patterns on this page apply to ordinary page markup.
|
|
30
31
|
|
|
@@ -240,7 +240,7 @@ Jay-html expressions resolve **contract or page tag names only**. They are **not
|
|
|
240
240
|
| Empty list hint | `hasItems: boolean` variant | `if="!hasItems"` |
|
|
241
241
|
| Count-based UI | `itemCount: number` data tag | `if="itemCount===0"` |
|
|
242
242
|
|
|
243
|
-
See [contracts/examples/category-list.md](
|
|
243
|
+
See [contracts/examples/category-list.md](https://github.com/jay-framework/jay/blob/main/packages/jay-stack/stack-cli/agent-kit-template/contracts/examples/category-list.md) (`hasCategories`).
|
|
244
244
|
|
|
245
245
|
### Common Errors
|
|
246
246
|
|
|
@@ -27,6 +27,18 @@ So a warning about an `<img>` might come from a full-stack component's template,
|
|
|
27
27
|
- **Errors** block the build. They must be fixed — there is no way to suppress them.
|
|
28
28
|
- **Warnings** must be either fixed or explicitly suppressed. Do not ignore warnings — each one has a clear resolution path (add the missing attribute, or suppress via `<script type="application/jay-validations">`).
|
|
29
29
|
|
|
30
|
+
**Broken internal links are errors** (DL#210). `validate` resolves every `<a href>` against the project's
|
|
31
|
+
**static routes + public assets**. An href that resolves to nothing is an error: a placeholder (`href="#"` or
|
|
32
|
+
empty), a typo, or a link to a page/asset that does not exist. Links that match a **dynamic** route pattern
|
|
33
|
+
(e.g. `/blog/[slug]`) are **not** checked here — the concrete slug list isn't known at validate time — so they
|
|
34
|
+
never false-error. External URLs, `mailto:`/`tel:`, in-page `#fragments`, and `{binding}` hrefs are skipped.
|
|
35
|
+
Fix the path, use `<button>` for JS-driven anchors, or suppress per-page (see the `jay-stack` table below).
|
|
36
|
+
|
|
37
|
+
One design-system rule is a **hard error**, not a warning: `REGION-CSS-NO-REF` (DL#209) — a `<jay:X>` region
|
|
38
|
+
whose `template=` ships CSS but which has no `ref=`. Without a `ref` the CSS has no scope anchor, so `sync`
|
|
39
|
+
silently drops it; the fix is to add a `ref` (see `design-system-guide.md` → _The shape of a region's CSS_).
|
|
40
|
+
It is unsuppressible because the alternative would leak the region's styles across the whole page.
|
|
41
|
+
|
|
30
42
|
## How to Read Warnings
|
|
31
43
|
|
|
32
44
|
Each warning has:
|
|
@@ -121,6 +133,7 @@ The core `jay-stack validate` emits four warnings that nudge you to reuse UI thr
|
|
|
121
133
|
| `REGION-OVERRIDE-NON-CONTENT` | `jay-validations="REGION-OVERRIDE-NON-CONTENT"` on the import |
|
|
122
134
|
| `COMPONENT-NO-TEMPLATE` | `jay-stack: allow-no-template: [Contract]` |
|
|
123
135
|
| `NO-DESIGN-SYSTEM` | `jay-stack: allow-no-design-system: true` |
|
|
136
|
+
| Broken internal link (DL#210) | `jay-stack: allow-broken-links: true` (per-page — skips all link checks for that page) |
|
|
124
137
|
|
|
125
138
|
```html
|
|
126
139
|
<script type="application/jay-validations">
|
|
@@ -135,11 +148,55 @@ The core `jay-stack validate` emits four warnings that nudge you to reuse UI thr
|
|
|
135
148
|
|
|
136
149
|
If a warning comes from dynamic content (`{post.content}`) or a generated file, you can't suppress it in the template. This is a validator limitation — the warning is a false positive. Don't loop trying to fix it.
|
|
137
150
|
|
|
151
|
+
## Design-system scorecard (metrics to optimize)
|
|
152
|
+
|
|
153
|
+
Below the warnings, `validate` prints a **report-only scorecard** (DL#207). It is **never** a pass/fail — it
|
|
154
|
+
emits no warnings and never affects the exit code. It turns "prefer design-system elements" from a nudge into
|
|
155
|
+
two numbers you can deliberately raise:
|
|
156
|
+
|
|
157
|
+
- **Tag coverage** — of every contract tag/ref a page's regions _could_ bind, how many it actually binds.
|
|
158
|
+
- **Design-system coverage %** — what share of a page's DOM elements come from `template=`-backed
|
|
159
|
+
design-system regions (vs. hand-authored markup). Higher = more of the page is built from the design system.
|
|
160
|
+
- **Reuse** — how many catalogued templates are used **more than once** across the project. A design system
|
|
161
|
+
whose templates are each used once isn't really a design system.
|
|
162
|
+
|
|
163
|
+
The healthy target: **most of each page is composed of design-system elements, and each template is reused
|
|
164
|
+
more than once.** When building a page, prefer linking an existing template (raising coverage and reuse) over
|
|
165
|
+
hand-authoring markup.
|
|
166
|
+
|
|
167
|
+
By default the scorecard prints **one-line totals** for the whole project:
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
📦 Tag coverage: 100% (43/43 tags used across 10 page(s))
|
|
171
|
+
📊 Design-system scorecard: 80% element coverage (74/92), 2 of 5 catalogued templates reused > 1
|
|
172
|
+
Run validate -v for per-page details.
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Run with `-v` / `--verbose` to expand the **per-page** breakdown (coverage per file, which tags are unused,
|
|
176
|
+
per-template reuse counts, and `⚠ low` flags on pages under 50% coverage).
|
|
177
|
+
|
|
178
|
+
## Two validation tiers
|
|
179
|
+
|
|
180
|
+
Validation comes in two tiers. This guide covers **Tier 1**, the always-on gate.
|
|
181
|
+
|
|
182
|
+
**Tier 1 — always on (`jay-stack validate`)** is the fast, template-only gate. It reads your `.jay-html` files and
|
|
183
|
+
route scan only — **no build needed** — so it's cheap enough for the hot agent loop and runs on every `validate`.
|
|
184
|
+
Everything described in this guide is Tier 1. For links, Tier 1 (DL#210) resolves every hand-authored `<a href>`
|
|
185
|
+
against the project's static routes + public assets and **errors** on broken internal links (typos, missing
|
|
186
|
+
pages), degenerate `#` / empty placeholder links, and self-links. Links matching a **dynamic** route pattern
|
|
187
|
+
(`/design-log/[...slug]`) are deferred — the concrete slug set isn't known without a build.
|
|
188
|
+
|
|
189
|
+
**Tier 2 — opt-in, against a build (`jay-stack validate --tier-2`)** adds deeper checks that need a build:
|
|
190
|
+
dynamic-slug links, broken links in rendered content, and per-instance meta/SEO. It's documented separately to
|
|
191
|
+
keep this guide focused — see **`validation-tier-2-guide.md`**. Run it pre-deploy / in CI.
|
|
192
|
+
|
|
138
193
|
## Running Validation
|
|
139
194
|
|
|
140
195
|
```bash
|
|
141
|
-
jay-stack validate
|
|
142
|
-
jay-stack validate
|
|
196
|
+
jay-stack validate # Tier 1: all pages (+ one-line scorecard totals)
|
|
197
|
+
jay-stack validate -v # add per-page tag coverage + scorecard detail
|
|
198
|
+
jay-stack validate --strict # treat warnings as errors
|
|
199
|
+
jay-stack validate --tier-2 # also run Tier 2 — see validation-tier-2-guide.md
|
|
143
200
|
```
|
|
144
201
|
|
|
145
202
|
Validation runs automatically during `jay-stack build`. Warnings don't block the build; errors do (with `--strict`).
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Tier 2 Validation — Deep Checks Against a Build
|
|
2
|
+
|
|
3
|
+
`jay-stack validate` runs **Tier 1**: the fast, template-only gate (links, placeholders, coverage) that needs no
|
|
4
|
+
build. See `validation-guide.md` for Tier 1.
|
|
5
|
+
|
|
6
|
+
**Tier 2** (`jay-stack validate --tier-2`) adds deeper checks that are only possible once a build exists. This
|
|
7
|
+
guide covers Tier 2 end to end.
|
|
8
|
+
|
|
9
|
+
## What Tier 2 is
|
|
10
|
+
|
|
11
|
+
Deep validation against an **existing build's output** (DL#211). It reads
|
|
12
|
+
`build/v<version>/backend/route-manifest.json` plus the per-instance `*.cache.json` slow-render results. It does
|
|
13
|
+
**not** run any render, so it's cheap — but it **requires a prior `jay-stack build`**. If no build is found, Tier 2
|
|
14
|
+
reports a single actionable error (run `jay-stack build` first, or drop `--tier-2`).
|
|
15
|
+
|
|
16
|
+
Run it **pre-deploy / in CI**, not in the hot authoring loop.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
jay-stack validate --tier-2 # validate against the highest build/v* output
|
|
20
|
+
jay-stack validate --t2 # short alias
|
|
21
|
+
jay-stack validate --tier-2 --build-dir <dir> # point at a specific build backend dir or build root
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Tier 2 findings print under their own `📦 Tier 2 — build-output validation (--tier-2)` section, separate from the
|
|
25
|
+
Tier 1 `📦 jay-stack (core)` section.
|
|
26
|
+
|
|
27
|
+
## The three checks
|
|
28
|
+
|
|
29
|
+
All run against the concrete URLs the build actually produced (routes × instances).
|
|
30
|
+
|
|
31
|
+
1. **Deferred dynamic-slug links** (ERROR) — the template links Tier 1 could only defer (because they matched a
|
|
32
|
+
dynamic pattern like `/design-log/[...slug]`) are resolved against the real URL set. A link to a dynamic route
|
|
33
|
+
with a slug the build never generated (e.g. `/design-log/wix/index`) is caught here.
|
|
34
|
+
|
|
35
|
+
2. **Broken links in rendered content** (WARNING) — `<a href>` found _inside_ the slow-rendered ViewState
|
|
36
|
+
(markdown bodies, descriptions) that resolve to a URL the build does not produce. Relative hrefs are resolved
|
|
37
|
+
against the instance's own URL. These are **warnings**, not errors, because content often doubles as repo/GitHub
|
|
38
|
+
docs where repo-relative links like `../pkg/foo.ts` are legitimately correct.
|
|
39
|
+
|
|
40
|
+
3. **Per-instance meta/SEO** (mostly WARNING) — resolves each route's `headMeta` template against each instance's
|
|
41
|
+
slow ViewState and validates the concrete `<title>` / `<meta name="description">`:
|
|
42
|
+
- Empty title/description, a title > 60 chars, or a description > 160 chars → **warning**.
|
|
43
|
+
- A leftover unresolved `{binding}` (a field never produced at any phase) → **error**.
|
|
44
|
+
- Note (DL#189): a field bound to the fast/interactive phase is legitimately empty at slow render, so
|
|
45
|
+
**emptiness is only ever a warning, never an error**.
|
|
46
|
+
|
|
47
|
+
If the build is older than your current source files, a **staleness warning** is emitted ("validated against a
|
|
48
|
+
build from <timestamp>; source has changed since") suggesting a rebuild.
|
|
49
|
+
|
|
50
|
+
## Suppressing Tier 2 findings
|
|
51
|
+
|
|
52
|
+
Per-page, via the same `<script type="application/jay-validations">` block used for Tier 1, under the `jay-stack:`
|
|
53
|
+
key:
|
|
54
|
+
|
|
55
|
+
- `allow-broken-links: true` — suppresses link findings (both template and rendered-content links) for that page.
|
|
56
|
+
- `allow-meta-issues: true` — suppresses meta/SEO findings for that page.
|
|
57
|
+
|
|
58
|
+
```html
|
|
59
|
+
<script type="application/jay-validations">
|
|
60
|
+
jay-stack:
|
|
61
|
+
allow-broken-links: true
|
|
62
|
+
allow-meta-issues: true
|
|
63
|
+
</script>
|
|
64
|
+
```
|
|
@@ -411,6 +411,102 @@ function buildPagePartsConfig(pageParts, pageServerModule, pageExportName, build
|
|
|
411
411
|
}))
|
|
412
412
|
};
|
|
413
413
|
}
|
|
414
|
+
function crossProductParams(parts) {
|
|
415
|
+
if (parts.length === 0) return [];
|
|
416
|
+
if (parts.length === 1) return parts[0].values;
|
|
417
|
+
const logger = getLogger();
|
|
418
|
+
for (let i = 0; i < parts.length; i++) {
|
|
419
|
+
for (let j = i + 1; j < parts.length; j++) {
|
|
420
|
+
for (const key of parts[i].keys) {
|
|
421
|
+
if (parts[j].keys.has(key)) {
|
|
422
|
+
logger.warn(
|
|
423
|
+
`[Build] Multiple loadParams provide key "${key}" — using first provider`
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
let result = parts[0].values;
|
|
430
|
+
for (let i = 1; i < parts.length; i++) {
|
|
431
|
+
const next = parts[i].values;
|
|
432
|
+
const combined = [];
|
|
433
|
+
for (const a of result) {
|
|
434
|
+
for (const b of next) {
|
|
435
|
+
combined.push({ ...a, ...b });
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
result = combined;
|
|
439
|
+
}
|
|
440
|
+
return result;
|
|
441
|
+
}
|
|
442
|
+
function paramsMatchInferred(params, inferredParams, optionalSegments) {
|
|
443
|
+
return Object.entries(inferredParams).every(([k, v]) => {
|
|
444
|
+
if (optionalSegments?.has(k)) return true;
|
|
445
|
+
return params[k] === v;
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
function computeSpecificity(route) {
|
|
449
|
+
const dynamicCount = (route.rawRoute.match(/\[/g) || []).length;
|
|
450
|
+
const inferredCount = route.inferredParams ? Object.keys(route.inferredParams).length : 0;
|
|
451
|
+
const unresolvedCount = Math.max(0, dynamicCount - inferredCount);
|
|
452
|
+
return 0 - unresolvedCount;
|
|
453
|
+
}
|
|
454
|
+
function buildUrl(route, params) {
|
|
455
|
+
return route.rawRoute.replace(/\[\[(\w+)\]\]/g, (_, name) => {
|
|
456
|
+
const value = params[name];
|
|
457
|
+
if (!value) return "";
|
|
458
|
+
if (route.inferredParams?.[name] === value) return "";
|
|
459
|
+
return value;
|
|
460
|
+
}).replace(/\[(\w+)\]/g, (_, name) => params[name] || "").replace(/\/\/+/g, "/").replace(/\/$/, "") || "/";
|
|
461
|
+
}
|
|
462
|
+
function materializeRouteParams(routes, loadParamsResults) {
|
|
463
|
+
const entries = [];
|
|
464
|
+
for (const route of routes) {
|
|
465
|
+
const specificity = computeSpecificity(route);
|
|
466
|
+
if (!route.hasDynamicParams) {
|
|
467
|
+
const params = route.inferredParams || {};
|
|
468
|
+
entries.push({ route, params, url: route.rawRoute, specificity });
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
const allParams = loadParamsResults.get(route) || [];
|
|
472
|
+
for (const params of allParams) {
|
|
473
|
+
if (route.inferredParams && !paramsMatchInferred(params, route.inferredParams, route.optionalSegments)) {
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
let mergedParams = params;
|
|
477
|
+
if (route.inferredParams) {
|
|
478
|
+
mergedParams = { ...params };
|
|
479
|
+
for (const [k, v] of Object.entries(route.inferredParams)) {
|
|
480
|
+
if (!(k in mergedParams)) {
|
|
481
|
+
mergedParams[k] = v;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
const url = buildUrl(route, mergedParams);
|
|
486
|
+
entries.push({ route, params: mergedParams, url, specificity });
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return entries;
|
|
490
|
+
}
|
|
491
|
+
function dedupeByUrl(entries) {
|
|
492
|
+
const logger = getLogger();
|
|
493
|
+
const byUrl = /* @__PURE__ */ new Map();
|
|
494
|
+
for (const entry of entries) {
|
|
495
|
+
const existing = byUrl.get(entry.url);
|
|
496
|
+
if (!existing) {
|
|
497
|
+
byUrl.set(entry.url, entry);
|
|
498
|
+
} else if (entry.specificity > existing.specificity) {
|
|
499
|
+
byUrl.set(entry.url, entry);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
const deduped = [...byUrl.values()];
|
|
503
|
+
if (deduped.length < entries.length) {
|
|
504
|
+
logger.info(
|
|
505
|
+
`[Build] Deduplication: ${entries.length} materialized → ${deduped.length} unique URLs`
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
return deduped;
|
|
509
|
+
}
|
|
414
510
|
function hashParams(params, suffix) {
|
|
415
511
|
const sorted = Object.keys(params).sort().reduce(
|
|
416
512
|
(acc, key) => {
|
|
@@ -472,10 +568,11 @@ async function buildInstance(route, params, pageModule, ctx, routeServerElementP
|
|
|
472
568
|
);
|
|
473
569
|
await fs.writeFile(pagePartsConfigPath, JSON.stringify(config, null, 2));
|
|
474
570
|
logger.info(`[Build] Page parts config: ${routeDir}/page-parts.json`);
|
|
571
|
+
const pageUrl = buildUrl(route, params);
|
|
475
572
|
const slowPhase = new DevSlowlyChangingPhase();
|
|
476
573
|
const slowResult = await slowPhase.runSlowlyForPage(
|
|
477
574
|
params,
|
|
478
|
-
{
|
|
575
|
+
{ language: "en", url: pageUrl },
|
|
479
576
|
pageParts.parts,
|
|
480
577
|
pageParts.discoveredInstances,
|
|
481
578
|
pageParts.headlessInstanceComponents,
|
|
@@ -501,7 +598,7 @@ async function buildInstance(route, params, pageModule, ctx, routeServerElementP
|
|
|
501
598
|
{
|
|
502
599
|
pageViewState: slowViewState,
|
|
503
600
|
pageParams: params,
|
|
504
|
-
pageProps: { language: "en", url:
|
|
601
|
+
pageProps: { language: "en", url: pageUrl }
|
|
505
602
|
}
|
|
506
603
|
);
|
|
507
604
|
if (slowResult2) {
|
|
@@ -764,7 +861,12 @@ async function compileServerElement(jayHtmlContent, jayHtmlFilename, jayHtmlDir,
|
|
|
764
861
|
cssFile = cssFilename;
|
|
765
862
|
}
|
|
766
863
|
getLogger().info(`[Build] Compiled server element: ${path__default.basename(outputPath)}`);
|
|
767
|
-
return {
|
|
864
|
+
return {
|
|
865
|
+
cssFile,
|
|
866
|
+
cssImports,
|
|
867
|
+
headMeta: parsedJayFile.headMeta,
|
|
868
|
+
scripts: parsedJayFile.scripts
|
|
869
|
+
};
|
|
768
870
|
}
|
|
769
871
|
async function compileRouteServerElement(jayHtmlPath, outputPath, projectRoot, tsConfigFilePath, minifyCss = true) {
|
|
770
872
|
const jayHtmlContent = await fs.readFile(jayHtmlPath, "utf-8");
|
|
@@ -974,102 +1076,6 @@ async function buildInstanceClient(hydrateEntryPath, instanceId, outputDir, proj
|
|
|
974
1076
|
logger.info(`[Build] Client bundle: ${result.jsFile}`);
|
|
975
1077
|
return result;
|
|
976
1078
|
}
|
|
977
|
-
function crossProductParams(parts) {
|
|
978
|
-
if (parts.length === 0) return [];
|
|
979
|
-
if (parts.length === 1) return parts[0].values;
|
|
980
|
-
const logger = getLogger();
|
|
981
|
-
for (let i = 0; i < parts.length; i++) {
|
|
982
|
-
for (let j = i + 1; j < parts.length; j++) {
|
|
983
|
-
for (const key of parts[i].keys) {
|
|
984
|
-
if (parts[j].keys.has(key)) {
|
|
985
|
-
logger.warn(
|
|
986
|
-
`[Build] Multiple loadParams provide key "${key}" — using first provider`
|
|
987
|
-
);
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
let result = parts[0].values;
|
|
993
|
-
for (let i = 1; i < parts.length; i++) {
|
|
994
|
-
const next = parts[i].values;
|
|
995
|
-
const combined = [];
|
|
996
|
-
for (const a of result) {
|
|
997
|
-
for (const b of next) {
|
|
998
|
-
combined.push({ ...a, ...b });
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
result = combined;
|
|
1002
|
-
}
|
|
1003
|
-
return result;
|
|
1004
|
-
}
|
|
1005
|
-
function paramsMatchInferred(params, inferredParams, optionalSegments) {
|
|
1006
|
-
return Object.entries(inferredParams).every(([k, v]) => {
|
|
1007
|
-
if (optionalSegments?.has(k)) return true;
|
|
1008
|
-
return params[k] === v;
|
|
1009
|
-
});
|
|
1010
|
-
}
|
|
1011
|
-
function computeSpecificity(route) {
|
|
1012
|
-
const dynamicCount = (route.rawRoute.match(/\[/g) || []).length;
|
|
1013
|
-
const inferredCount = route.inferredParams ? Object.keys(route.inferredParams).length : 0;
|
|
1014
|
-
const unresolvedCount = Math.max(0, dynamicCount - inferredCount);
|
|
1015
|
-
return 0 - unresolvedCount;
|
|
1016
|
-
}
|
|
1017
|
-
function buildUrl(route, params) {
|
|
1018
|
-
return route.rawRoute.replace(/\[\[(\w+)\]\]/g, (_, name) => {
|
|
1019
|
-
const value = params[name];
|
|
1020
|
-
if (!value) return "";
|
|
1021
|
-
if (route.inferredParams?.[name] === value) return "";
|
|
1022
|
-
return value;
|
|
1023
|
-
}).replace(/\[(\w+)\]/g, (_, name) => params[name] || "").replace(/\/\/+/g, "/").replace(/\/$/, "") || "/";
|
|
1024
|
-
}
|
|
1025
|
-
function materializeRouteParams(routes, loadParamsResults) {
|
|
1026
|
-
const entries = [];
|
|
1027
|
-
for (const route of routes) {
|
|
1028
|
-
const specificity = computeSpecificity(route);
|
|
1029
|
-
if (!route.hasDynamicParams) {
|
|
1030
|
-
const params = route.inferredParams || {};
|
|
1031
|
-
entries.push({ route, params, url: route.rawRoute, specificity });
|
|
1032
|
-
continue;
|
|
1033
|
-
}
|
|
1034
|
-
const allParams = loadParamsResults.get(route) || [];
|
|
1035
|
-
for (const params of allParams) {
|
|
1036
|
-
if (route.inferredParams && !paramsMatchInferred(params, route.inferredParams, route.optionalSegments)) {
|
|
1037
|
-
continue;
|
|
1038
|
-
}
|
|
1039
|
-
let mergedParams = params;
|
|
1040
|
-
if (route.inferredParams) {
|
|
1041
|
-
mergedParams = { ...params };
|
|
1042
|
-
for (const [k, v] of Object.entries(route.inferredParams)) {
|
|
1043
|
-
if (!(k in mergedParams)) {
|
|
1044
|
-
mergedParams[k] = v;
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
const url = buildUrl(route, mergedParams);
|
|
1049
|
-
entries.push({ route, params: mergedParams, url, specificity });
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
return entries;
|
|
1053
|
-
}
|
|
1054
|
-
function dedupeByUrl(entries) {
|
|
1055
|
-
const logger = getLogger();
|
|
1056
|
-
const byUrl = /* @__PURE__ */ new Map();
|
|
1057
|
-
for (const entry of entries) {
|
|
1058
|
-
const existing = byUrl.get(entry.url);
|
|
1059
|
-
if (!existing) {
|
|
1060
|
-
byUrl.set(entry.url, entry);
|
|
1061
|
-
} else if (entry.specificity > existing.specificity) {
|
|
1062
|
-
byUrl.set(entry.url, entry);
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
const deduped = [...byUrl.values()];
|
|
1066
|
-
if (deduped.length < entries.length) {
|
|
1067
|
-
logger.info(
|
|
1068
|
-
`[Build] Deduplication: ${entries.length} materialized → ${deduped.length} unique URLs`
|
|
1069
|
-
);
|
|
1070
|
-
}
|
|
1071
|
-
return deduped;
|
|
1072
|
-
}
|
|
1073
1079
|
async function discoverPluginClientPackages(projectRoot) {
|
|
1074
1080
|
const projectRequire = createRequire(path__default.join(projectRoot, "package.json"));
|
|
1075
1081
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -1404,6 +1410,9 @@ async function buildVersion(options) {
|
|
|
1404
1410
|
entry.noIndex = true;
|
|
1405
1411
|
}
|
|
1406
1412
|
}
|
|
1413
|
+
if (seResult.scripts?.length) {
|
|
1414
|
+
entry.scripts = seResult.scripts;
|
|
1415
|
+
}
|
|
1407
1416
|
logger.important(`[Build] Route server element: ${routeDir}`);
|
|
1408
1417
|
} catch (err) {
|
|
1409
1418
|
logger.error(`[Build] Route server element FAILED ${route.rawRoute}: ${err.message}`);
|