@jay-framework/jay-stack-cli 0.25.0 → 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/jay-html-template-syntax.md +1 -1
- package/agent-kit-template/designer/validation-guide.md +27 -3
- 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 +600 -8
- 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).
|
|
@@ -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,13 @@ 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
|
+
|
|
30
37
|
One design-system rule is a **hard error**, not a warning: `REGION-CSS-NO-REF` (DL#209) — a `<jay:X>` region
|
|
31
38
|
whose `template=` ships CSS but which has no `ref=`. Without a `ref` the CSS has no scope anchor, so `sync`
|
|
32
39
|
silently drops it; the fix is to add a `ref` (see `design-system-guide.md` → _The shape of a region's CSS_).
|
|
@@ -126,6 +133,7 @@ The core `jay-stack validate` emits four warnings that nudge you to reuse UI thr
|
|
|
126
133
|
| `REGION-OVERRIDE-NON-CONTENT` | `jay-validations="REGION-OVERRIDE-NON-CONTENT"` on the import |
|
|
127
134
|
| `COMPONENT-NO-TEMPLATE` | `jay-stack: allow-no-template: [Contract]` |
|
|
128
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) |
|
|
129
137
|
|
|
130
138
|
```html
|
|
131
139
|
<script type="application/jay-validations">
|
|
@@ -167,12 +175,28 @@ By default the scorecard prints **one-line totals** for the whole project:
|
|
|
167
175
|
Run with `-v` / `--verbose` to expand the **per-page** breakdown (coverage per file, which tags are unused,
|
|
168
176
|
per-template reuse counts, and `⚠ low` flags on pages under 50% coverage).
|
|
169
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
|
+
|
|
170
193
|
## Running Validation
|
|
171
194
|
|
|
172
195
|
```bash
|
|
173
|
-
jay-stack validate
|
|
174
|
-
jay-stack validate -v
|
|
175
|
-
jay-stack validate --strict
|
|
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
|
|
176
200
|
```
|
|
177
201
|
|
|
178
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}`);
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import { checkValidationErrors, JAY_CONTRACT_EXTENSION as JAY_CONTRACT_EXTENSION
|
|
|
12
12
|
import fs$2 from "node:fs/promises";
|
|
13
13
|
import * as path from "node:path";
|
|
14
14
|
import path__default from "node:path";
|
|
15
|
-
import { scanPlugins, sortPluginsByDependencies, getServiceRegistry, actionRegistry, resolveServices, ActionRegistry, discoverPluginsWithInit, executePluginServerInits, runInitCallbacks, registerService } from "@jay-framework/stack-server-runtime";
|
|
15
|
+
import { scanPlugins, sortPluginsByDependencies, getServiceRegistry, actionRegistry, resolveServices, headMetaToHeadTags, ActionRegistry, discoverPluginsWithInit, executePluginServerInits, runInitCallbacks, registerService } from "@jay-framework/stack-server-runtime";
|
|
16
16
|
import { createRequire } from "module";
|
|
17
17
|
import jsBeautify from "js-beautify";
|
|
18
18
|
import "node:crypto";
|
|
@@ -21,10 +21,11 @@ import fs__default from "node:fs";
|
|
|
21
21
|
import { createRequire as createRequire$1 } from "node:module";
|
|
22
22
|
import { isJayAction, isJayStreamAction, isJayCliCommand, CONSOLE_CONTEXT } from "@jay-framework/fullstack-component";
|
|
23
23
|
import { parse, NodeType, HTMLElement } from "node-html-parser";
|
|
24
|
-
import { Command } from "commander";
|
|
24
|
+
import { Command, Option } from "commander";
|
|
25
25
|
import chalk from "chalk";
|
|
26
26
|
import { glob } from "glob";
|
|
27
27
|
import postcss from "postcss";
|
|
28
|
+
import { scanRoutes, JayRouteParamType } from "@jay-framework/stack-route-scanner";
|
|
28
29
|
import { fileURLToPath } from "node:url";
|
|
29
30
|
import { select, confirm, input } from "@inquirer/prompts";
|
|
30
31
|
const DEFAULT_CONFIG = {
|
|
@@ -4745,6 +4746,428 @@ function scopeInnerBody(block) {
|
|
|
4745
4746
|
function normalizeCssBody(body) {
|
|
4746
4747
|
return body.replace(/\s+/g, " ").replace(/\s*([{}:;,])\s*/g, "$1").replace(/;}/g, "}").trim();
|
|
4747
4748
|
}
|
|
4749
|
+
const SUPPRESS_KEY = "allow-broken-links";
|
|
4750
|
+
const SUPPRESS_HINT = 'suppress with jay-stack:\n allow-broken-links: true\nin a <script type="application/jay-validations"> block';
|
|
4751
|
+
function normalizePath(p) {
|
|
4752
|
+
let s2 = p.split("#")[0].split("?")[0];
|
|
4753
|
+
s2 = s2.replace(/\/{2,}/g, "/");
|
|
4754
|
+
if (s2.length > 1) s2 = s2.replace(/\/+$/, "");
|
|
4755
|
+
return s2 === "" ? "/" : s2;
|
|
4756
|
+
}
|
|
4757
|
+
function escapeRegExp(s2) {
|
|
4758
|
+
return s2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4759
|
+
}
|
|
4760
|
+
function routeToConcreteUrl(route) {
|
|
4761
|
+
const parts = [];
|
|
4762
|
+
for (const seg of route.segments) {
|
|
4763
|
+
if (typeof seg === "string") {
|
|
4764
|
+
parts.push(seg);
|
|
4765
|
+
} else {
|
|
4766
|
+
const v = route.inferredParams?.[seg.name];
|
|
4767
|
+
if (v === void 0) return null;
|
|
4768
|
+
parts.push(v);
|
|
4769
|
+
}
|
|
4770
|
+
}
|
|
4771
|
+
return normalizePath("/" + parts.join("/"));
|
|
4772
|
+
}
|
|
4773
|
+
function routeToRegex(route) {
|
|
4774
|
+
let pattern = "";
|
|
4775
|
+
for (const seg of route.segments) {
|
|
4776
|
+
if (typeof seg === "string") {
|
|
4777
|
+
pattern += "/" + escapeRegExp(seg);
|
|
4778
|
+
} else if (seg.type === JayRouteParamType.optional) {
|
|
4779
|
+
pattern += "(?:/[^/]+)?";
|
|
4780
|
+
} else if (seg.type === JayRouteParamType.catchAll) {
|
|
4781
|
+
pattern += "(?:/[^/]+)+";
|
|
4782
|
+
} else {
|
|
4783
|
+
pattern += "/[^/]+";
|
|
4784
|
+
}
|
|
4785
|
+
}
|
|
4786
|
+
if (pattern === "") pattern = "/";
|
|
4787
|
+
return new RegExp("^" + pattern + "$");
|
|
4788
|
+
}
|
|
4789
|
+
async function buildRouteOracle(pagesBase) {
|
|
4790
|
+
let routes = [];
|
|
4791
|
+
try {
|
|
4792
|
+
routes = await scanRoutes(pagesBase, {
|
|
4793
|
+
jayHtmlFilename: "page.jay-html",
|
|
4794
|
+
compFilename: "page.ts"
|
|
4795
|
+
});
|
|
4796
|
+
} catch {
|
|
4797
|
+
routes = [];
|
|
4798
|
+
}
|
|
4799
|
+
const staticUrls = /* @__PURE__ */ new Set();
|
|
4800
|
+
const dynamicMatchers = [];
|
|
4801
|
+
const selfUrlByPath = /* @__PURE__ */ new Map();
|
|
4802
|
+
for (const route of routes) {
|
|
4803
|
+
const concrete = routeToConcreteUrl(route);
|
|
4804
|
+
if (concrete !== null) {
|
|
4805
|
+
staticUrls.add(concrete);
|
|
4806
|
+
if (route.jayHtmlPath) selfUrlByPath.set(path$1.resolve(route.jayHtmlPath), concrete);
|
|
4807
|
+
}
|
|
4808
|
+
if (route.segments.some((s2) => typeof s2 !== "string")) {
|
|
4809
|
+
dynamicMatchers.push(routeToRegex(route));
|
|
4810
|
+
}
|
|
4811
|
+
}
|
|
4812
|
+
return { staticUrls, dynamicMatchers, selfUrlByPath };
|
|
4813
|
+
}
|
|
4814
|
+
async function collectPublicAssets(publicFolder) {
|
|
4815
|
+
const assets = /* @__PURE__ */ new Set();
|
|
4816
|
+
async function walk(dir, prefix) {
|
|
4817
|
+
let entries;
|
|
4818
|
+
try {
|
|
4819
|
+
entries = await promises.readdir(dir, { withFileTypes: true });
|
|
4820
|
+
} catch {
|
|
4821
|
+
return;
|
|
4822
|
+
}
|
|
4823
|
+
for (const entry of entries) {
|
|
4824
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
4825
|
+
if (entry.isDirectory()) {
|
|
4826
|
+
await walk(path$1.join(dir, entry.name), rel);
|
|
4827
|
+
} else {
|
|
4828
|
+
assets.add("/" + rel);
|
|
4829
|
+
}
|
|
4830
|
+
}
|
|
4831
|
+
}
|
|
4832
|
+
await walk(publicFolder, "");
|
|
4833
|
+
return assets;
|
|
4834
|
+
}
|
|
4835
|
+
function originOf(baseUrl) {
|
|
4836
|
+
try {
|
|
4837
|
+
return new URL(baseUrl).origin;
|
|
4838
|
+
} catch {
|
|
4839
|
+
return void 0;
|
|
4840
|
+
}
|
|
4841
|
+
}
|
|
4842
|
+
function classifyHref(raw, baseUrl) {
|
|
4843
|
+
if (raw.includes("{")) return { kind: "skip" };
|
|
4844
|
+
const t = raw.trim();
|
|
4845
|
+
if (t === "" || t === "#") return { kind: "degenerate" };
|
|
4846
|
+
if (t.startsWith("#")) return { kind: "skip" };
|
|
4847
|
+
if (/^(mailto:|tel:|javascript:|data:|sms:|ftp:)/i.test(t)) return { kind: "skip" };
|
|
4848
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(t) || t.startsWith("//")) {
|
|
4849
|
+
const sameOrigin = baseUrl ? originOf(baseUrl) : void 0;
|
|
4850
|
+
if (sameOrigin) {
|
|
4851
|
+
try {
|
|
4852
|
+
const u2 = new URL(t.startsWith("//") ? "https:" + t : t);
|
|
4853
|
+
if (u2.origin === sameOrigin)
|
|
4854
|
+
return { kind: "check", targetPath: normalizePath(u2.pathname) };
|
|
4855
|
+
} catch {
|
|
4856
|
+
}
|
|
4857
|
+
}
|
|
4858
|
+
return { kind: "skip" };
|
|
4859
|
+
}
|
|
4860
|
+
if (t.startsWith("/")) return { kind: "check", targetPath: normalizePath(t) };
|
|
4861
|
+
return { kind: "skip" };
|
|
4862
|
+
}
|
|
4863
|
+
function checkInternalLinks(options) {
|
|
4864
|
+
const { parsedFiles, oracle, assetUrls, baseUrl, projectRoot } = options;
|
|
4865
|
+
const findings = [];
|
|
4866
|
+
for (const { relativePath, parsed } of parsedFiles) {
|
|
4867
|
+
if (parsed.validationOverrides?.["jay-stack"]?.[SUPPRESS_KEY] === true) continue;
|
|
4868
|
+
const ownUrl = oracle.selfUrlByPath.get(path$1.resolve(projectRoot ?? ".", relativePath));
|
|
4869
|
+
for (const anchor of parsed.body.querySelectorAll("a[href]")) {
|
|
4870
|
+
const raw = anchor.getAttribute("href");
|
|
4871
|
+
if (raw === void 0 || raw === null) continue;
|
|
4872
|
+
const verdict = classifyHref(raw, baseUrl);
|
|
4873
|
+
if (verdict.kind === "skip") continue;
|
|
4874
|
+
if (verdict.kind === "degenerate") {
|
|
4875
|
+
findings.push({
|
|
4876
|
+
file: relativePath,
|
|
4877
|
+
message: `Anchor has a placeholder href "${raw}" — it links nowhere and ships as a dead link.`,
|
|
4878
|
+
suggestion: `Point the link at a real page, use <button> for a JS-driven action, or ${SUPPRESS_HINT}.`
|
|
4879
|
+
});
|
|
4880
|
+
continue;
|
|
4881
|
+
}
|
|
4882
|
+
const target = verdict.targetPath;
|
|
4883
|
+
if (ownUrl !== void 0 && target === ownUrl) {
|
|
4884
|
+
findings.push({
|
|
4885
|
+
file: relativePath,
|
|
4886
|
+
message: `Link points to the page's own route "${target}" — a self-link goes nowhere.`,
|
|
4887
|
+
suggestion: `Remove the link, use "#" within-page navigation, or point it at a different page.`
|
|
4888
|
+
});
|
|
4889
|
+
continue;
|
|
4890
|
+
}
|
|
4891
|
+
if (oracle.staticUrls.has(target)) continue;
|
|
4892
|
+
if (assetUrls.has(target)) continue;
|
|
4893
|
+
if (oracle.dynamicMatchers.some((re) => re.test(target))) continue;
|
|
4894
|
+
findings.push({
|
|
4895
|
+
file: relativePath,
|
|
4896
|
+
message: `Broken internal link "${raw}" — no page or public asset produces the URL "${target}".`,
|
|
4897
|
+
suggestion: `Create a placeholder page/route for "${target}", or remove the link. Otherwise ${SUPPRESS_HINT}.`
|
|
4898
|
+
});
|
|
4899
|
+
}
|
|
4900
|
+
}
|
|
4901
|
+
return findings;
|
|
4902
|
+
}
|
|
4903
|
+
const META_SUPPRESS_HINT = 'suppress with jay-stack:\n allow-meta-issues: true\nin the page <script type="application/jay-validations"> block';
|
|
4904
|
+
const LINK_SUPPRESS_HINT = 'suppress with jay-stack:\n allow-broken-links: true\nin the page <script type="application/jay-validations"> block';
|
|
4905
|
+
const UNRESOLVED_BINDING = /\{[A-Za-z_$][\w$]*(?:\.[\w$]+)*\}/;
|
|
4906
|
+
const TITLE_MAX = 60;
|
|
4907
|
+
const DESCRIPTION_MAX = 160;
|
|
4908
|
+
function buildUrlFromManifest(pattern, params) {
|
|
4909
|
+
return pattern.replace(/\[\[(\w+)\]\]/g, (_, name) => params[name] || "").replace(/\[\.\.\.(\w+)\]/g, (_, name) => params[name] || "").replace(/\[(\w+)\]/g, (_, name) => params[name] || "").replace(/\/\/+/g, "/").replace(/\/$/, "") || "/";
|
|
4910
|
+
}
|
|
4911
|
+
function routeInstanceUrl(route, instance) {
|
|
4912
|
+
return normalizePath(buildUrlFromManifest(route.pattern, instance.params));
|
|
4913
|
+
}
|
|
4914
|
+
async function discoverBuildBackendDir(projectRoot, explicit) {
|
|
4915
|
+
if (explicit) {
|
|
4916
|
+
const abs = path$1.resolve(projectRoot, explicit);
|
|
4917
|
+
if (await fileExists(path$1.join(abs, "route-manifest.json"))) return abs;
|
|
4918
|
+
const nested = path$1.join(abs, "backend");
|
|
4919
|
+
if (await fileExists(path$1.join(nested, "route-manifest.json"))) return nested;
|
|
4920
|
+
const fromRoot = await highestVersionBackend(abs);
|
|
4921
|
+
if (fromRoot) return fromRoot;
|
|
4922
|
+
return null;
|
|
4923
|
+
}
|
|
4924
|
+
return highestVersionBackend(path$1.join(projectRoot, "build"));
|
|
4925
|
+
}
|
|
4926
|
+
async function highestVersionBackend(buildRoot) {
|
|
4927
|
+
let entries;
|
|
4928
|
+
try {
|
|
4929
|
+
entries = (await promises.readdir(buildRoot, { withFileTypes: true })).filter((e2) => e2.isDirectory() && /^v/.test(e2.name)).map((e2) => e2.name);
|
|
4930
|
+
} catch {
|
|
4931
|
+
return null;
|
|
4932
|
+
}
|
|
4933
|
+
if (entries.length === 0) return null;
|
|
4934
|
+
entries.sort(compareVersionDesc);
|
|
4935
|
+
for (const name of entries) {
|
|
4936
|
+
const backend = path$1.join(buildRoot, name, "backend");
|
|
4937
|
+
if (await fileExists(path$1.join(backend, "route-manifest.json"))) return backend;
|
|
4938
|
+
}
|
|
4939
|
+
return null;
|
|
4940
|
+
}
|
|
4941
|
+
function compareVersionDesc(a, b) {
|
|
4942
|
+
const pa = a.replace(/^v/, "").split(".").map(Number);
|
|
4943
|
+
const pb = b.replace(/^v/, "").split(".").map(Number);
|
|
4944
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
4945
|
+
const da = pa[i] || 0;
|
|
4946
|
+
const db = pb[i] || 0;
|
|
4947
|
+
if (da !== db) return db - da;
|
|
4948
|
+
}
|
|
4949
|
+
return 0;
|
|
4950
|
+
}
|
|
4951
|
+
async function fileExists(p) {
|
|
4952
|
+
try {
|
|
4953
|
+
await promises.access(p);
|
|
4954
|
+
return true;
|
|
4955
|
+
} catch {
|
|
4956
|
+
return false;
|
|
4957
|
+
}
|
|
4958
|
+
}
|
|
4959
|
+
async function loadBuildManifest(backendDir) {
|
|
4960
|
+
let manifest;
|
|
4961
|
+
try {
|
|
4962
|
+
manifest = JSON.parse(
|
|
4963
|
+
await promises.readFile(path$1.join(backendDir, "route-manifest.json"), "utf8")
|
|
4964
|
+
);
|
|
4965
|
+
} catch {
|
|
4966
|
+
return null;
|
|
4967
|
+
}
|
|
4968
|
+
let metadata = null;
|
|
4969
|
+
try {
|
|
4970
|
+
metadata = JSON.parse(
|
|
4971
|
+
await promises.readFile(path$1.join(backendDir, "build-metadata.json"), "utf8")
|
|
4972
|
+
);
|
|
4973
|
+
} catch {
|
|
4974
|
+
metadata = null;
|
|
4975
|
+
}
|
|
4976
|
+
return { manifest, metadata };
|
|
4977
|
+
}
|
|
4978
|
+
function buildBuildOracle(manifest, backendDir) {
|
|
4979
|
+
const urls = /* @__PURE__ */ new Set();
|
|
4980
|
+
const instances = [];
|
|
4981
|
+
for (const route of manifest.routes) {
|
|
4982
|
+
const hasDynamic = route.segments.some((s2) => s2.type !== "static");
|
|
4983
|
+
if (route.instances.length === 0) {
|
|
4984
|
+
if (!hasDynamic) urls.add(normalizePath(route.pattern));
|
|
4985
|
+
continue;
|
|
4986
|
+
}
|
|
4987
|
+
for (const instance of route.instances) {
|
|
4988
|
+
const url = routeInstanceUrl(route, instance);
|
|
4989
|
+
urls.add(url);
|
|
4990
|
+
instances.push({
|
|
4991
|
+
url,
|
|
4992
|
+
cacheAbsPath: path$1.resolve(backendDir, instance.cachePath),
|
|
4993
|
+
pattern: route.pattern,
|
|
4994
|
+
headMeta: route.headMeta
|
|
4995
|
+
});
|
|
4996
|
+
}
|
|
4997
|
+
}
|
|
4998
|
+
return { urls, instances };
|
|
4999
|
+
}
|
|
5000
|
+
async function loadSlowViewState(cacheAbsPath) {
|
|
5001
|
+
try {
|
|
5002
|
+
const cache = JSON.parse(await promises.readFile(cacheAbsPath, "utf8"));
|
|
5003
|
+
return cache.slowViewState ?? {};
|
|
5004
|
+
} catch {
|
|
5005
|
+
return {};
|
|
5006
|
+
}
|
|
5007
|
+
}
|
|
5008
|
+
function checkTemplateLinksAgainstBuild(args) {
|
|
5009
|
+
const { parsedFiles, buildUrls, assetUrls, dynamicMatchers, baseUrl } = args;
|
|
5010
|
+
const findings = [];
|
|
5011
|
+
for (const { relativePath, parsed } of parsedFiles) {
|
|
5012
|
+
if (parsed.validationOverrides?.["jay-stack"]?.["allow-broken-links"] === true) continue;
|
|
5013
|
+
for (const anchor of parsed.body.querySelectorAll("a[href]")) {
|
|
5014
|
+
const raw = anchor.getAttribute("href");
|
|
5015
|
+
if (raw === void 0 || raw === null) continue;
|
|
5016
|
+
const verdict = classifyHref(raw, baseUrl);
|
|
5017
|
+
if (verdict.kind !== "check") continue;
|
|
5018
|
+
const target = verdict.targetPath;
|
|
5019
|
+
if (buildUrls.has(target) || assetUrls.has(target)) continue;
|
|
5020
|
+
if (!dynamicMatchers.some((re) => re.test(target))) continue;
|
|
5021
|
+
findings.push({
|
|
5022
|
+
file: relativePath,
|
|
5023
|
+
message: `Broken link "${raw}" — matches a dynamic route but "${target}" is not among the URLs this build produces.`,
|
|
5024
|
+
suggestion: `Create the slug/page, fix the path, or remove the link. Otherwise ${LINK_SUPPRESS_HINT}.`,
|
|
5025
|
+
severity: "error",
|
|
5026
|
+
source: "build-output"
|
|
5027
|
+
});
|
|
5028
|
+
}
|
|
5029
|
+
}
|
|
5030
|
+
return findings;
|
|
5031
|
+
}
|
|
5032
|
+
function resolveContentHref(raw, instanceUrl, baseUrl) {
|
|
5033
|
+
const t = raw.trim();
|
|
5034
|
+
if (t === "" || t === "#" || t.startsWith("#")) return { kind: "skip" };
|
|
5035
|
+
if (t.includes("{")) return { kind: "skip" };
|
|
5036
|
+
if (t.startsWith("/") || t.startsWith("//") || /^[a-z][a-z0-9+.-]*:/i.test(t)) {
|
|
5037
|
+
const v = classifyHref(t, baseUrl);
|
|
5038
|
+
return v.kind === "check" ? v : { kind: "skip" };
|
|
5039
|
+
}
|
|
5040
|
+
try {
|
|
5041
|
+
const u2 = new URL(t, "https://_local_" + (instanceUrl === "/" ? "/" : instanceUrl));
|
|
5042
|
+
return { kind: "check", targetPath: normalizePath(u2.pathname) };
|
|
5043
|
+
} catch {
|
|
5044
|
+
return { kind: "skip" };
|
|
5045
|
+
}
|
|
5046
|
+
}
|
|
5047
|
+
function collectContentHrefs(viewState) {
|
|
5048
|
+
const hrefs = [];
|
|
5049
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5050
|
+
const walk = (node) => {
|
|
5051
|
+
if (node == null) return;
|
|
5052
|
+
if (typeof node === "string") {
|
|
5053
|
+
if (node.includes("<a ")) {
|
|
5054
|
+
const root = parse(node);
|
|
5055
|
+
for (const a of root.querySelectorAll("a[href]")) {
|
|
5056
|
+
const h = a.getAttribute("href");
|
|
5057
|
+
if (h) hrefs.push(h);
|
|
5058
|
+
}
|
|
5059
|
+
}
|
|
5060
|
+
return;
|
|
5061
|
+
}
|
|
5062
|
+
if (typeof node !== "object") return;
|
|
5063
|
+
if (seen.has(node)) return;
|
|
5064
|
+
seen.add(node);
|
|
5065
|
+
if (Array.isArray(node)) {
|
|
5066
|
+
for (const item of node) walk(item);
|
|
5067
|
+
} else {
|
|
5068
|
+
for (const v of Object.values(node)) walk(v);
|
|
5069
|
+
}
|
|
5070
|
+
};
|
|
5071
|
+
walk(viewState);
|
|
5072
|
+
return hrefs;
|
|
5073
|
+
}
|
|
5074
|
+
function checkContentLinks(args) {
|
|
5075
|
+
const { instanceUrl, slowViewState, buildUrls, assetUrls, baseUrl } = args;
|
|
5076
|
+
const findings = [];
|
|
5077
|
+
const reported = /* @__PURE__ */ new Set();
|
|
5078
|
+
for (const raw of collectContentHrefs(slowViewState)) {
|
|
5079
|
+
const verdict = resolveContentHref(raw, instanceUrl, baseUrl);
|
|
5080
|
+
if (verdict.kind !== "check") continue;
|
|
5081
|
+
const target = verdict.targetPath;
|
|
5082
|
+
if (buildUrls.has(target) || assetUrls.has(target)) continue;
|
|
5083
|
+
if (reported.has(target)) continue;
|
|
5084
|
+
reported.add(target);
|
|
5085
|
+
findings.push({
|
|
5086
|
+
file: instanceUrl,
|
|
5087
|
+
message: `Broken link "${raw}" in rendered content of ${instanceUrl} — resolves to "${target}", which this build does not produce.`,
|
|
5088
|
+
// Content links come from authored markdown/CMS data that often doubles as repo docs (where a
|
|
5089
|
+
// repo-relative "../pkg/foo.ts" is correct), so this is a warning, not a build-failing error —
|
|
5090
|
+
// unlike hand-authored template links (Check 1), which are site-only and stay errors.
|
|
5091
|
+
suggestion: `Fix the link in the source content, point it at an absolute URL, or ${LINK_SUPPRESS_HINT}.`,
|
|
5092
|
+
severity: "warning",
|
|
5093
|
+
source: "build-output"
|
|
5094
|
+
});
|
|
5095
|
+
}
|
|
5096
|
+
return findings;
|
|
5097
|
+
}
|
|
5098
|
+
function checkInstanceMeta(args) {
|
|
5099
|
+
const { instanceUrl, headMeta, slowViewState } = args;
|
|
5100
|
+
if (!headMeta) return [];
|
|
5101
|
+
const findings = [];
|
|
5102
|
+
const tags = headMetaToHeadTags(headMeta, slowViewState);
|
|
5103
|
+
const addLeftover = (what, value) => findings.push({
|
|
5104
|
+
file: instanceUrl,
|
|
5105
|
+
message: `<${what}> for ${instanceUrl} resolves to "${value}" — an unresolved binding was not produced by slow render.`,
|
|
5106
|
+
suggestion: `Ensure the field is produced by the slow phase, or make the ${what} static.`,
|
|
5107
|
+
severity: "error",
|
|
5108
|
+
source: "build-output"
|
|
5109
|
+
});
|
|
5110
|
+
const addWarn = (message) => findings.push({
|
|
5111
|
+
file: instanceUrl,
|
|
5112
|
+
message,
|
|
5113
|
+
suggestion: `Fix the content or ${META_SUPPRESS_HINT}.`,
|
|
5114
|
+
severity: "warning",
|
|
5115
|
+
source: "build-output"
|
|
5116
|
+
});
|
|
5117
|
+
for (const tag of tags) {
|
|
5118
|
+
if (tag.tag === "title") {
|
|
5119
|
+
const value = tag.children ?? "";
|
|
5120
|
+
if (UNRESOLVED_BINDING.test(value)) addLeftover("title", value);
|
|
5121
|
+
else if (value.trim() === "") addWarn(`<title> for ${instanceUrl} is empty.`);
|
|
5122
|
+
else if (value.length > TITLE_MAX)
|
|
5123
|
+
addWarn(
|
|
5124
|
+
`<title> for ${instanceUrl} is ${value.length} chars (recommend ≤ ${TITLE_MAX}).`
|
|
5125
|
+
);
|
|
5126
|
+
} else if (tag.tag === "meta" && tag.attrs?.name === "description") {
|
|
5127
|
+
const value = tag.attrs.content ?? "";
|
|
5128
|
+
if (UNRESOLVED_BINDING.test(value)) addLeftover('meta name="description"', value);
|
|
5129
|
+
else if (value.trim() === "")
|
|
5130
|
+
addWarn(`<meta name="description"> for ${instanceUrl} is empty.`);
|
|
5131
|
+
else if (value.length > DESCRIPTION_MAX)
|
|
5132
|
+
addWarn(
|
|
5133
|
+
`<meta name="description"> for ${instanceUrl} is ${value.length} chars (recommend ≤ ${DESCRIPTION_MAX}).`
|
|
5134
|
+
);
|
|
5135
|
+
}
|
|
5136
|
+
}
|
|
5137
|
+
return findings;
|
|
5138
|
+
}
|
|
5139
|
+
function isBuildStale(metadata, newestSourceMtimeMs) {
|
|
5140
|
+
if (!metadata?.buildTimestamp) return false;
|
|
5141
|
+
const built = Date.parse(metadata.buildTimestamp);
|
|
5142
|
+
if (Number.isNaN(built)) return false;
|
|
5143
|
+
return newestSourceMtimeMs > built;
|
|
5144
|
+
}
|
|
5145
|
+
async function newestSourceMtime(dirs) {
|
|
5146
|
+
let newest = 0;
|
|
5147
|
+
const walk = async (dir) => {
|
|
5148
|
+
let entries;
|
|
5149
|
+
try {
|
|
5150
|
+
entries = await promises.readdir(dir, { withFileTypes: true });
|
|
5151
|
+
} catch {
|
|
5152
|
+
return;
|
|
5153
|
+
}
|
|
5154
|
+
for (const entry of entries) {
|
|
5155
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
5156
|
+
const full = path$1.join(dir, entry.name);
|
|
5157
|
+
if (entry.isDirectory()) {
|
|
5158
|
+
await walk(full);
|
|
5159
|
+
} else {
|
|
5160
|
+
try {
|
|
5161
|
+
const st = await promises.stat(full);
|
|
5162
|
+
if (st.mtimeMs > newest) newest = st.mtimeMs;
|
|
5163
|
+
} catch {
|
|
5164
|
+
}
|
|
5165
|
+
}
|
|
5166
|
+
}
|
|
5167
|
+
};
|
|
5168
|
+
for (const dir of dirs) await walk(dir);
|
|
5169
|
+
return newest;
|
|
5170
|
+
}
|
|
4748
5171
|
async function findJayFiles(dir) {
|
|
4749
5172
|
return await glob(`${dir}/**/*${JAY_EXTENSION$1}`);
|
|
4750
5173
|
}
|
|
@@ -6235,6 +6658,38 @@ async function validateJayFiles(options = {}) {
|
|
|
6235
6658
|
(p) => (reuseByTemplate.get(p) ?? 0) >= 2
|
|
6236
6659
|
).length
|
|
6237
6660
|
};
|
|
6661
|
+
const linkOracle = await buildRouteOracle(scanDir);
|
|
6662
|
+
const assetUrls = await collectPublicAssets(
|
|
6663
|
+
path$1.resolve(projectRoot, resolvedConfig.devServer.publicFolder)
|
|
6664
|
+
);
|
|
6665
|
+
const linkFindings = checkInternalLinks({
|
|
6666
|
+
parsedFiles,
|
|
6667
|
+
oracle: linkOracle,
|
|
6668
|
+
assetUrls,
|
|
6669
|
+
baseUrl: config.site?.baseUrl,
|
|
6670
|
+
projectRoot
|
|
6671
|
+
});
|
|
6672
|
+
for (const finding of linkFindings) {
|
|
6673
|
+
errors.push({
|
|
6674
|
+
file: finding.file,
|
|
6675
|
+
message: finding.message,
|
|
6676
|
+
stage: "generate",
|
|
6677
|
+
source: "internal-links",
|
|
6678
|
+
suggestion: finding.suggestion
|
|
6679
|
+
});
|
|
6680
|
+
}
|
|
6681
|
+
if (options.tier2) {
|
|
6682
|
+
await runBuildOutputValidation({
|
|
6683
|
+
projectRoot,
|
|
6684
|
+
buildDir: options.buildDir,
|
|
6685
|
+
parsedFiles,
|
|
6686
|
+
linkOracle,
|
|
6687
|
+
assetUrls,
|
|
6688
|
+
baseUrl: config.site?.baseUrl,
|
|
6689
|
+
errors,
|
|
6690
|
+
warnings
|
|
6691
|
+
});
|
|
6692
|
+
}
|
|
6238
6693
|
const pluginValidators = await runPluginValidators(projectRoot, parsedFiles, errors, warnings);
|
|
6239
6694
|
return {
|
|
6240
6695
|
valid: errors.length === 0,
|
|
@@ -6248,6 +6703,89 @@ async function validateJayFiles(options = {}) {
|
|
|
6248
6703
|
designSystemReuse
|
|
6249
6704
|
};
|
|
6250
6705
|
}
|
|
6706
|
+
async function runBuildOutputValidation(args) {
|
|
6707
|
+
const { projectRoot, buildDir, parsedFiles, linkOracle, assetUrls, baseUrl, errors, warnings } = args;
|
|
6708
|
+
const backendDir = await discoverBuildBackendDir(projectRoot, buildDir);
|
|
6709
|
+
if (!backendDir) {
|
|
6710
|
+
errors.push({
|
|
6711
|
+
file: "build",
|
|
6712
|
+
message: "Tier 2: no build found. Run `jay-stack build` first, or point --build-dir at a build.",
|
|
6713
|
+
stage: "generate",
|
|
6714
|
+
source: "build-output"
|
|
6715
|
+
});
|
|
6716
|
+
return;
|
|
6717
|
+
}
|
|
6718
|
+
const loaded = await loadBuildManifest(backendDir);
|
|
6719
|
+
if (!loaded) {
|
|
6720
|
+
errors.push({
|
|
6721
|
+
file: backendDir,
|
|
6722
|
+
message: `Tier 2: could not read route-manifest.json in ${backendDir}.`,
|
|
6723
|
+
stage: "generate",
|
|
6724
|
+
source: "build-output"
|
|
6725
|
+
});
|
|
6726
|
+
return;
|
|
6727
|
+
}
|
|
6728
|
+
const newest = await newestSourceMtime([
|
|
6729
|
+
path$1.join(projectRoot, "src"),
|
|
6730
|
+
path$1.join(projectRoot, "content")
|
|
6731
|
+
]);
|
|
6732
|
+
if (isBuildStale(loaded.metadata, newest)) {
|
|
6733
|
+
warnings.push({
|
|
6734
|
+
file: "build",
|
|
6735
|
+
message: `Tier 2: validated against a build from ${loaded.metadata?.buildTimestamp}; source has changed since.`,
|
|
6736
|
+
source: "build-output",
|
|
6737
|
+
suggestion: "Re-run `jay-stack build` for accurate results."
|
|
6738
|
+
});
|
|
6739
|
+
}
|
|
6740
|
+
const oracle = buildBuildOracle(loaded.manifest, backendDir);
|
|
6741
|
+
const findings = [];
|
|
6742
|
+
findings.push(
|
|
6743
|
+
...checkTemplateLinksAgainstBuild({
|
|
6744
|
+
parsedFiles,
|
|
6745
|
+
buildUrls: oracle.urls,
|
|
6746
|
+
assetUrls,
|
|
6747
|
+
dynamicMatchers: linkOracle.dynamicMatchers,
|
|
6748
|
+
baseUrl
|
|
6749
|
+
})
|
|
6750
|
+
);
|
|
6751
|
+
for (const instance of oracle.instances) {
|
|
6752
|
+
const slowViewState = await loadSlowViewState(instance.cacheAbsPath);
|
|
6753
|
+
findings.push(
|
|
6754
|
+
...checkContentLinks({
|
|
6755
|
+
instanceUrl: instance.url,
|
|
6756
|
+
slowViewState,
|
|
6757
|
+
buildUrls: oracle.urls,
|
|
6758
|
+
assetUrls,
|
|
6759
|
+
baseUrl
|
|
6760
|
+
})
|
|
6761
|
+
);
|
|
6762
|
+
findings.push(
|
|
6763
|
+
...checkInstanceMeta({
|
|
6764
|
+
instanceUrl: instance.url,
|
|
6765
|
+
headMeta: instance.headMeta,
|
|
6766
|
+
slowViewState
|
|
6767
|
+
})
|
|
6768
|
+
);
|
|
6769
|
+
}
|
|
6770
|
+
for (const finding of findings) {
|
|
6771
|
+
if (finding.severity === "error") {
|
|
6772
|
+
errors.push({
|
|
6773
|
+
file: finding.file,
|
|
6774
|
+
message: finding.message,
|
|
6775
|
+
stage: "generate",
|
|
6776
|
+
source: "build-output",
|
|
6777
|
+
suggestion: finding.suggestion
|
|
6778
|
+
});
|
|
6779
|
+
} else {
|
|
6780
|
+
warnings.push({
|
|
6781
|
+
file: finding.file,
|
|
6782
|
+
message: finding.message,
|
|
6783
|
+
source: "build-output",
|
|
6784
|
+
suggestion: finding.suggestion
|
|
6785
|
+
});
|
|
6786
|
+
}
|
|
6787
|
+
}
|
|
6788
|
+
}
|
|
6251
6789
|
function printJayValidationResult(result, options) {
|
|
6252
6790
|
const logger = getLogger();
|
|
6253
6791
|
if (options.json) {
|
|
@@ -6255,9 +6793,11 @@ function printJayValidationResult(result, options) {
|
|
|
6255
6793
|
return;
|
|
6256
6794
|
}
|
|
6257
6795
|
logger.important("");
|
|
6258
|
-
const coreErrors = result.errors.filter(
|
|
6796
|
+
const coreErrors = result.errors.filter(
|
|
6797
|
+
(e2) => e2.stage !== "plugin" && e2.source !== "build-output"
|
|
6798
|
+
);
|
|
6259
6799
|
const coreWarnings = result.warnings.filter((w) => !w.source);
|
|
6260
|
-
logger.important(chalk.bold("📦 jay-stack (core)"));
|
|
6800
|
+
logger.important(chalk.bold("📦 Tier 1 — jay-stack (core)"));
|
|
6261
6801
|
if (coreErrors.length === 0) {
|
|
6262
6802
|
logger.important(
|
|
6263
6803
|
chalk.green(
|
|
@@ -6280,6 +6820,40 @@ function printJayValidationResult(result, options) {
|
|
|
6280
6820
|
logger.important(chalk.blue(` Suggestion: ${warning.suggestion}`));
|
|
6281
6821
|
}
|
|
6282
6822
|
}
|
|
6823
|
+
const buildErrors = result.errors.filter((e2) => e2.source === "build-output");
|
|
6824
|
+
const buildWarnings = result.warnings.filter((w) => w.source === "build-output");
|
|
6825
|
+
if (buildErrors.length > 0 || buildWarnings.length > 0) {
|
|
6826
|
+
logger.important("");
|
|
6827
|
+
logger.important(chalk.bold("📦 Tier 2 — build-output validation (--tier-2)"));
|
|
6828
|
+
for (const error of buildErrors) {
|
|
6829
|
+
logger.important(chalk.red(` ❌ ${error.file}`));
|
|
6830
|
+
logger.important(chalk.gray(` ${error.message}`));
|
|
6831
|
+
if (error.suggestion) {
|
|
6832
|
+
logger.important(chalk.blue(` Suggestion: ${error.suggestion}`));
|
|
6833
|
+
}
|
|
6834
|
+
}
|
|
6835
|
+
const buildWarnGroups = /* @__PURE__ */ new Map();
|
|
6836
|
+
for (const warning of buildWarnings) {
|
|
6837
|
+
const group = buildWarnGroups.get(warning.file) || [];
|
|
6838
|
+
group.push(warning);
|
|
6839
|
+
buildWarnGroups.set(warning.file, group);
|
|
6840
|
+
}
|
|
6841
|
+
for (const [file, groupWarns] of buildWarnGroups) {
|
|
6842
|
+
logger.important(chalk.yellow(` ⚠ ${file}`));
|
|
6843
|
+
for (const warning of groupWarns) {
|
|
6844
|
+
if (warning.message) {
|
|
6845
|
+
logger.important(chalk.gray(` ${warning.message}`));
|
|
6846
|
+
}
|
|
6847
|
+
}
|
|
6848
|
+
const suggestions = [...new Set(groupWarns.map((w) => w.suggestion).filter(Boolean))];
|
|
6849
|
+
if (suggestions.length > 0) {
|
|
6850
|
+
logger.important(chalk.blue(` Suggestions:`));
|
|
6851
|
+
for (const s2 of suggestions) {
|
|
6852
|
+
logger.important(chalk.blue(` ${s2}`));
|
|
6853
|
+
}
|
|
6854
|
+
}
|
|
6855
|
+
}
|
|
6856
|
+
}
|
|
6283
6857
|
const pluginErrors = result.errors.filter((e2) => e2.stage === "plugin");
|
|
6284
6858
|
const pluginWarnings = result.warnings.filter((w) => !!w.source);
|
|
6285
6859
|
for (const validatorName of result.pluginValidators) {
|
|
@@ -6422,12 +6996,22 @@ function printJayValidationResult(result, options) {
|
|
|
6422
6996
|
)
|
|
6423
6997
|
);
|
|
6424
6998
|
}
|
|
6999
|
+
if (!options.tier2) {
|
|
7000
|
+
logger.important("");
|
|
7001
|
+
logger.important(
|
|
7002
|
+
chalk.gray(
|
|
7003
|
+
"ℹ This was Tier 1 (template) validation. Tier 2 runs deeper checks against an existing build\n (dynamic-slug links, rendered-content links, per-instance meta):\n jay-stack validate --tier-2\n See: agent-kit/designer/validation-tier-2-guide.md"
|
|
7004
|
+
)
|
|
7005
|
+
);
|
|
7006
|
+
}
|
|
6425
7007
|
}
|
|
6426
7008
|
async function runValidate(scanPath, options) {
|
|
6427
7009
|
const result = await validateJayFiles({
|
|
6428
7010
|
path: scanPath,
|
|
6429
7011
|
verbose: options.verbose,
|
|
6430
|
-
json: options.json
|
|
7012
|
+
json: options.json,
|
|
7013
|
+
tier2: options.tier2,
|
|
7014
|
+
buildDir: options.buildDir
|
|
6431
7015
|
});
|
|
6432
7016
|
printJayValidationResult(result, options);
|
|
6433
7017
|
if (!result.valid) {
|
|
@@ -6574,7 +7158,7 @@ async function runBuild(projectPath, options) {
|
|
|
6574
7158
|
}
|
|
6575
7159
|
const ctx = await resolveProductionContext(projectPath, options.version);
|
|
6576
7160
|
await generateDefinitionFiles(ctx);
|
|
6577
|
-
const { buildVersion } = await import("./index-
|
|
7161
|
+
const { buildVersion } = await import("./index-BQbkAEhi.js");
|
|
6578
7162
|
await buildVersion({
|
|
6579
7163
|
version: ctx.version,
|
|
6580
7164
|
projectRoot: ctx.resolvedPath,
|
|
@@ -7694,9 +8278,17 @@ program.command("cleanup").description("Delete orphaned files from previous rebu
|
|
|
7694
8278
|
process.exit(1);
|
|
7695
8279
|
}
|
|
7696
8280
|
});
|
|
7697
|
-
program.command("validate").description("Validate all .jay-html and .jay-contract files").option("-p, --path <path>", "Scan root (default: cwd)").option("-v, --verbose", "Show per-file validation status").option("--json", "Output results as JSON").
|
|
8281
|
+
program.command("validate").description("Validate all .jay-html and .jay-contract files").option("-p, --path <path>", "Scan root (default: cwd)").option("-v, --verbose", "Show per-file validation status").option("--json", "Output results as JSON").addOption(
|
|
8282
|
+
new Option(
|
|
8283
|
+
"--tier-2",
|
|
8284
|
+
"Run Tier 2: deep validation against an existing build (dynamic-slug links, rendered content, per-instance meta)"
|
|
8285
|
+
)
|
|
8286
|
+
).addOption(new Option("--t2", "Alias for --tier-2").hideHelp()).option(
|
|
8287
|
+
"--build-dir <dir>",
|
|
8288
|
+
"Build backend dir or build root for Tier 2 (default: highest build/v*)"
|
|
8289
|
+
).action(async (options) => {
|
|
7698
8290
|
try {
|
|
7699
|
-
await runValidate(options.path, options);
|
|
8291
|
+
await runValidate(options.path, { ...options, tier2: options.tier2 || options.t2 });
|
|
7700
8292
|
} catch (error) {
|
|
7701
8293
|
if (options.json) {
|
|
7702
8294
|
getLogger().important(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jay-framework/jay-stack-cli",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -25,17 +25,18 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@inquirer/prompts": "^8.5.2",
|
|
28
|
-
"@jay-framework/compiler-inline-composition": "^0.25.
|
|
29
|
-
"@jay-framework/compiler-jay-html": "^0.25.
|
|
30
|
-
"@jay-framework/compiler-shared": "^0.25.
|
|
31
|
-
"@jay-framework/dev-server": "^0.25.
|
|
32
|
-
"@jay-framework/fullstack-component": "^0.25.
|
|
33
|
-
"@jay-framework/logger": "^0.25.
|
|
34
|
-
"@jay-framework/plugin-validator": "^0.25.
|
|
35
|
-
"@jay-framework/production-build": "^0.25.
|
|
36
|
-
"@jay-framework/production-server": "^0.25.
|
|
37
|
-
"@jay-framework/stack-
|
|
38
|
-
"@jay-framework/stack-server-
|
|
28
|
+
"@jay-framework/compiler-inline-composition": "^0.25.1",
|
|
29
|
+
"@jay-framework/compiler-jay-html": "^0.25.1",
|
|
30
|
+
"@jay-framework/compiler-shared": "^0.25.1",
|
|
31
|
+
"@jay-framework/dev-server": "^0.25.1",
|
|
32
|
+
"@jay-framework/fullstack-component": "^0.25.1",
|
|
33
|
+
"@jay-framework/logger": "^0.25.1",
|
|
34
|
+
"@jay-framework/plugin-validator": "^0.25.1",
|
|
35
|
+
"@jay-framework/production-build": "^0.25.1",
|
|
36
|
+
"@jay-framework/production-server": "^0.25.1",
|
|
37
|
+
"@jay-framework/stack-route-scanner": "^0.25.1",
|
|
38
|
+
"@jay-framework/stack-server-build": "^0.25.1",
|
|
39
|
+
"@jay-framework/stack-server-runtime": "^0.25.1",
|
|
39
40
|
"chalk": "^4.1.2",
|
|
40
41
|
"commander": "^14.0.0",
|
|
41
42
|
"express": "^5.0.1",
|
|
@@ -46,7 +47,7 @@
|
|
|
46
47
|
"yaml": "^2.3.4"
|
|
47
48
|
},
|
|
48
49
|
"devDependencies": {
|
|
49
|
-
"@jay-framework/dev-environment": "^0.25.
|
|
50
|
+
"@jay-framework/dev-environment": "^0.25.1",
|
|
50
51
|
"@types/express": "^5.0.2",
|
|
51
52
|
"@types/node": "^22.15.21",
|
|
52
53
|
"nodemon": "^3.0.3",
|