@jay-framework/jay-stack-cli 0.25.4 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-kit-template/designer/contracts-and-plugins.md +14 -0
- package/agent-kit-template/designer/validation-guide.md +26 -10
- package/agent-kit-template/developer/INSTRUCTIONS.md +19 -18
- package/agent-kit-template/developer/configuration.md +4 -0
- package/agent-kit-template/developer/page-components.md +9 -6
- package/agent-kit-template/developer/query-param-pages.md +426 -0
- package/agent-kit-template/developer/render-results.md +28 -1
- package/agent-kit-template/devops/INSTRUCTIONS.md +88 -11
- package/agent-kit-template/devops/fetch-handler.md +21 -0
- package/agent-kit-template/devops/invalidation.md +2 -7
- package/agent-kit-template/devops/production-build.md +8 -3
- package/agent-kit-template/devops/serving-modes.md +25 -0
- package/agent-kit-template/devops/source-editing.md +89 -0
- package/agent-kit-template/plugin/INSTRUCTIONS.md +25 -24
- package/agent-kit-template/plugin/actions-guide.md +57 -0
- package/agent-kit-template/plugin/contracts-guide.md +42 -0
- package/agent-kit-template/plugin/render-results.md +19 -0
- package/agent-kit-template/plugin/source-editing.md +70 -0
- package/agent-kit-template/plugin/validation.md +36 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +340 -249
- package/package.json +16 -16
- package/dist/index-B5oavmfA.js +0 -1803
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import YAML from "yaml";
|
|
|
9
9
|
import { getLogger, setDevLogger, createDevLogger } from "@jay-framework/logger";
|
|
10
10
|
import { parseJayFile, JAY_IMPORT_RESOLVER, generateElementDefinitionFile, parseContract, compileContract, parseAction, generateElementFile, generateServerElementFile, htmlElementTagNameMap, loadLinkedContract, getLinkedContractDir } from "@jay-framework/compiler-jay-html";
|
|
11
11
|
import { checkValidationErrors, JAY_CONTRACT_EXTENSION as JAY_CONTRACT_EXTENSION$1, loadPluginManifest as loadPluginManifest$1, normalizeActionEntry as normalizeActionEntry$1, JAY_EXTENSION as JAY_EXTENSION$1, RuntimeMode as RuntimeMode$1, GenerateTarget, findDynamicContract, isEnumType as isEnumType$1, equalJayTypes, prettifyHtml } from "@jay-framework/compiler-shared";
|
|
12
|
-
import fs$
|
|
12
|
+
import fs$1$1 from "node:fs/promises";
|
|
13
13
|
import * as path from "node:path";
|
|
14
14
|
import path__default from "node:path";
|
|
15
15
|
import { scanPlugins, sortPluginsByDependencies, getServiceRegistry, actionRegistry, resolveServices, headMetaToHeadTags, ActionRegistry, discoverPluginsWithInit, executePluginServerInits, runInitCallbacks, registerService } from "@jay-framework/stack-server-runtime";
|
|
@@ -26,6 +26,7 @@ import chalk from "chalk";
|
|
|
26
26
|
import { glob } from "glob";
|
|
27
27
|
import postcss from "postcss";
|
|
28
28
|
import { scanRoutes, JayRouteParamType } from "@jay-framework/stack-route-scanner";
|
|
29
|
+
import { FilesystemArtifactStore } from "@jay-framework/production-server";
|
|
29
30
|
import { fileURLToPath } from "node:url";
|
|
30
31
|
import { select, confirm, input } from "@inquirer/prompts";
|
|
31
32
|
const DEFAULT_CONFIG = {
|
|
@@ -65,7 +66,11 @@ function getConfigWithDefaults(config) {
|
|
|
65
66
|
pagesBase: config.devServer?.pagesBase || DEFAULT_CONFIG.devServer.pagesBase,
|
|
66
67
|
componentsBase: config.devServer?.componentsBase || DEFAULT_CONFIG.devServer.componentsBase,
|
|
67
68
|
publicFolder: config.devServer?.publicFolder || DEFAULT_CONFIG.devServer.publicFolder,
|
|
68
|
-
configBase: config.devServer?.configBase || DEFAULT_CONFIG.devServer.configBase
|
|
69
|
+
configBase: config.devServer?.configBase || DEFAULT_CONFIG.devServer.configBase,
|
|
70
|
+
// No default — absent means Vite's localhost-only host check (DL#220).
|
|
71
|
+
allowedHosts: config.devServer?.allowedHosts,
|
|
72
|
+
// Default off — source-editing markers (DL#219) only for opt-in sessions.
|
|
73
|
+
sourceEditing: config.devServer?.sourceEditing
|
|
69
74
|
},
|
|
70
75
|
site: config.site
|
|
71
76
|
};
|
|
@@ -220,7 +225,9 @@ async function startDevServer(options = {}) {
|
|
|
220
225
|
publicBaseUrlPath: "/",
|
|
221
226
|
jayRollupConfig: jayOptions,
|
|
222
227
|
logLevel: options.logLevel,
|
|
223
|
-
httpServer
|
|
228
|
+
httpServer,
|
|
229
|
+
allowedHosts: resolvedConfig.devServer.allowedHosts,
|
|
230
|
+
sourceEditing: resolvedConfig.devServer.sourceEditing
|
|
224
231
|
});
|
|
225
232
|
app.use(server);
|
|
226
233
|
const publicPath = path$1.resolve(resolvedConfig.devServer.publicFolder);
|
|
@@ -3306,7 +3313,7 @@ async function validateSchema(context, result) {
|
|
|
3306
3313
|
}
|
|
3307
3314
|
}
|
|
3308
3315
|
const hasCapability = Boolean(
|
|
3309
|
-
manifest.contracts || manifest.dynamic_contracts || manifest.actions || manifest.validators || manifest.routes || manifest.services || manifest.contexts || manifest.init || manifest.setup || manifest.agentkit || manifest.commands
|
|
3316
|
+
manifest.contracts || manifest.dynamic_contracts || manifest.actions || manifest.validators || manifest.projectValidators || manifest.routes || manifest.services || manifest.contexts || manifest.init || manifest.setup || manifest.agentkit || manifest.commands
|
|
3310
3317
|
);
|
|
3311
3318
|
if (!hasCapability) {
|
|
3312
3319
|
if (manifest.global === true) {
|
|
@@ -3322,7 +3329,7 @@ async function validateSchema(context, result) {
|
|
|
3322
3329
|
} else {
|
|
3323
3330
|
result.warnings.push({
|
|
3324
3331
|
type: "schema",
|
|
3325
|
-
message: "Plugin declares no capabilities (contracts, dynamic_contracts, actions, validators, routes, services, contexts, init, setup, agentkit, commands)",
|
|
3332
|
+
message: "Plugin declares no capabilities (contracts, dynamic_contracts, actions, validators, projectValidators, routes, services, contexts, init, setup, agentkit, commands)",
|
|
3326
3333
|
location: "plugin.yaml",
|
|
3327
3334
|
suggestion: "Declare at least one capability. See agent-kit/plugin/plugin-structure.md"
|
|
3328
3335
|
});
|
|
@@ -3512,6 +3519,43 @@ async function validateSchema(context, result) {
|
|
|
3512
3519
|
});
|
|
3513
3520
|
}
|
|
3514
3521
|
}
|
|
3522
|
+
if (manifest.projectValidators) {
|
|
3523
|
+
if (!Array.isArray(manifest.projectValidators)) {
|
|
3524
|
+
result.errors.push({
|
|
3525
|
+
type: "schema",
|
|
3526
|
+
message: 'Field "projectValidators" must be an array',
|
|
3527
|
+
location: "plugin.yaml"
|
|
3528
|
+
});
|
|
3529
|
+
} else {
|
|
3530
|
+
manifest.projectValidators.forEach((validator, index) => {
|
|
3531
|
+
if (!validator.name) {
|
|
3532
|
+
result.errors.push({
|
|
3533
|
+
type: "schema",
|
|
3534
|
+
message: `Project validator at index ${index} is missing "name" field`,
|
|
3535
|
+
location: "plugin.yaml"
|
|
3536
|
+
});
|
|
3537
|
+
}
|
|
3538
|
+
if (!validator.handler) {
|
|
3539
|
+
result.errors.push({
|
|
3540
|
+
type: "schema",
|
|
3541
|
+
message: `Project validator "${validator.name || index}" is missing "handler" field`,
|
|
3542
|
+
location: "plugin.yaml",
|
|
3543
|
+
suggestion: "Specify the relative path to the project validator handler module"
|
|
3544
|
+
});
|
|
3545
|
+
}
|
|
3546
|
+
if (validator.handler) {
|
|
3547
|
+
validateHandlerRef(
|
|
3548
|
+
validator.handler,
|
|
3549
|
+
`Project validator "${validator.name}" handler`,
|
|
3550
|
+
"plugin.yaml projectValidators",
|
|
3551
|
+
context,
|
|
3552
|
+
result,
|
|
3553
|
+
"./tools"
|
|
3554
|
+
);
|
|
3555
|
+
}
|
|
3556
|
+
});
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
3515
3559
|
if (manifest.setup) {
|
|
3516
3560
|
if (typeof manifest.setup !== "string") {
|
|
3517
3561
|
result.errors.push({
|
|
@@ -3593,7 +3637,7 @@ function hasComponentCapability(context) {
|
|
|
3593
3637
|
return Boolean(m.contracts || m.dynamic_contracts || m.routes);
|
|
3594
3638
|
}
|
|
3595
3639
|
function needsToolsEntry(manifest) {
|
|
3596
|
-
if (manifest.validators || manifest.commands || manifest.agentkit || manifest.setup) {
|
|
3640
|
+
if (manifest.validators || manifest.projectValidators || manifest.commands || manifest.agentkit || manifest.setup) {
|
|
3597
3641
|
return true;
|
|
3598
3642
|
}
|
|
3599
3643
|
if (manifest.actions) {
|
|
@@ -3695,6 +3739,25 @@ function resolveContractFile(contractSpec, context) {
|
|
|
3695
3739
|
}
|
|
3696
3740
|
async function validateContract(contract, index, context, generateTypes, result) {
|
|
3697
3741
|
result.contractsChecked = (result.contractsChecked || 0) + 1;
|
|
3742
|
+
const hasEditingGuide = typeof contract.editingGuide === "string" && contract.editingGuide.length > 0;
|
|
3743
|
+
const isReadonly = contract.readonly === true;
|
|
3744
|
+
if (!hasEditingGuide && !isReadonly) {
|
|
3745
|
+
result.warnings.push({
|
|
3746
|
+
type: "source-editing-coverage",
|
|
3747
|
+
message: `Contract "${contract.name}" declares no source-editing story. Add "editingGuide: <path.md>" (how to edit its bound data) or "readonly: true" (not editable in place).`,
|
|
3748
|
+
location: `plugin.yaml contracts[${index}]`,
|
|
3749
|
+
suggestion: "See the plugin source-editing guide (DL#219)."
|
|
3750
|
+
});
|
|
3751
|
+
} else if (hasEditingGuide) {
|
|
3752
|
+
const guideFullPath = path$1.resolve(context.pluginPath, contract.editingGuide);
|
|
3753
|
+
if (!fs$1.existsSync(guideFullPath)) {
|
|
3754
|
+
result.warnings.push({
|
|
3755
|
+
type: "source-editing-coverage",
|
|
3756
|
+
message: `Contract "${contract.name}" editingGuide file not found: ${contract.editingGuide}`,
|
|
3757
|
+
location: `plugin.yaml contracts[${index}]`
|
|
3758
|
+
});
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3698
3761
|
const contractPath = resolveContractFile(contract.contract, context);
|
|
3699
3762
|
if (!contractPath) {
|
|
3700
3763
|
result.errors.push({
|
|
@@ -4198,7 +4261,7 @@ function facetLabel(f) {
|
|
|
4198
4261
|
}
|
|
4199
4262
|
}
|
|
4200
4263
|
const CONTENT_MARKER = "jay-content";
|
|
4201
|
-
const META_ATTRS = /* @__PURE__ */ new Set(["jc", "override", "page-scope", CONTENT_MARKER]);
|
|
4264
|
+
const META_ATTRS = /* @__PURE__ */ new Set(["jc", "override", "page-scope", CONTENT_MARKER, "jay-policy"]);
|
|
4202
4265
|
function isMetaAttr(name) {
|
|
4203
4266
|
return META_ATTRS.has(name.toLowerCase());
|
|
4204
4267
|
}
|
|
@@ -4821,6 +4884,8 @@ function normalizeCssBody(body) {
|
|
|
4821
4884
|
}
|
|
4822
4885
|
const SUPPRESS_KEY = "allow-broken-links";
|
|
4823
4886
|
const SUPPRESS_HINT = 'suppress with jay-stack:\n allow-broken-links: true\nin a <script type="application/jay-validations"> block';
|
|
4887
|
+
const SELF_LINK_ALLOW_ATTR = "jay-allow-self-link";
|
|
4888
|
+
const TODO_SCHEME = /^todo:/i;
|
|
4824
4889
|
function normalizePath(p) {
|
|
4825
4890
|
let s2 = p.split("#")[0].split("?")[0];
|
|
4826
4891
|
s2 = s2.replace(/\/{2,}/g, "/");
|
|
@@ -4916,64 +4981,137 @@ function originOf(baseUrl) {
|
|
|
4916
4981
|
return void 0;
|
|
4917
4982
|
}
|
|
4918
4983
|
}
|
|
4984
|
+
function fragmentOf(raw) {
|
|
4985
|
+
const hash = raw.indexOf("#");
|
|
4986
|
+
return hash === -1 ? "" : raw.slice(hash + 1);
|
|
4987
|
+
}
|
|
4919
4988
|
function classifyHref(raw, baseUrl) {
|
|
4920
4989
|
if (raw.includes("{")) return { kind: "skip" };
|
|
4921
4990
|
const t = raw.trim();
|
|
4922
4991
|
if (t === "" || t === "#") return { kind: "degenerate" };
|
|
4923
|
-
if (
|
|
4992
|
+
if (TODO_SCHEME.test(t)) return { kind: "todo", note: t.replace(TODO_SCHEME, "").trim() };
|
|
4993
|
+
if (t.startsWith("#")) return { kind: "localFragment", fragment: t.slice(1) };
|
|
4924
4994
|
if (/^(mailto:|tel:|javascript:|data:|sms:|ftp:)/i.test(t)) return { kind: "skip" };
|
|
4925
4995
|
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(t) || t.startsWith("//")) {
|
|
4926
4996
|
const sameOrigin = baseUrl ? originOf(baseUrl) : void 0;
|
|
4927
4997
|
if (sameOrigin) {
|
|
4928
4998
|
try {
|
|
4929
4999
|
const u2 = new URL(t.startsWith("//") ? "https:" + t : t);
|
|
4930
|
-
if (u2.origin === sameOrigin)
|
|
4931
|
-
|
|
5000
|
+
if (u2.origin === sameOrigin) {
|
|
5001
|
+
const frag = u2.hash.slice(1);
|
|
5002
|
+
return frag ? { kind: "check", targetPath: normalizePath(u2.pathname), fragment: frag } : { kind: "check", targetPath: normalizePath(u2.pathname) };
|
|
5003
|
+
}
|
|
4932
5004
|
} catch {
|
|
4933
5005
|
}
|
|
4934
5006
|
}
|
|
4935
5007
|
return { kind: "skip" };
|
|
4936
5008
|
}
|
|
4937
|
-
if (t.startsWith("/"))
|
|
5009
|
+
if (t.startsWith("/")) {
|
|
5010
|
+
const frag = fragmentOf(t);
|
|
5011
|
+
return frag ? { kind: "check", targetPath: normalizePath(t), fragment: frag } : { kind: "check", targetPath: normalizePath(t) };
|
|
5012
|
+
}
|
|
4938
5013
|
return { kind: "skip" };
|
|
4939
5014
|
}
|
|
5015
|
+
function fragmentNeedsAnchor(fragment) {
|
|
5016
|
+
const f = fragment.trim();
|
|
5017
|
+
return f !== "" && f.toLowerCase() !== "top";
|
|
5018
|
+
}
|
|
5019
|
+
function collectAnchorIds(body) {
|
|
5020
|
+
const ids = /* @__PURE__ */ new Set();
|
|
5021
|
+
for (const el of body.querySelectorAll("[id]")) {
|
|
5022
|
+
const id = el.getAttribute("id");
|
|
5023
|
+
if (id) ids.add(id);
|
|
5024
|
+
}
|
|
5025
|
+
for (const a of body.querySelectorAll("a[name]")) {
|
|
5026
|
+
const name = a.getAttribute("name");
|
|
5027
|
+
if (name) ids.add(name);
|
|
5028
|
+
}
|
|
5029
|
+
return ids;
|
|
5030
|
+
}
|
|
4940
5031
|
function checkInternalLinks(options) {
|
|
4941
5032
|
const { parsedFiles, oracle, assetUrls, baseUrl, projectRoot } = options;
|
|
4942
5033
|
const findings = [];
|
|
5034
|
+
const idsByUrl = /* @__PURE__ */ new Map();
|
|
4943
5035
|
for (const { relativePath, parsed } of parsedFiles) {
|
|
4944
|
-
|
|
5036
|
+
const url = oracle.selfUrlByPath.get(path$1.resolve(projectRoot ?? ".", relativePath));
|
|
5037
|
+
if (url !== void 0) idsByUrl.set(url, collectAnchorIds(parsed.body));
|
|
5038
|
+
}
|
|
5039
|
+
for (const { relativePath, parsed } of parsedFiles) {
|
|
5040
|
+
const allowBroken = parsed.validationOverrides?.["jay-stack"]?.[SUPPRESS_KEY] === true;
|
|
4945
5041
|
const ownUrl = oracle.selfUrlByPath.get(path$1.resolve(projectRoot ?? ".", relativePath));
|
|
5042
|
+
const ownIds = ownUrl !== void 0 ? idsByUrl.get(ownUrl) : void 0;
|
|
4946
5043
|
for (const anchor of parsed.body.querySelectorAll("a[href]")) {
|
|
4947
5044
|
const raw = anchor.getAttribute("href");
|
|
4948
5045
|
if (raw === void 0 || raw === null) continue;
|
|
4949
5046
|
const verdict = classifyHref(raw, baseUrl);
|
|
4950
5047
|
if (verdict.kind === "skip") continue;
|
|
4951
5048
|
const tag = openingTag(anchor);
|
|
5049
|
+
if (verdict.kind === "todo") {
|
|
5050
|
+
const note = verdict.note ? ` — intended target "${verdict.note}" is not wired up yet` : "";
|
|
5051
|
+
findings.push({
|
|
5052
|
+
file: relativePath,
|
|
5053
|
+
severity: "warning",
|
|
5054
|
+
message: `TODO link ${tag}${note}. This reminder cannot be suppressed — resolve it before release.`,
|
|
5055
|
+
suggestion: `Replace the "todo:" href with the real destination once the page/route exists.`
|
|
5056
|
+
});
|
|
5057
|
+
continue;
|
|
5058
|
+
}
|
|
5059
|
+
if (verdict.kind === "localFragment") {
|
|
5060
|
+
if (!fragmentNeedsAnchor(verdict.fragment)) continue;
|
|
5061
|
+
if (ownIds && !ownIds.has(verdict.fragment)) {
|
|
5062
|
+
findings.push({
|
|
5063
|
+
file: relativePath,
|
|
5064
|
+
severity: "error",
|
|
5065
|
+
message: `Broken fragment link ${tag} — this page has no element with id "${verdict.fragment}".`,
|
|
5066
|
+
suggestion: `Add id="${verdict.fragment}" to the target element, or fix the fragment.`
|
|
5067
|
+
});
|
|
5068
|
+
}
|
|
5069
|
+
continue;
|
|
5070
|
+
}
|
|
4952
5071
|
if (verdict.kind === "degenerate") {
|
|
5072
|
+
if (allowBroken) continue;
|
|
4953
5073
|
findings.push({
|
|
4954
5074
|
file: relativePath,
|
|
5075
|
+
severity: "error",
|
|
4955
5076
|
message: `Anchor ${tag} has a placeholder href "${raw}" — it links nowhere and ships as a dead link.`,
|
|
4956
5077
|
suggestion: `Point the link at a real page, use <button> for a JS-driven action, or ${SUPPRESS_HINT}.`
|
|
4957
5078
|
});
|
|
4958
5079
|
continue;
|
|
4959
5080
|
}
|
|
4960
5081
|
const target = verdict.targetPath;
|
|
4961
|
-
|
|
5082
|
+
const fragment = verdict.fragment ?? "";
|
|
5083
|
+
if (ownUrl !== void 0 && target === ownUrl && !fragmentNeedsAnchor(fragment)) {
|
|
5084
|
+
if (anchor.hasAttribute(SELF_LINK_ALLOW_ATTR)) continue;
|
|
4962
5085
|
findings.push({
|
|
4963
5086
|
file: relativePath,
|
|
5087
|
+
severity: "warning",
|
|
4964
5088
|
message: `Anchor ${tag} points to the page's own route "${target}" — a self-link goes nowhere.`,
|
|
4965
|
-
suggestion: `Remove the link, use "#"
|
|
5089
|
+
suggestion: `Remove the link, use an in-page "#" fragment, or add ${SELF_LINK_ALLOW_ATTR} to the anchor if intentional.`
|
|
4966
5090
|
});
|
|
4967
5091
|
continue;
|
|
4968
5092
|
}
|
|
4969
|
-
|
|
4970
|
-
if (
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
5093
|
+
const resolves = oracle.staticUrls.has(target) || assetUrls.has(target) || oracle.dynamicMatchers.some((re) => re.test(target));
|
|
5094
|
+
if (!resolves) {
|
|
5095
|
+
if (allowBroken) continue;
|
|
5096
|
+
findings.push({
|
|
5097
|
+
file: relativePath,
|
|
5098
|
+
severity: "error",
|
|
5099
|
+
message: `Broken internal link in anchor ${tag} — no page or public asset produces the URL "${target}".`,
|
|
5100
|
+
suggestion: `Create a placeholder page/route for "${target}", or remove the link. Otherwise ${SUPPRESS_HINT}.`
|
|
5101
|
+
});
|
|
5102
|
+
continue;
|
|
5103
|
+
}
|
|
5104
|
+
if (fragmentNeedsAnchor(fragment)) {
|
|
5105
|
+
const targetIds = target === ownUrl ? ownIds : idsByUrl.get(target);
|
|
5106
|
+
if (targetIds && !targetIds.has(fragment)) {
|
|
5107
|
+
findings.push({
|
|
5108
|
+
file: relativePath,
|
|
5109
|
+
severity: "error",
|
|
5110
|
+
message: `Broken fragment link ${tag} — page "${target}" has no element with id "${fragment}".`,
|
|
5111
|
+
suggestion: `Add id="${fragment}" to the target element on "${target}", or fix the fragment.`
|
|
5112
|
+
});
|
|
5113
|
+
}
|
|
5114
|
+
}
|
|
4977
5115
|
}
|
|
4978
5116
|
}
|
|
4979
5117
|
return findings;
|
|
@@ -4983,6 +5121,47 @@ const LINK_SUPPRESS_HINT = 'suppress with jay-stack:\n allow-broken-links: true
|
|
|
4983
5121
|
const UNRESOLVED_BINDING = /\{[A-Za-z_$][\w$]*(?:\.[\w$]+)*\}/;
|
|
4984
5122
|
const TITLE_MAX = 60;
|
|
4985
5123
|
const DESCRIPTION_MAX = 160;
|
|
5124
|
+
const PAGE_CACHE_SOFT_BYTES = 300 * 1024;
|
|
5125
|
+
const PAGE_CACHE_HARD_BYTES = 512 * 1024;
|
|
5126
|
+
async function checkPageCacheFileSizes(args) {
|
|
5127
|
+
const { cacheAbsPath, routePattern } = args;
|
|
5128
|
+
const findings = [];
|
|
5129
|
+
const rel = path$1.basename(path$1.dirname(cacheAbsPath)) + "/" + path$1.basename(cacheAbsPath);
|
|
5130
|
+
let bytes;
|
|
5131
|
+
try {
|
|
5132
|
+
bytes = (await promises.stat(cacheAbsPath)).size;
|
|
5133
|
+
} catch (err) {
|
|
5134
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
5135
|
+
findings.push({
|
|
5136
|
+
file: routePattern,
|
|
5137
|
+
message: `${rel} is listed in the build manifest but could not be read (${detail}).`,
|
|
5138
|
+
suggestion: "Rebuild the project or fix the missing pre-rendered cache file.",
|
|
5139
|
+
severity: "error",
|
|
5140
|
+
source: "build-output"
|
|
5141
|
+
});
|
|
5142
|
+
return findings;
|
|
5143
|
+
}
|
|
5144
|
+
if (bytes >= PAGE_CACHE_HARD_BYTES) {
|
|
5145
|
+
const hardKiB = PAGE_CACHE_HARD_BYTES / 1024;
|
|
5146
|
+
findings.push({
|
|
5147
|
+
file: routePattern,
|
|
5148
|
+
message: `${rel} is ${bytes} bytes (≥ ${PAGE_CACHE_HARD_BYTES} bytes / ${hardKiB} KiB hard limit). Shrink slow-phase carry before deploy.`,
|
|
5149
|
+
suggestion: "Reduce slow-phase carryForward in this cache file before deploy (smaller payloads, defer non-first-paint data). See agent-kit developer/query-param-pages.md — cache size thresholds.",
|
|
5150
|
+
severity: "error",
|
|
5151
|
+
source: "build-output"
|
|
5152
|
+
});
|
|
5153
|
+
} else if (bytes >= PAGE_CACHE_SOFT_BYTES) {
|
|
5154
|
+
const softKiB = PAGE_CACHE_SOFT_BYTES / 1024;
|
|
5155
|
+
findings.push({
|
|
5156
|
+
file: routePattern,
|
|
5157
|
+
message: `${rel} is ${bytes} bytes (≥ ${PAGE_CACHE_SOFT_BYTES} bytes / ${softKiB} KiB soft budget). Consider deferring non-critical carry data.`,
|
|
5158
|
+
suggestion: "Review carryForward size: keep only what the default view needs for first paint; defer the rest (per-request fetch or post-hydration load). Oversize caches often need product decisions about what loads when — see agent-kit developer/query-param-pages.md.",
|
|
5159
|
+
severity: "warning",
|
|
5160
|
+
source: "build-output"
|
|
5161
|
+
});
|
|
5162
|
+
}
|
|
5163
|
+
return findings;
|
|
5164
|
+
}
|
|
4986
5165
|
function buildUrlFromManifest(pattern, params) {
|
|
4987
5166
|
return pattern.replace(/\[\[(\w+)\]\]/g, (_, name) => params[name] || "").replace(/\[\.\.\.(\w+)\]/g, (_, name) => params[name] || "").replace(/\[(\w+)\]/g, (_, name) => params[name] || "").replace(/\/\/+/g, "/").replace(/\/$/, "") || "/";
|
|
4988
5167
|
}
|
|
@@ -5053,16 +5232,15 @@ async function loadBuildManifest(backendDir) {
|
|
|
5053
5232
|
}
|
|
5054
5233
|
return { manifest, metadata };
|
|
5055
5234
|
}
|
|
5056
|
-
function buildBuildOracle(manifest, backendDir) {
|
|
5235
|
+
async function buildBuildOracle(manifest, backendDir) {
|
|
5236
|
+
const store = new FilesystemArtifactStore(backendDir);
|
|
5057
5237
|
const urls = /* @__PURE__ */ new Set();
|
|
5058
5238
|
const instances = [];
|
|
5059
5239
|
for (const route of manifest.routes) {
|
|
5060
5240
|
const hasDynamic = route.segments.some((s2) => s2.type !== "static");
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
}
|
|
5065
|
-
for (const instance of route.instances) {
|
|
5241
|
+
let count = 0;
|
|
5242
|
+
for await (const instance of store.listInstances(route, { fresh: true })) {
|
|
5243
|
+
count++;
|
|
5066
5244
|
const url = routeInstanceUrl(route, instance);
|
|
5067
5245
|
urls.add(url);
|
|
5068
5246
|
instances.push({
|
|
@@ -5072,6 +5250,7 @@ function buildBuildOracle(manifest, backendDir) {
|
|
|
5072
5250
|
headMeta: route.headMeta
|
|
5073
5251
|
});
|
|
5074
5252
|
}
|
|
5253
|
+
if (count === 0 && !hasDynamic) urls.add(normalizePath(route.pattern));
|
|
5075
5254
|
}
|
|
5076
5255
|
return { urls, instances };
|
|
5077
5256
|
}
|
|
@@ -5329,122 +5508,6 @@ async function newestSourceMtime(dirs) {
|
|
|
5329
5508
|
for (const dir of dirs) await walk(dir);
|
|
5330
5509
|
return newest;
|
|
5331
5510
|
}
|
|
5332
|
-
const URN_PATTERN = /^urn:air:[^:\s]+:[^:\s]+:[^:\s]+$/;
|
|
5333
|
-
const MEDIA_TYPE_PATTERN = /^[a-z]+\/[A-Za-z0-9.+-]+$/;
|
|
5334
|
-
const ARD_SUGGEST = "See developer/seo-guide.md (ARD v0.91) for the schema and a worked example.";
|
|
5335
|
-
function validateArdDocument(doc) {
|
|
5336
|
-
const findings = [];
|
|
5337
|
-
if (!doc || typeof doc !== "object") {
|
|
5338
|
-
return [
|
|
5339
|
-
{
|
|
5340
|
-
message: "ard.json must be a JSON object with an `entries` array.",
|
|
5341
|
-
severity: "error",
|
|
5342
|
-
suggestion: ARD_SUGGEST
|
|
5343
|
-
}
|
|
5344
|
-
];
|
|
5345
|
-
}
|
|
5346
|
-
const entries = doc.entries;
|
|
5347
|
-
if (!Array.isArray(entries)) {
|
|
5348
|
-
return [
|
|
5349
|
-
{
|
|
5350
|
-
message: "ard.json is missing the required `entries` array.",
|
|
5351
|
-
severity: "error",
|
|
5352
|
-
suggestion: ARD_SUGGEST
|
|
5353
|
-
}
|
|
5354
|
-
];
|
|
5355
|
-
}
|
|
5356
|
-
entries.forEach((entry, i) => {
|
|
5357
|
-
const at = `ard.json entries[${i}]`;
|
|
5358
|
-
if (!entry || typeof entry !== "object") {
|
|
5359
|
-
findings.push({ message: `${at} must be an object.`, severity: "error" });
|
|
5360
|
-
return;
|
|
5361
|
-
}
|
|
5362
|
-
if (typeof entry.identifier !== "string" || !URN_PATTERN.test(entry.identifier)) {
|
|
5363
|
-
findings.push({
|
|
5364
|
-
message: `${at}.identifier must match urn:air:<publisher>:<namespace>:<name>.`,
|
|
5365
|
-
severity: "error",
|
|
5366
|
-
suggestion: ARD_SUGGEST
|
|
5367
|
-
});
|
|
5368
|
-
}
|
|
5369
|
-
if (typeof entry.displayName !== "string" || entry.displayName.trim() === "") {
|
|
5370
|
-
findings.push({
|
|
5371
|
-
message: `${at}.displayName is required.`,
|
|
5372
|
-
severity: "error"
|
|
5373
|
-
});
|
|
5374
|
-
}
|
|
5375
|
-
if (typeof entry.type !== "string" || !MEDIA_TYPE_PATTERN.test(entry.type)) {
|
|
5376
|
-
findings.push({
|
|
5377
|
-
message: `${at}.type must be an IANA media type (e.g. application/mcp-server-card+json).`,
|
|
5378
|
-
severity: "error"
|
|
5379
|
-
});
|
|
5380
|
-
}
|
|
5381
|
-
const hasUrl = entry.url !== void 0;
|
|
5382
|
-
const hasData = entry.data !== void 0;
|
|
5383
|
-
if (hasUrl === hasData) {
|
|
5384
|
-
findings.push({
|
|
5385
|
-
message: `${at} must have exactly one of \`url\` or \`data\`.`,
|
|
5386
|
-
severity: "error"
|
|
5387
|
-
});
|
|
5388
|
-
}
|
|
5389
|
-
const rq = entry.representativeQueries;
|
|
5390
|
-
if (rq === void 0) {
|
|
5391
|
-
findings.push({
|
|
5392
|
-
message: `${at} is missing representativeQueries (2–5 recommended) — agents may not surface this resource.`,
|
|
5393
|
-
severity: "warning",
|
|
5394
|
-
suggestion: ARD_SUGGEST
|
|
5395
|
-
});
|
|
5396
|
-
} else if (!Array.isArray(rq) || rq.length < 2 || rq.length > 5) {
|
|
5397
|
-
findings.push({
|
|
5398
|
-
message: `${at}.representativeQueries should list 2–5 example queries.`,
|
|
5399
|
-
severity: "warning"
|
|
5400
|
-
});
|
|
5401
|
-
}
|
|
5402
|
-
});
|
|
5403
|
-
return findings;
|
|
5404
|
-
}
|
|
5405
|
-
async function checkArdFiles(projectRoot) {
|
|
5406
|
-
const wellKnown = path$1.resolve(projectRoot, "public/.well-known");
|
|
5407
|
-
const ardPath = path$1.join(wellKnown, "ard.json");
|
|
5408
|
-
const aliasPath = path$1.join(wellKnown, "ai-catalog.json");
|
|
5409
|
-
const ardRaw = await readIfExists(ardPath);
|
|
5410
|
-
const aliasRaw = await readIfExists(aliasPath);
|
|
5411
|
-
if (ardRaw === null && aliasRaw === null) {
|
|
5412
|
-
return { findings: [], absent: true };
|
|
5413
|
-
}
|
|
5414
|
-
const findings = [];
|
|
5415
|
-
if (ardRaw === null && aliasRaw !== null) {
|
|
5416
|
-
findings.push({
|
|
5417
|
-
message: "Only public/.well-known/ai-catalog.json found — ARD renamed this to ard.json. Add ard.json (keep ai-catalog.json as a copy for older clients).",
|
|
5418
|
-
severity: "warning",
|
|
5419
|
-
suggestion: ARD_SUGGEST
|
|
5420
|
-
});
|
|
5421
|
-
} else if (ardRaw !== null && aliasRaw !== null && ardRaw.trim() !== aliasRaw.trim()) {
|
|
5422
|
-
findings.push({
|
|
5423
|
-
message: "public/.well-known/ard.json and ai-catalog.json differ — the alias should be a copy of ard.json.",
|
|
5424
|
-
severity: "warning"
|
|
5425
|
-
});
|
|
5426
|
-
}
|
|
5427
|
-
const canonicalRaw = ardRaw ?? aliasRaw;
|
|
5428
|
-
let parsed;
|
|
5429
|
-
try {
|
|
5430
|
-
parsed = JSON.parse(canonicalRaw);
|
|
5431
|
-
} catch (e2) {
|
|
5432
|
-
findings.push({
|
|
5433
|
-
message: `${ardRaw !== null ? "ard.json" : "ai-catalog.json"} is not valid JSON: ${e2.message}`,
|
|
5434
|
-
severity: "error"
|
|
5435
|
-
});
|
|
5436
|
-
return { findings, absent: false };
|
|
5437
|
-
}
|
|
5438
|
-
findings.push(...validateArdDocument(parsed));
|
|
5439
|
-
return { findings, absent: false };
|
|
5440
|
-
}
|
|
5441
|
-
async function readIfExists(p) {
|
|
5442
|
-
try {
|
|
5443
|
-
return await promises.readFile(p, "utf8");
|
|
5444
|
-
} catch {
|
|
5445
|
-
return null;
|
|
5446
|
-
}
|
|
5447
|
-
}
|
|
5448
5511
|
async function findJayFiles(dir) {
|
|
5449
5512
|
return await glob(`${dir}/**/*${JAY_EXTENSION$1}`);
|
|
5450
5513
|
}
|
|
@@ -6448,6 +6511,37 @@ function resolveContractLinks(contract, contractPath) {
|
|
|
6448
6511
|
const contractDir = path$1.dirname(contractPath);
|
|
6449
6512
|
return { ...contract, tags: resolveLinkedTags(contract.tags, contractDir) };
|
|
6450
6513
|
}
|
|
6514
|
+
async function loadPluginToolsHandler(plugin, handlerRef, capabilityLabel, name, source, errors) {
|
|
6515
|
+
try {
|
|
6516
|
+
let handlerModule;
|
|
6517
|
+
if (plugin.isLocal) {
|
|
6518
|
+
const handlerPath = path$1.resolve(plugin.pluginPath, handlerRef);
|
|
6519
|
+
handlerModule = await import(handlerPath);
|
|
6520
|
+
} else {
|
|
6521
|
+
handlerModule = await import(`${plugin.packageName}/tools`);
|
|
6522
|
+
}
|
|
6523
|
+
const fn = plugin.isLocal ? handlerModule.validate ?? handlerModule.default : handlerModule[handlerRef];
|
|
6524
|
+
if (typeof fn !== "function") {
|
|
6525
|
+
const expected = plugin.isLocal ? "validate" : handlerRef;
|
|
6526
|
+
errors.push({
|
|
6527
|
+
file: `plugin:${plugin.name}`,
|
|
6528
|
+
message: `${capabilityLabel} "${name}" handler does not export a "${expected}" function`,
|
|
6529
|
+
stage: "plugin",
|
|
6530
|
+
source
|
|
6531
|
+
});
|
|
6532
|
+
return void 0;
|
|
6533
|
+
}
|
|
6534
|
+
return fn;
|
|
6535
|
+
} catch (loadErr) {
|
|
6536
|
+
errors.push({
|
|
6537
|
+
file: `plugin:${plugin.name}`,
|
|
6538
|
+
message: `Failed to load ${capabilityLabel.toLowerCase()} "${name}": ${loadErr.message}`,
|
|
6539
|
+
stage: "plugin",
|
|
6540
|
+
source
|
|
6541
|
+
});
|
|
6542
|
+
return void 0;
|
|
6543
|
+
}
|
|
6544
|
+
}
|
|
6451
6545
|
async function runPluginValidators(projectRoot, parsedFiles, errors, warnings) {
|
|
6452
6546
|
const scannedPlugins = await scanPlugins({ projectRoot, includeDevDeps: true });
|
|
6453
6547
|
const loadedValidators = [];
|
|
@@ -6455,37 +6549,16 @@ async function runPluginValidators(projectRoot, parsedFiles, errors, warnings) {
|
|
|
6455
6549
|
if (!plugin.manifest.validators) continue;
|
|
6456
6550
|
for (const validatorDef of plugin.manifest.validators) {
|
|
6457
6551
|
const source = `${plugin.name}/${validatorDef.name}`;
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
6463
|
-
|
|
6464
|
-
|
|
6465
|
-
|
|
6466
|
-
}
|
|
6467
|
-
validatorFn = plugin.isLocal ? handlerModule.validate ?? handlerModule.default : handlerModule[validatorDef.handler];
|
|
6468
|
-
if (typeof validatorFn !== "function") {
|
|
6469
|
-
errors.push({
|
|
6470
|
-
file: `plugin:${plugin.name}`,
|
|
6471
|
-
message: `Validator "${validatorDef.name}" handler does not export a "validate" function`,
|
|
6472
|
-
stage: "plugin",
|
|
6473
|
-
source
|
|
6474
|
-
});
|
|
6475
|
-
loadedValidators.push(source);
|
|
6476
|
-
continue;
|
|
6477
|
-
}
|
|
6478
|
-
} catch (loadErr) {
|
|
6479
|
-
errors.push({
|
|
6480
|
-
file: `plugin:${plugin.name}`,
|
|
6481
|
-
message: `Failed to load validator "${validatorDef.name}": ${loadErr.message}`,
|
|
6482
|
-
stage: "plugin",
|
|
6483
|
-
source
|
|
6484
|
-
});
|
|
6485
|
-
loadedValidators.push(source);
|
|
6486
|
-
continue;
|
|
6487
|
-
}
|
|
6552
|
+
const validatorFn = await loadPluginToolsHandler(
|
|
6553
|
+
plugin,
|
|
6554
|
+
validatorDef.handler,
|
|
6555
|
+
"Validator",
|
|
6556
|
+
validatorDef.name,
|
|
6557
|
+
source,
|
|
6558
|
+
errors
|
|
6559
|
+
);
|
|
6488
6560
|
loadedValidators.push(source);
|
|
6561
|
+
if (!validatorFn) continue;
|
|
6489
6562
|
for (const { relativePath, parsed } of parsedFiles) {
|
|
6490
6563
|
const pageContractPath = parsed.contractRef ? path$1.resolve(
|
|
6491
6564
|
path$1.dirname(path$1.resolve(projectRoot, relativePath)),
|
|
@@ -6568,6 +6641,56 @@ async function runPluginValidators(projectRoot, parsedFiles, errors, warnings) {
|
|
|
6568
6641
|
}
|
|
6569
6642
|
return loadedValidators;
|
|
6570
6643
|
}
|
|
6644
|
+
async function runProjectValidators(projectRoot, errors, warnings) {
|
|
6645
|
+
const scannedPlugins = await scanPlugins({ projectRoot, includeDevDeps: true });
|
|
6646
|
+
const loadedValidators = [];
|
|
6647
|
+
for (const [, plugin] of scannedPlugins) {
|
|
6648
|
+
if (!plugin.manifest.projectValidators) continue;
|
|
6649
|
+
for (const validatorDef of plugin.manifest.projectValidators) {
|
|
6650
|
+
const source = `${plugin.name}/${validatorDef.name}`;
|
|
6651
|
+
const validatorFn = await loadPluginToolsHandler(
|
|
6652
|
+
plugin,
|
|
6653
|
+
validatorDef.handler,
|
|
6654
|
+
"Project validator",
|
|
6655
|
+
validatorDef.name,
|
|
6656
|
+
source,
|
|
6657
|
+
errors
|
|
6658
|
+
);
|
|
6659
|
+
loadedValidators.push(source);
|
|
6660
|
+
if (!validatorFn) continue;
|
|
6661
|
+
const ctx = { projectRoot };
|
|
6662
|
+
try {
|
|
6663
|
+
const findings = await validatorFn(ctx);
|
|
6664
|
+
for (const finding of findings) {
|
|
6665
|
+
if (finding.severity === "error") {
|
|
6666
|
+
errors.push({
|
|
6667
|
+
file: finding.file,
|
|
6668
|
+
message: finding.message,
|
|
6669
|
+
stage: "plugin",
|
|
6670
|
+
source,
|
|
6671
|
+
suggestion: finding.suggestion
|
|
6672
|
+
});
|
|
6673
|
+
} else {
|
|
6674
|
+
warnings.push({
|
|
6675
|
+
file: finding.file,
|
|
6676
|
+
message: finding.message,
|
|
6677
|
+
source,
|
|
6678
|
+
suggestion: finding.suggestion
|
|
6679
|
+
});
|
|
6680
|
+
}
|
|
6681
|
+
}
|
|
6682
|
+
} catch (runErr) {
|
|
6683
|
+
errors.push({
|
|
6684
|
+
file: ".jay",
|
|
6685
|
+
message: `Project validator "${source}" threw: ${runErr.message}`,
|
|
6686
|
+
stage: "plugin",
|
|
6687
|
+
source
|
|
6688
|
+
});
|
|
6689
|
+
}
|
|
6690
|
+
}
|
|
6691
|
+
}
|
|
6692
|
+
return loadedValidators;
|
|
6693
|
+
}
|
|
6571
6694
|
async function validateJayFiles(options = {}) {
|
|
6572
6695
|
const projectRoot = options.projectRoot ?? process.cwd();
|
|
6573
6696
|
const config = loadConfig(projectRoot);
|
|
@@ -6924,56 +7047,6 @@ async function validateJayFiles(options = {}) {
|
|
|
6924
7047
|
}
|
|
6925
7048
|
};
|
|
6926
7049
|
}
|
|
6927
|
-
const robotsTxtPath = path$1.resolve(projectRoot, "public/robots.txt");
|
|
6928
|
-
if (!fs$1.existsSync(robotsTxtPath)) {
|
|
6929
|
-
warnings.push({
|
|
6930
|
-
file: "public/robots.txt",
|
|
6931
|
-
message: "public/robots.txt not found — search engines may crawl pages you don't intend to expose.",
|
|
6932
|
-
suggestion: "Create public/robots.txt with: User-agent: *\nAllow: /\nSitemap: https://your-domain.com/sitemap.xml"
|
|
6933
|
-
});
|
|
6934
|
-
}
|
|
6935
|
-
if (!config.site?.baseUrl) {
|
|
6936
|
-
warnings.push({
|
|
6937
|
-
file: ".jay",
|
|
6938
|
-
message: "site.baseUrl not configured — sitemap.xml will not be generated in production.",
|
|
6939
|
-
suggestion: "Add to .jay config:\n site:\n baseUrl: https://your-domain.com"
|
|
6940
|
-
});
|
|
6941
|
-
}
|
|
6942
|
-
if (!config.site?.name) {
|
|
6943
|
-
warnings.push({
|
|
6944
|
-
file: ".jay",
|
|
6945
|
-
message: "site.name not configured — Organization/WebSite JSON-LD and llms.txt will not be emitted, reducing how well search engines and agents understand the site.",
|
|
6946
|
-
suggestion: "Add to .jay config:\n site:\n name: Your Site Name\n logo: /logo.png\n description: What your site is about"
|
|
6947
|
-
});
|
|
6948
|
-
}
|
|
6949
|
-
const allowNoArd = config.validations?.["allow-no-ard"] === true;
|
|
6950
|
-
const ardResult = await checkArdFiles(projectRoot);
|
|
6951
|
-
if (ardResult.absent) {
|
|
6952
|
-
if (!allowNoArd) {
|
|
6953
|
-
warnings.push({
|
|
6954
|
-
file: "public/.well-known/ard.json",
|
|
6955
|
-
message: "public/.well-known/ard.json not found — agents cannot discover this site’s resources/APIs via Agentic Resource Discovery.",
|
|
6956
|
-
suggestion: "Author public/.well-known/ard.json (ARD v0.91), or suppress with `validations: { allow-no-ard: true }` in .jay if the site has no agent resources to advertise. See developer/seo-guide.md."
|
|
6957
|
-
});
|
|
6958
|
-
}
|
|
6959
|
-
} else {
|
|
6960
|
-
for (const finding of ardResult.findings) {
|
|
6961
|
-
if (finding.severity === "error") {
|
|
6962
|
-
errors.push({
|
|
6963
|
-
file: "public/.well-known/ard.json",
|
|
6964
|
-
message: finding.message,
|
|
6965
|
-
stage: "generate",
|
|
6966
|
-
suggestion: finding.suggestion
|
|
6967
|
-
});
|
|
6968
|
-
} else {
|
|
6969
|
-
warnings.push({
|
|
6970
|
-
file: "public/.well-known/ard.json",
|
|
6971
|
-
message: finding.message,
|
|
6972
|
-
suggestion: finding.suggestion
|
|
6973
|
-
});
|
|
6974
|
-
}
|
|
6975
|
-
}
|
|
6976
|
-
}
|
|
6977
7050
|
const allowNoErrorPages = config.validations?.["allow-no-error-pages"] === true;
|
|
6978
7051
|
if (!allowNoErrorPages) {
|
|
6979
7052
|
const scanResult = await scanRoutes(scanDir, {
|
|
@@ -7060,13 +7133,22 @@ async function validateJayFiles(options = {}) {
|
|
|
7060
7133
|
projectRoot
|
|
7061
7134
|
});
|
|
7062
7135
|
for (const finding of linkFindings) {
|
|
7063
|
-
|
|
7064
|
-
|
|
7065
|
-
|
|
7066
|
-
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
|
|
7136
|
+
if (finding.severity === "warning") {
|
|
7137
|
+
warnings.push({
|
|
7138
|
+
file: finding.file,
|
|
7139
|
+
message: finding.message,
|
|
7140
|
+
source: "internal-links",
|
|
7141
|
+
suggestion: finding.suggestion
|
|
7142
|
+
});
|
|
7143
|
+
} else {
|
|
7144
|
+
errors.push({
|
|
7145
|
+
file: finding.file,
|
|
7146
|
+
message: finding.message,
|
|
7147
|
+
stage: "generate",
|
|
7148
|
+
source: "internal-links",
|
|
7149
|
+
suggestion: finding.suggestion
|
|
7150
|
+
});
|
|
7151
|
+
}
|
|
7070
7152
|
}
|
|
7071
7153
|
if (options.tier2) {
|
|
7072
7154
|
await runBuildOutputValidation({
|
|
@@ -7081,6 +7163,7 @@ async function validateJayFiles(options = {}) {
|
|
|
7081
7163
|
});
|
|
7082
7164
|
}
|
|
7083
7165
|
const pluginValidators = await runPluginValidators(projectRoot, parsedFiles, errors, warnings);
|
|
7166
|
+
const projectValidators = await runProjectValidators(projectRoot, errors, warnings);
|
|
7084
7167
|
return {
|
|
7085
7168
|
valid: errors.length === 0,
|
|
7086
7169
|
jayHtmlFilesScanned: jayHtmlFiles.length,
|
|
@@ -7088,7 +7171,7 @@ async function validateJayFiles(options = {}) {
|
|
|
7088
7171
|
errors,
|
|
7089
7172
|
warnings,
|
|
7090
7173
|
coverage,
|
|
7091
|
-
pluginValidators,
|
|
7174
|
+
pluginValidators: [...pluginValidators, ...projectValidators],
|
|
7092
7175
|
designSystemCoverage,
|
|
7093
7176
|
designSystemReuse
|
|
7094
7177
|
};
|
|
@@ -7127,7 +7210,7 @@ async function runBuildOutputValidation(args) {
|
|
|
7127
7210
|
suggestion: "Re-run `jay-stack build` for accurate results."
|
|
7128
7211
|
});
|
|
7129
7212
|
}
|
|
7130
|
-
const oracle = buildBuildOracle(loaded.manifest, backendDir);
|
|
7213
|
+
const oracle = await buildBuildOracle(loaded.manifest, backendDir);
|
|
7131
7214
|
const findings = [];
|
|
7132
7215
|
findings.push(
|
|
7133
7216
|
...checkTemplateLinksAgainstBuild({
|
|
@@ -7139,6 +7222,14 @@ async function runBuildOutputValidation(args) {
|
|
|
7139
7222
|
})
|
|
7140
7223
|
);
|
|
7141
7224
|
findings.push(...checkBuildJsonLd(loaded.manifest));
|
|
7225
|
+
for (const inst of oracle.instances) {
|
|
7226
|
+
findings.push(
|
|
7227
|
+
...await checkPageCacheFileSizes({
|
|
7228
|
+
cacheAbsPath: inst.cacheAbsPath,
|
|
7229
|
+
routePattern: inst.pattern
|
|
7230
|
+
})
|
|
7231
|
+
);
|
|
7232
|
+
}
|
|
7142
7233
|
for (const instance of oracle.instances) {
|
|
7143
7234
|
const slowViewState = await loadSlowViewState(instance.cacheAbsPath);
|
|
7144
7235
|
findings.push(
|
|
@@ -7507,7 +7598,7 @@ async function resolveProductionContext(projectPath, versionOverride) {
|
|
|
7507
7598
|
let siteBaseUrl;
|
|
7508
7599
|
let site;
|
|
7509
7600
|
try {
|
|
7510
|
-
const jayConfig = YAML.parse(await fs$
|
|
7601
|
+
const jayConfig = YAML.parse(await fs$1$1.readFile(jayConfigPath, "utf-8"));
|
|
7511
7602
|
pagesBase = jayConfig?.devServer?.pagesBase || pagesBase;
|
|
7512
7603
|
siteBaseUrl = jayConfig?.site?.baseUrl;
|
|
7513
7604
|
site = jayConfig?.site;
|
|
@@ -7527,7 +7618,7 @@ async function resolveProductionContext(projectPath, versionOverride) {
|
|
|
7527
7618
|
async function resolveVersionFromPackageJson(projectRoot) {
|
|
7528
7619
|
try {
|
|
7529
7620
|
const pkgJson = JSON.parse(
|
|
7530
|
-
await fs$
|
|
7621
|
+
await fs$1$1.readFile(path__default.join(projectRoot, "package.json"), "utf-8")
|
|
7531
7622
|
);
|
|
7532
7623
|
if (pkgJson.version) {
|
|
7533
7624
|
return pkgJson.version;
|
|
@@ -7548,7 +7639,7 @@ async function runBuild(projectPath, options) {
|
|
|
7548
7639
|
}
|
|
7549
7640
|
const ctx = await resolveProductionContext(projectPath, options.version);
|
|
7550
7641
|
await generateDefinitionFiles(ctx);
|
|
7551
|
-
const { buildVersion } = await import("
|
|
7642
|
+
const { buildVersion } = await import("@jay-framework/production-build");
|
|
7552
7643
|
await buildVersion({
|
|
7553
7644
|
version: ctx.version,
|
|
7554
7645
|
projectRoot: ctx.resolvedPath,
|
|
@@ -7914,22 +8005,22 @@ async function ensureAgentKitDocs(projectRoot, _force, mode) {
|
|
|
7914
8005
|
const roleOutputDir = path__default.join(agentKitDir, role);
|
|
7915
8006
|
let files;
|
|
7916
8007
|
try {
|
|
7917
|
-
files = (await fs$
|
|
8008
|
+
files = (await fs$1$1.readdir(roleTemplateDir)).filter((f) => f.endsWith(".md"));
|
|
7918
8009
|
} catch {
|
|
7919
8010
|
continue;
|
|
7920
8011
|
}
|
|
7921
|
-
await fs$
|
|
8012
|
+
await fs$1$1.mkdir(roleOutputDir, { recursive: true });
|
|
7922
8013
|
for (const filename of files) {
|
|
7923
|
-
await fs$
|
|
8014
|
+
await fs$1$1.copyFile(
|
|
7924
8015
|
path__default.join(roleTemplateDir, filename),
|
|
7925
8016
|
path__default.join(roleOutputDir, filename)
|
|
7926
8017
|
);
|
|
7927
8018
|
getLogger().info(chalk.gray(` Created agent-kit/${role}/${filename}`));
|
|
7928
8019
|
}
|
|
7929
8020
|
}
|
|
7930
|
-
const topLevelFiles = (await fs$
|
|
8021
|
+
const topLevelFiles = (await fs$1$1.readdir(templateDir)).filter((f) => f.endsWith(".md"));
|
|
7931
8022
|
for (const filename of topLevelFiles) {
|
|
7932
|
-
await fs$
|
|
8023
|
+
await fs$1$1.copyFile(path__default.join(templateDir, filename), path__default.join(agentKitDir, filename));
|
|
7933
8024
|
getLogger().info(chalk.gray(` Created agent-kit/${filename}`));
|
|
7934
8025
|
}
|
|
7935
8026
|
const sharedDirs = ["contracts"];
|
|
@@ -7941,15 +8032,15 @@ async function ensureAgentKitDocs(projectRoot, _force, mode) {
|
|
|
7941
8032
|
}
|
|
7942
8033
|
}
|
|
7943
8034
|
async function copyDirRecursive(src, dest) {
|
|
7944
|
-
await fs$
|
|
7945
|
-
const entries = await fs$
|
|
8035
|
+
await fs$1$1.mkdir(dest, { recursive: true });
|
|
8036
|
+
const entries = await fs$1$1.readdir(src, { withFileTypes: true });
|
|
7946
8037
|
for (const entry of entries) {
|
|
7947
8038
|
const srcPath = path__default.join(src, entry.name);
|
|
7948
8039
|
const destPath = path__default.join(dest, entry.name);
|
|
7949
8040
|
if (entry.isDirectory()) {
|
|
7950
8041
|
await copyDirRecursive(srcPath, destPath);
|
|
7951
8042
|
} else if (entry.name.endsWith(".md")) {
|
|
7952
|
-
await fs$
|
|
8043
|
+
await fs$1$1.copyFile(srcPath, destPath);
|
|
7953
8044
|
}
|
|
7954
8045
|
}
|
|
7955
8046
|
}
|
|
@@ -7965,7 +8056,7 @@ async function mergePluginAgentKitGuides(projectRoot, mode) {
|
|
|
7965
8056
|
const roleSourceDir = path__default.join(pluginAgentKitDir, role);
|
|
7966
8057
|
let files;
|
|
7967
8058
|
try {
|
|
7968
|
-
files = (await fs$
|
|
8059
|
+
files = (await fs$1$1.readdir(roleSourceDir)).filter(
|
|
7969
8060
|
(f) => f.endsWith(".md") && f !== "INSTRUCTIONS.md"
|
|
7970
8061
|
);
|
|
7971
8062
|
} catch {
|
|
@@ -7973,13 +8064,13 @@ async function mergePluginAgentKitGuides(projectRoot, mode) {
|
|
|
7973
8064
|
}
|
|
7974
8065
|
if (files.length === 0) continue;
|
|
7975
8066
|
const roleOutputDir = path__default.join(agentKitDir, role);
|
|
7976
|
-
await fs$
|
|
8067
|
+
await fs$1$1.mkdir(roleOutputDir, { recursive: true });
|
|
7977
8068
|
for (const filename of files) {
|
|
7978
8069
|
const sourcePath = path__default.join(roleSourceDir, filename);
|
|
7979
|
-
await fs$
|
|
8070
|
+
await fs$1$1.copyFile(sourcePath, path__default.join(roleOutputDir, filename));
|
|
7980
8071
|
let description = "";
|
|
7981
8072
|
try {
|
|
7982
|
-
const content = await fs$
|
|
8073
|
+
const content = await fs$1$1.readFile(sourcePath, "utf-8");
|
|
7983
8074
|
const lines = content.split("\n");
|
|
7984
8075
|
let pastHeading = false;
|
|
7985
8076
|
for (const line of lines) {
|
|
@@ -8018,7 +8109,7 @@ async function mergePluginAgentKitGuides(projectRoot, mode) {
|
|
|
8018
8109
|
lines.push(`| [${filename}](${filename}) | ${pluginName} | ${description} |`);
|
|
8019
8110
|
}
|
|
8020
8111
|
lines.push("");
|
|
8021
|
-
await fs$
|
|
8112
|
+
await fs$1$1.appendFile(instructionsPath, lines.join("\n"));
|
|
8022
8113
|
}
|
|
8023
8114
|
}
|
|
8024
8115
|
async function generatePluginAgentKit(projectRoot, options, initErrors, viteServer) {
|