@jay-framework/jay-stack-cli 0.24.7 → 0.25.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.
|
@@ -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
|
|
|
@@ -27,6 +27,11 @@ 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
|
+
One design-system rule is a **hard error**, not a warning: `REGION-CSS-NO-REF` (DL#209) — a `<jay:X>` region
|
|
31
|
+
whose `template=` ships CSS but which has no `ref=`. Without a `ref` the CSS has no scope anchor, so `sync`
|
|
32
|
+
silently drops it; the fix is to add a `ref` (see `design-system-guide.md` → _The shape of a region's CSS_).
|
|
33
|
+
It is unsuppressible because the alternative would leak the region's styles across the whole page.
|
|
34
|
+
|
|
30
35
|
## How to Read Warnings
|
|
31
36
|
|
|
32
37
|
Each warning has:
|
|
@@ -135,10 +140,38 @@ The core `jay-stack validate` emits four warnings that nudge you to reuse UI thr
|
|
|
135
140
|
|
|
136
141
|
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
142
|
|
|
143
|
+
## Design-system scorecard (metrics to optimize)
|
|
144
|
+
|
|
145
|
+
Below the warnings, `validate` prints a **report-only scorecard** (DL#207). It is **never** a pass/fail — it
|
|
146
|
+
emits no warnings and never affects the exit code. It turns "prefer design-system elements" from a nudge into
|
|
147
|
+
two numbers you can deliberately raise:
|
|
148
|
+
|
|
149
|
+
- **Tag coverage** — of every contract tag/ref a page's regions _could_ bind, how many it actually binds.
|
|
150
|
+
- **Design-system coverage %** — what share of a page's DOM elements come from `template=`-backed
|
|
151
|
+
design-system regions (vs. hand-authored markup). Higher = more of the page is built from the design system.
|
|
152
|
+
- **Reuse** — how many catalogued templates are used **more than once** across the project. A design system
|
|
153
|
+
whose templates are each used once isn't really a design system.
|
|
154
|
+
|
|
155
|
+
The healthy target: **most of each page is composed of design-system elements, and each template is reused
|
|
156
|
+
more than once.** When building a page, prefer linking an existing template (raising coverage and reuse) over
|
|
157
|
+
hand-authoring markup.
|
|
158
|
+
|
|
159
|
+
By default the scorecard prints **one-line totals** for the whole project:
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
📦 Tag coverage: 100% (43/43 tags used across 10 page(s))
|
|
163
|
+
📊 Design-system scorecard: 80% element coverage (74/92), 2 of 5 catalogued templates reused > 1
|
|
164
|
+
Run validate -v for per-page details.
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Run with `-v` / `--verbose` to expand the **per-page** breakdown (coverage per file, which tags are unused,
|
|
168
|
+
per-template reuse counts, and `⚠ low` flags on pages under 50% coverage).
|
|
169
|
+
|
|
138
170
|
## Running Validation
|
|
139
171
|
|
|
140
172
|
```bash
|
|
141
|
-
jay-stack validate # validate all pages
|
|
173
|
+
jay-stack validate # validate all pages (+ one-line scorecard totals)
|
|
174
|
+
jay-stack validate -v # add per-page tag coverage + scorecard detail
|
|
142
175
|
jay-stack validate --strict # treat warnings as errors
|
|
143
176
|
```
|
|
144
177
|
|
package/dist/index.js
CHANGED
|
@@ -4253,11 +4253,10 @@ function firstColon(decl) {
|
|
|
4253
4253
|
}
|
|
4254
4254
|
function diffBodies(sourceParent, regionParent) {
|
|
4255
4255
|
const out = [];
|
|
4256
|
-
|
|
4257
|
-
diffChildren([], sourceParent, regionParent, ignoreClass, out);
|
|
4256
|
+
diffChildren([], sourceParent, regionParent, out);
|
|
4258
4257
|
return out;
|
|
4259
4258
|
}
|
|
4260
|
-
function diffChildren(parentPath, sParent, rParent,
|
|
4259
|
+
function diffChildren(parentPath, sParent, rParent, out) {
|
|
4261
4260
|
const rSup = isElement(rParent) ? parseOverride(rParent) : NO_SUPPRESSION;
|
|
4262
4261
|
const sSup = isElement(sParent) ? parseContent(sParent) : NO_SUPPRESSION;
|
|
4263
4262
|
const sup = unionSuppression(rSup, sSup);
|
|
@@ -4280,24 +4279,23 @@ function diffChildren(parentPath, sParent, rParent, ignoreClass, out) {
|
|
|
4280
4279
|
if (rc.nodeType !== NodeType.ELEMENT_NODE) continue;
|
|
4281
4280
|
const re = rc;
|
|
4282
4281
|
if (isRegionTag(re)) continue;
|
|
4283
|
-
|
|
4284
|
-
diffElement([...parentPath, i], s2[i], re, childIgnoreClass, out);
|
|
4282
|
+
diffElement([...parentPath, i], s2[i], re, out);
|
|
4285
4283
|
}
|
|
4286
4284
|
}
|
|
4287
|
-
function diffElement(path2, se, re,
|
|
4285
|
+
function diffElement(path2, se, re, out) {
|
|
4288
4286
|
const sup = unionSuppression(parseOverride(re), parseContent(se));
|
|
4289
4287
|
if (sup.all) return;
|
|
4290
|
-
compareAttributes(path2, se, re, sup,
|
|
4288
|
+
compareAttributes(path2, se, re, sup, out);
|
|
4291
4289
|
compareStyle(path2, se, re, sup, out);
|
|
4292
|
-
diffChildren(path2, se, re,
|
|
4290
|
+
diffChildren(path2, se, re, out);
|
|
4293
4291
|
}
|
|
4294
|
-
function compareAttributes(path2, se, re, sup,
|
|
4292
|
+
function compareAttributes(path2, se, re, sup, out) {
|
|
4295
4293
|
const sm = attrMap(se);
|
|
4296
4294
|
const rm = attrMap(re);
|
|
4297
4295
|
const names = [.../* @__PURE__ */ new Set([...sm.keys(), ...rm.keys()])].sort();
|
|
4298
4296
|
for (const name of names) {
|
|
4299
|
-
const sv =
|
|
4300
|
-
const rv =
|
|
4297
|
+
const sv = sm.get(name);
|
|
4298
|
+
const rv = rm.get(name);
|
|
4301
4299
|
const change = scalarChange(sv, rv, (a, b) => normalizeExpr(a) === normalizeExpr(b));
|
|
4302
4300
|
if (!change) continue;
|
|
4303
4301
|
const facet = { kind: "attribute", path: path2, element: tagOf(re), name };
|
|
@@ -4319,11 +4317,6 @@ function compareStyle(path2, se, re, sup, out) {
|
|
|
4319
4317
|
out.push({ facet, change, sourceValue: sv, regionValue: rv });
|
|
4320
4318
|
}
|
|
4321
4319
|
}
|
|
4322
|
-
function stripClassToken(value, token) {
|
|
4323
|
-
if (value === void 0 || !token) return value;
|
|
4324
|
-
const kept = value.split(/\s+/).filter((t) => t && t !== token);
|
|
4325
|
-
return kept.length ? kept.join(" ") : void 0;
|
|
4326
|
-
}
|
|
4327
4320
|
function scalarChange(sv, rv, equal) {
|
|
4328
4321
|
if (sv === void 0 && rv === void 0) return void 0;
|
|
4329
4322
|
if (sv === void 0) return "added";
|
|
@@ -4541,7 +4534,7 @@ function coalesceCss(contributions) {
|
|
|
4541
4534
|
(g) => g.selectors.length > 0 ? scopeWrap(g.css, g.selectors.join(", "), g.to) : g.css
|
|
4542
4535
|
).join("\n\n");
|
|
4543
4536
|
}
|
|
4544
|
-
function fillRegions(container, stack, opts, errors, cssBlocks
|
|
4537
|
+
function fillRegions(container, stack, opts, errors, cssBlocks) {
|
|
4545
4538
|
for (const region of directRegions(container)) {
|
|
4546
4539
|
const name = contractName(region);
|
|
4547
4540
|
const templatePath = opts.resolveTemplate(name);
|
|
@@ -4559,15 +4552,11 @@ function fillRegions(container, stack, opts, errors, cssBlocks, parentScoped = f
|
|
|
4559
4552
|
);
|
|
4560
4553
|
continue;
|
|
4561
4554
|
}
|
|
4562
|
-
const
|
|
4563
|
-
const filled = opts.preserveOverrides ? mergeOverrides(template.body, existing) : template.body;
|
|
4555
|
+
const filled = opts.preserveOverrides ? mergeOverrides(template.body, region.innerHTML) : template.body;
|
|
4564
4556
|
region.set_content(filled);
|
|
4565
|
-
const ref = readAttr(region, "ref");
|
|
4566
|
-
let scoped = false;
|
|
4567
4557
|
if (template.css) {
|
|
4568
4558
|
const selector = (opts.scopeSelector ?? defaultScopeSelector)(region);
|
|
4569
4559
|
if (selector) {
|
|
4570
|
-
scoped = true;
|
|
4571
4560
|
const to = [
|
|
4572
4561
|
...new Set(
|
|
4573
4562
|
directRegions(region).filter((c) => opts.resolveTemplate(contractName(c)) !== null).map((c) => readAttr(c, "ref")).filter((r) => !!r).map((r) => `.${r}`)
|
|
@@ -4576,15 +4565,14 @@ function fillRegions(container, stack, opts, errors, cssBlocks, parentScoped = f
|
|
|
4576
4565
|
cssBlocks.push({
|
|
4577
4566
|
key: `${templatePath}\0${[...to].sort().join(",")}`,
|
|
4578
4567
|
selector,
|
|
4579
|
-
css:
|
|
4568
|
+
css: template.css,
|
|
4580
4569
|
to
|
|
4581
4570
|
});
|
|
4582
4571
|
} else {
|
|
4583
4572
|
cssBlocks.push({ key: templatePath, selector: null, css: template.css });
|
|
4584
4573
|
}
|
|
4585
4574
|
}
|
|
4586
|
-
|
|
4587
|
-
fillRegions(region, [...stack, templatePath], opts, errors, cssBlocks, scoped);
|
|
4575
|
+
fillRegions(region, [...stack, templatePath], opts, errors, cssBlocks);
|
|
4588
4576
|
}
|
|
4589
4577
|
}
|
|
4590
4578
|
function directRegions(container) {
|
|
@@ -4612,39 +4600,6 @@ function scopeWrap(css, selector, to) {
|
|
|
4612
4600
|
${css}
|
|
4613
4601
|
}`;
|
|
4614
4602
|
}
|
|
4615
|
-
function rootClassesOf(templateBody) {
|
|
4616
|
-
const classes = /* @__PURE__ */ new Set();
|
|
4617
|
-
for (const child of parse(templateBody).childNodes) {
|
|
4618
|
-
if (child instanceof HTMLElement) {
|
|
4619
|
-
for (const c of child.classList.values()) classes.add(c);
|
|
4620
|
-
}
|
|
4621
|
-
}
|
|
4622
|
-
return classes;
|
|
4623
|
-
}
|
|
4624
|
-
function scopeReadyCss(templateBody, css) {
|
|
4625
|
-
const rootClasses = rootClassesOf(templateBody);
|
|
4626
|
-
if (rootClasses.size === 0) return css;
|
|
4627
|
-
try {
|
|
4628
|
-
const root = postcss.parse(css);
|
|
4629
|
-
root.walkRules((rule) => {
|
|
4630
|
-
rule.selectors = rule.selectors.map((sel) => rewriteRootSelector(sel, rootClasses));
|
|
4631
|
-
});
|
|
4632
|
-
return root.toString();
|
|
4633
|
-
} catch {
|
|
4634
|
-
return css;
|
|
4635
|
-
}
|
|
4636
|
-
}
|
|
4637
|
-
function rewriteRootSelector(selector, rootClasses) {
|
|
4638
|
-
return selector.replace(
|
|
4639
|
-
/\.([A-Za-z0-9_-]+)/g,
|
|
4640
|
-
(match, name) => rootClasses.has(name) ? ":scope" : match
|
|
4641
|
-
);
|
|
4642
|
-
}
|
|
4643
|
-
function stampScopeAnchor(region, className) {
|
|
4644
|
-
for (const child of region.childNodes) {
|
|
4645
|
-
if (child instanceof HTMLElement) child.classList.add(className);
|
|
4646
|
-
}
|
|
4647
|
-
}
|
|
4648
4603
|
function mergeOverrides(templateBody, existingBody) {
|
|
4649
4604
|
const tRoot = parse(templateBody);
|
|
4650
4605
|
const eRoot = parse(existingBody);
|
|
@@ -5369,8 +5324,7 @@ function checkRegionCssDrift(jayHtml, loadTemplate) {
|
|
|
5369
5324
|
if (content === void 0) {
|
|
5370
5325
|
templateCss = null;
|
|
5371
5326
|
} else {
|
|
5372
|
-
|
|
5373
|
-
templateCss = scopeReadyCss(body ? body.innerHTML : "", extractStyleCss(content));
|
|
5327
|
+
templateCss = extractStyleCss(content);
|
|
5374
5328
|
}
|
|
5375
5329
|
templateCssCache.set(imp.template, templateCss);
|
|
5376
5330
|
}
|
|
@@ -5462,6 +5416,33 @@ function checkNestedRegionRefs(jayHtml, loadTemplate) {
|
|
|
5462
5416
|
}
|
|
5463
5417
|
return warnings;
|
|
5464
5418
|
}
|
|
5419
|
+
function checkRegionCssNoRef(jayHtml, loadTemplate) {
|
|
5420
|
+
const importsWithTemplate = jayHtml.headlessImports.filter((imp) => imp.template);
|
|
5421
|
+
if (importsWithTemplate.length === 0) return [];
|
|
5422
|
+
const findings = [];
|
|
5423
|
+
const shipsCssCache = /* @__PURE__ */ new Map();
|
|
5424
|
+
const templateShipsCss = (rel) => {
|
|
5425
|
+
let v = shipsCssCache.get(rel);
|
|
5426
|
+
if (v === void 0) {
|
|
5427
|
+
const content = loadTemplate(rel);
|
|
5428
|
+
v = content !== void 0 && extractStyleCss(content).trim() !== "";
|
|
5429
|
+
shipsCssCache.set(rel, v);
|
|
5430
|
+
}
|
|
5431
|
+
return v;
|
|
5432
|
+
};
|
|
5433
|
+
for (const region of collectRegionElements(jayHtml.body)) {
|
|
5434
|
+
const contractName2 = (region.rawTagName ?? "").toLowerCase().substring(4);
|
|
5435
|
+
const imp = importsWithTemplate.find((i) => i.contractName === contractName2);
|
|
5436
|
+
if (!imp?.template) continue;
|
|
5437
|
+
if (region.getAttribute("ref")) continue;
|
|
5438
|
+
if (!templateShipsCss(imp.template)) continue;
|
|
5439
|
+
findings.push({
|
|
5440
|
+
message: `<jay:${contractName2}> flattens template="${imp.template}" which ships CSS, but the region has no ref= — so its CSS cannot be scoped and \`jay-stack sync\` will silently drop it.`,
|
|
5441
|
+
suggestion: `Add a ref= to the <jay:${contractName2}> so sync can scope its CSS as an @scope (.<ref>) block and preserve it.`
|
|
5442
|
+
});
|
|
5443
|
+
}
|
|
5444
|
+
return findings;
|
|
5445
|
+
}
|
|
5465
5446
|
const CONTENT_TAGS = /* @__PURE__ */ new Set([
|
|
5466
5447
|
// inline text semantics
|
|
5467
5448
|
"span",
|
|
@@ -5888,6 +5869,9 @@ async function validateJayFiles(options = {}) {
|
|
|
5888
5869
|
const parsedFiles = [];
|
|
5889
5870
|
let regionsSeen = 0;
|
|
5890
5871
|
let templateImportsSeen = 0;
|
|
5872
|
+
const designSystemCoverage = [];
|
|
5873
|
+
const reuseByTemplate = /* @__PURE__ */ new Map();
|
|
5874
|
+
const cataloguedContractFiles = /* @__PURE__ */ new Set();
|
|
5891
5875
|
const pageJayHtmlFiles = await findJayFiles(scanDir);
|
|
5892
5876
|
const componentJayHtmlFiles = await findJayFiles(componentsDir).catch(() => []);
|
|
5893
5877
|
const jayHtmlFiles = [...pageJayHtmlFiles, ...componentJayHtmlFiles];
|
|
@@ -5895,6 +5879,7 @@ async function validateJayFiles(options = {}) {
|
|
|
5895
5879
|
const componentContractFiles = await findContractFiles(componentsDir).catch(
|
|
5896
5880
|
() => []
|
|
5897
5881
|
);
|
|
5882
|
+
for (const cf of componentContractFiles) cataloguedContractFiles.add(cf);
|
|
5898
5883
|
const contractFiles = [...await findContractFiles(scanDir), ...componentContractFiles];
|
|
5899
5884
|
const contractNameByFile = /* @__PURE__ */ new Map();
|
|
5900
5885
|
if (options.verbose) {
|
|
@@ -6044,10 +6029,45 @@ async function validateJayFiles(options = {}) {
|
|
|
6044
6029
|
suggestion: finding.suggestion
|
|
6045
6030
|
});
|
|
6046
6031
|
}
|
|
6032
|
+
const cssNoRefErrors = checkRegionCssNoRef(parsedFile.val, readTemplateRel);
|
|
6033
|
+
for (const finding of cssNoRefErrors) {
|
|
6034
|
+
errors.push({
|
|
6035
|
+
file: relativePath,
|
|
6036
|
+
message: finding.message,
|
|
6037
|
+
stage: "generate",
|
|
6038
|
+
suggestion: finding.suggestion
|
|
6039
|
+
});
|
|
6040
|
+
}
|
|
6047
6041
|
regionsSeen += collectRegionElements(parsedFile.val.body).length;
|
|
6048
6042
|
templateImportsSeen += parsedFile.val.headlessImports.filter(
|
|
6049
6043
|
(imp) => imp.template
|
|
6050
6044
|
).length;
|
|
6045
|
+
if (!componentJayHtmlFileSet.has(jayFile)) {
|
|
6046
|
+
const body = parsedFile.val.body;
|
|
6047
|
+
const total = body.querySelectorAll("*").length;
|
|
6048
|
+
const coveredNodes = /* @__PURE__ */ new Set();
|
|
6049
|
+
for (const region of directChildRegions(body)) {
|
|
6050
|
+
const contractName2 = regionContractName(region);
|
|
6051
|
+
const imp = parsedFile.val.headlessImports.find(
|
|
6052
|
+
(i) => i.contractName === contractName2
|
|
6053
|
+
);
|
|
6054
|
+
if (!imp?.template) continue;
|
|
6055
|
+
coveredNodes.add(region);
|
|
6056
|
+
for (const descendant of region.querySelectorAll("*")) {
|
|
6057
|
+
coveredNodes.add(descendant);
|
|
6058
|
+
}
|
|
6059
|
+
const templateAbs = path$1.resolve(dirname, imp.template);
|
|
6060
|
+
reuseByTemplate.set(templateAbs, (reuseByTemplate.get(templateAbs) ?? 0) + 1);
|
|
6061
|
+
if (imp.contractPath) cataloguedContractFiles.add(imp.contractPath);
|
|
6062
|
+
}
|
|
6063
|
+
const covered = coveredNodes.size;
|
|
6064
|
+
designSystemCoverage.push({
|
|
6065
|
+
file: relativePath,
|
|
6066
|
+
coveragePct: total ? covered / total : 0,
|
|
6067
|
+
covered,
|
|
6068
|
+
total
|
|
6069
|
+
});
|
|
6070
|
+
}
|
|
6051
6071
|
const notLinkedWarnings = componentJayHtmlFileSet.has(jayFile) ? [] : checkRegionNotLinked(
|
|
6052
6072
|
parsedFile.val,
|
|
6053
6073
|
(imp) => listTemplatesForContractFile(imp.contractPath).map((t) => ({
|
|
@@ -6198,6 +6218,23 @@ async function validateJayFiles(options = {}) {
|
|
|
6198
6218
|
suggestion: noDesignSystem.suggestion
|
|
6199
6219
|
});
|
|
6200
6220
|
}
|
|
6221
|
+
const cataloguedTemplatePaths = /* @__PURE__ */ new Set();
|
|
6222
|
+
for (const contractFile of cataloguedContractFiles) {
|
|
6223
|
+
for (const t of listTemplatesForContractFile(contractFile)) {
|
|
6224
|
+
cataloguedTemplatePaths.add(path$1.resolve(t.path));
|
|
6225
|
+
}
|
|
6226
|
+
}
|
|
6227
|
+
const rel = (p) => "./" + path$1.relative(projectRoot, p).replace(/\\/g, "/");
|
|
6228
|
+
const perTemplate = {};
|
|
6229
|
+
for (const [abs, count] of reuseByTemplate) perTemplate[rel(abs)] = count;
|
|
6230
|
+
const designSystemReuse = {
|
|
6231
|
+
perTemplate,
|
|
6232
|
+
catalogued: cataloguedTemplatePaths.size,
|
|
6233
|
+
reusedMoreThanOnce: [...reuseByTemplate.values()].filter((n) => n >= 2).length,
|
|
6234
|
+
reusedOfCatalogued: [...cataloguedTemplatePaths].filter(
|
|
6235
|
+
(p) => (reuseByTemplate.get(p) ?? 0) >= 2
|
|
6236
|
+
).length
|
|
6237
|
+
};
|
|
6201
6238
|
const pluginValidators = await runPluginValidators(projectRoot, parsedFiles, errors, warnings);
|
|
6202
6239
|
return {
|
|
6203
6240
|
valid: errors.length === 0,
|
|
@@ -6206,7 +6243,9 @@ async function validateJayFiles(options = {}) {
|
|
|
6206
6243
|
errors,
|
|
6207
6244
|
warnings,
|
|
6208
6245
|
coverage,
|
|
6209
|
-
pluginValidators
|
|
6246
|
+
pluginValidators,
|
|
6247
|
+
designSystemCoverage,
|
|
6248
|
+
designSystemReuse
|
|
6210
6249
|
};
|
|
6211
6250
|
}
|
|
6212
6251
|
function printJayValidationResult(result, options) {
|
|
@@ -6280,23 +6319,84 @@ function printJayValidationResult(result, options) {
|
|
|
6280
6319
|
}
|
|
6281
6320
|
}
|
|
6282
6321
|
}
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
|
|
6322
|
+
const { designSystemCoverage, designSystemReuse } = result;
|
|
6323
|
+
const reuseTemplates = Object.keys(designSystemReuse.perTemplate);
|
|
6324
|
+
const hasTagCoverage = result.coverage.length > 0;
|
|
6325
|
+
const hasScorecard = designSystemCoverage.length > 0 || reuseTemplates.length > 0;
|
|
6326
|
+
let totalUsedTags = 0;
|
|
6327
|
+
let totalTags = 0;
|
|
6328
|
+
for (const fileCov of result.coverage) {
|
|
6329
|
+
for (const contract of fileCov.contracts) {
|
|
6330
|
+
totalUsedTags += contract.usedTags;
|
|
6331
|
+
totalTags += contract.totalTags;
|
|
6332
|
+
}
|
|
6333
|
+
}
|
|
6334
|
+
let totalCovered = 0;
|
|
6335
|
+
let totalElements = 0;
|
|
6336
|
+
for (const cov of designSystemCoverage) {
|
|
6337
|
+
totalCovered += cov.covered;
|
|
6338
|
+
totalElements += cov.total;
|
|
6339
|
+
}
|
|
6340
|
+
if (options.verbose) {
|
|
6341
|
+
if (hasTagCoverage) {
|
|
6342
|
+
logger.important("");
|
|
6343
|
+
logger.important(chalk.bold("📦 Tag Coverage"));
|
|
6344
|
+
for (const fileCov of result.coverage) {
|
|
6345
|
+
logger.important(` ${fileCov.file}`);
|
|
6346
|
+
for (const contract of fileCov.contracts) {
|
|
6347
|
+
const label = contract.key ? `${contract.key} (${contract.contractName})` : contract.contractName;
|
|
6294
6348
|
logger.important(
|
|
6295
|
-
|
|
6349
|
+
` ${label}: ${contract.usedTags}/${contract.totalTags} tags used`
|
|
6296
6350
|
);
|
|
6351
|
+
if (contract.unusedTags.length > 0) {
|
|
6352
|
+
logger.important(
|
|
6353
|
+
chalk.gray(` Unused: ${contract.unusedTags.join(", ")}`)
|
|
6354
|
+
);
|
|
6355
|
+
}
|
|
6297
6356
|
}
|
|
6298
6357
|
}
|
|
6299
6358
|
}
|
|
6359
|
+
if (hasScorecard) {
|
|
6360
|
+
logger.important("");
|
|
6361
|
+
logger.important(chalk.bold("📊 Design-system scorecard"));
|
|
6362
|
+
for (const cov of designSystemCoverage) {
|
|
6363
|
+
const pct = Math.round(cov.coveragePct * 100);
|
|
6364
|
+
const lowFlag = pct < 50 ? chalk.yellow(" ⚠ low") : "";
|
|
6365
|
+
logger.important(
|
|
6366
|
+
chalk.gray(
|
|
6367
|
+
` Coverage: ${cov.file} ${pct}% (${cov.covered}/${cov.total} elements)`
|
|
6368
|
+
) + lowFlag
|
|
6369
|
+
);
|
|
6370
|
+
}
|
|
6371
|
+
if (reuseTemplates.length > 0) {
|
|
6372
|
+
const parts = reuseTemplates.map((t) => {
|
|
6373
|
+
const n = designSystemReuse.perTemplate[t];
|
|
6374
|
+
return n < 2 ? `${t} ×${n}${chalk.yellow(" ⚠ single-use")}` : `${t} ×${n}`;
|
|
6375
|
+
});
|
|
6376
|
+
logger.important(chalk.gray(` Reuse: ${parts.join(" ")}`));
|
|
6377
|
+
}
|
|
6378
|
+
logger.important(
|
|
6379
|
+
chalk.gray(
|
|
6380
|
+
` ${designSystemReuse.reusedOfCatalogued} of ${designSystemReuse.catalogued} catalogued templates reused > 1`
|
|
6381
|
+
)
|
|
6382
|
+
);
|
|
6383
|
+
}
|
|
6384
|
+
} else if (hasTagCoverage || hasScorecard) {
|
|
6385
|
+
logger.important("");
|
|
6386
|
+
if (hasTagCoverage) {
|
|
6387
|
+
const pct = totalTags ? Math.round(totalUsedTags / totalTags * 100) : 0;
|
|
6388
|
+
logger.important(
|
|
6389
|
+
chalk.bold("📦 Tag coverage: ") + `${pct}% (${totalUsedTags}/${totalTags} tags used across ${result.coverage.length} page(s))`
|
|
6390
|
+
);
|
|
6391
|
+
}
|
|
6392
|
+
if (hasScorecard) {
|
|
6393
|
+
const pct = totalElements ? Math.round(totalCovered / totalElements * 100) : 0;
|
|
6394
|
+
const lowFlag = pct < 50 ? chalk.yellow(" ⚠ low") : "";
|
|
6395
|
+
logger.important(
|
|
6396
|
+
chalk.bold("📊 Design-system scorecard: ") + `${pct}% element coverage (${totalCovered}/${totalElements}), ${designSystemReuse.reusedOfCatalogued} of ${designSystemReuse.catalogued} catalogued templates reused > 1` + lowFlag
|
|
6397
|
+
);
|
|
6398
|
+
}
|
|
6399
|
+
logger.important(chalk.gray(" Run validate -v for per-page details."));
|
|
6300
6400
|
}
|
|
6301
6401
|
logger.important("");
|
|
6302
6402
|
if (result.valid && result.warnings.length === 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jay-framework/jay-stack-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -25,17 +25,17 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@inquirer/prompts": "^8.5.2",
|
|
28
|
-
"@jay-framework/compiler-inline-composition": "^0.
|
|
29
|
-
"@jay-framework/compiler-jay-html": "^0.
|
|
30
|
-
"@jay-framework/compiler-shared": "^0.
|
|
31
|
-
"@jay-framework/dev-server": "^0.
|
|
32
|
-
"@jay-framework/fullstack-component": "^0.
|
|
33
|
-
"@jay-framework/logger": "^0.
|
|
34
|
-
"@jay-framework/plugin-validator": "^0.
|
|
35
|
-
"@jay-framework/production-build": "^0.
|
|
36
|
-
"@jay-framework/production-server": "^0.
|
|
37
|
-
"@jay-framework/stack-server-build": "^0.
|
|
38
|
-
"@jay-framework/stack-server-runtime": "^0.
|
|
28
|
+
"@jay-framework/compiler-inline-composition": "^0.25.0",
|
|
29
|
+
"@jay-framework/compiler-jay-html": "^0.25.0",
|
|
30
|
+
"@jay-framework/compiler-shared": "^0.25.0",
|
|
31
|
+
"@jay-framework/dev-server": "^0.25.0",
|
|
32
|
+
"@jay-framework/fullstack-component": "^0.25.0",
|
|
33
|
+
"@jay-framework/logger": "^0.25.0",
|
|
34
|
+
"@jay-framework/plugin-validator": "^0.25.0",
|
|
35
|
+
"@jay-framework/production-build": "^0.25.0",
|
|
36
|
+
"@jay-framework/production-server": "^0.25.0",
|
|
37
|
+
"@jay-framework/stack-server-build": "^0.25.0",
|
|
38
|
+
"@jay-framework/stack-server-runtime": "^0.25.0",
|
|
39
39
|
"chalk": "^4.1.2",
|
|
40
40
|
"commander": "^14.0.0",
|
|
41
41
|
"express": "^5.0.1",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"yaml": "^2.3.4"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@jay-framework/dev-environment": "^0.
|
|
49
|
+
"@jay-framework/dev-environment": "^0.25.0",
|
|
50
50
|
"@types/express": "^5.0.2",
|
|
51
51
|
"@types/node": "^22.15.21",
|
|
52
52
|
"nodemon": "^3.0.3",
|