@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.
@@ -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 | Complexity | Key patterns |
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](../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 this CSS into the page and rewrites it
152
- > into the canonical `@scope (.<ref>)` / `:scope` form for you — that scoped shape is a _page-side_ concern
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 promo">
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
- :scope {
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
- Note two things sync did to the CSS — both required for the styles to actually apply. **Write region CSS in
240
- this shape yourself**; the rules are below in [The shape of a region's CSS](#the-shape-of-a-regions-css).
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 rewritten into a canonical
296
- form. `sync` produces this form and `validate` enforces it — but when you **hand-write or edit** a region's
297
- CSS, write it this way directly:
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. The `ref` is a real class on the region root (scope-anchor)
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
- ### 3. The component's own root rule targets `:scope`, not its block class
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
- This is the one that trips people up. **Inside `@scope (.<ref>) { … }`, a scoped selector matches
322
- _descendants_ of the scope root only — the scope root element itself is reachable solely through `:scope`.**
323
- So a rule for the region's own root element must be written as `:scope`, _not_ as the root's class:
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
- } /* ❌ never matches — .card IS the scope root, not a descendant */
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 is correct */
331
+ } /* ✅ descendant — also a plain class selector */
336
332
  }
337
333
  ```
338
334
 
339
- Rule of thumb: the component's **root block class** → `:scope`; keep compounds and descendants on that
340
- class (`.card.active` → `:scope.active`, `.card .card-heading` → `:scope .card-heading`), and leave every
341
- **descendant/element** selector as its ordinary class.
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
- ### 4. Instances of the same component coalesce into one block
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
- :scope {
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 `.card { … }` root rule is rewritten to `:scope { … }`.)
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` + `: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, and the component's own root rule must target `:scope`,
26
- not its block class (inside `@scope`, a class selector matches descendants of the scope root only). This has
27
- its own rules and is covered in full in
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](../contracts/examples/category-list.md) (`hasCategories`).
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 # validate all pages
142
- jay-stack validate --strict # treat warnings as errors
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 &lt;timestamp&gt;; 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
- { params },
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 { cssFile, cssImports, headMeta: parsedJayFile.headMeta };
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}`);