@jay-framework/jay-stack-cli 0.25.0 → 0.25.2
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/INSTRUCTIONS.md +1 -0
- 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/error-pages-guide.md +109 -0
- package/agent-kit-template/designer/jay-html-template-syntax.md +1 -1
- package/agent-kit-template/designer/routing.md +7 -0
- package/agent-kit-template/designer/validation-guide.md +27 -3
- package/agent-kit-template/designer/validation-tier-2-guide.md +64 -0
- package/agent-kit-template/developer/INSTRUCTIONS.md +1 -0
- package/agent-kit-template/developer/error-pages-guide.md +86 -0
- package/agent-kit-template/developer/render-results.md +16 -0
- package/agent-kit-template/developer/seo-guide.md +110 -1
- package/agent-kit-template/plugin/validation.md +9 -0
- package/dist/{index-l6QVpvnY.js → index-B5oavmfA.js} +906 -710
- package/dist/index.d.ts +34 -1
- package/dist/index.js +1023 -21
- package/package.json +14 -13
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 = {
|
|
@@ -49,7 +50,8 @@ function loadConfig(projectRoot) {
|
|
|
49
50
|
...DEFAULT_CONFIG.devServer,
|
|
50
51
|
...userConfig.devServer
|
|
51
52
|
},
|
|
52
|
-
site: userConfig.site
|
|
53
|
+
site: userConfig.site,
|
|
54
|
+
validations: userConfig.validations
|
|
53
55
|
};
|
|
54
56
|
} catch (error) {
|
|
55
57
|
getLogger().warn(`Failed to parse .jay YAML config file, using defaults: ${error}`);
|
|
@@ -198,6 +200,13 @@ const generateContractDefinitionFiles$1 = /* @__PURE__ */ Object.freeze(/* @__PU
|
|
|
198
200
|
__proto__: null,
|
|
199
201
|
generateContractDefinitionFiles
|
|
200
202
|
}, Symbol.toStringTag, { value: "Module" }));
|
|
203
|
+
function setWellKnownHeaders(res, filePath) {
|
|
204
|
+
if (filePath.includes(`${path$1.sep}.well-known${path$1.sep}`)) {
|
|
205
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
206
|
+
if (filePath.endsWith(".md")) res.setHeader("Content-Type", "text/markdown; charset=utf-8");
|
|
207
|
+
if (filePath.endsWith(".gz")) res.setHeader("Content-Type", "application/gzip");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
201
210
|
async function startDevServer(options = {}) {
|
|
202
211
|
const projectPath = options.projectPath || process.cwd();
|
|
203
212
|
if (projectPath !== process.cwd()) {
|
|
@@ -213,7 +222,7 @@ async function startDevServer(options = {}) {
|
|
|
213
222
|
const httpServer = http.createServer(app);
|
|
214
223
|
const devServerPort = await getPort({ port: resolvedConfig.devServer.portRange });
|
|
215
224
|
const log = getLogger();
|
|
216
|
-
const { server, viteServer, routes, service } = await mkDevServer({
|
|
225
|
+
const { server, viteServer, routes, service, errorPageHandler } = await mkDevServer({
|
|
217
226
|
pagesRootFolder: path$1.resolve(resolvedConfig.devServer.pagesBase),
|
|
218
227
|
projectRootFolder: process.cwd(),
|
|
219
228
|
publicBaseUrlPath: "/",
|
|
@@ -231,18 +240,20 @@ async function startDevServer(options = {}) {
|
|
|
231
240
|
});
|
|
232
241
|
}
|
|
233
242
|
if (fs$1.existsSync(publicPath)) {
|
|
234
|
-
app.use(express.static(publicPath));
|
|
243
|
+
app.use(express.static(publicPath, { dotfiles: "allow", setHeaders: setWellKnownHeaders }));
|
|
235
244
|
} else {
|
|
236
245
|
log.important(`⚠️ Public folder not found: ${resolvedConfig.devServer.publicFolder}`);
|
|
237
246
|
}
|
|
247
|
+
const pageRouter = express.Router();
|
|
238
248
|
routes.forEach((route) => {
|
|
239
|
-
|
|
249
|
+
pageRouter.get(route.path, route.handler);
|
|
240
250
|
});
|
|
241
251
|
service.attachRouteRegistrar((added) => {
|
|
242
252
|
added.forEach((route) => {
|
|
243
|
-
|
|
253
|
+
pageRouter.get(route.path, route.handler);
|
|
244
254
|
});
|
|
245
255
|
});
|
|
256
|
+
app.use(pageRouter);
|
|
246
257
|
void (async () => {
|
|
247
258
|
await generatePageDefinitionFiles(
|
|
248
259
|
routes.map((route) => route.fsRoute.jayHtmlPath),
|
|
@@ -297,6 +308,7 @@ async function startDevServer(options = {}) {
|
|
|
297
308
|
}, options.timeout * 1e3);
|
|
298
309
|
}
|
|
299
310
|
}
|
|
311
|
+
app.use(errorPageHandler(404));
|
|
300
312
|
process.on("SIGTERM", shutdown);
|
|
301
313
|
process.on("SIGINT", shutdown);
|
|
302
314
|
}
|
|
@@ -3003,6 +3015,61 @@ async function validateAiditorSettings(context, result) {
|
|
|
3003
3015
|
});
|
|
3004
3016
|
}
|
|
3005
3017
|
}
|
|
3018
|
+
async function compileValidateRouteJayHtml(absoluteJayHtmlPath, pluginRoot) {
|
|
3019
|
+
const content = fs$1.readFileSync(absoluteJayHtmlPath, "utf-8");
|
|
3020
|
+
const dirname = path$1.dirname(absoluteJayHtmlPath);
|
|
3021
|
+
const filename = path$1.basename(absoluteJayHtmlPath, JAY_EXTENSION$1);
|
|
3022
|
+
const errors = [];
|
|
3023
|
+
try {
|
|
3024
|
+
const parsedFile = await parseJayFile(
|
|
3025
|
+
content,
|
|
3026
|
+
filename,
|
|
3027
|
+
dirname,
|
|
3028
|
+
{},
|
|
3029
|
+
JAY_IMPORT_RESOLVER,
|
|
3030
|
+
pluginRoot
|
|
3031
|
+
);
|
|
3032
|
+
for (const validation of parsedFile.validations) {
|
|
3033
|
+
errors.push(validation);
|
|
3034
|
+
}
|
|
3035
|
+
if (!parsedFile.val || parsedFile.validations.length > 0) {
|
|
3036
|
+
return errors;
|
|
3037
|
+
}
|
|
3038
|
+
const generatedFile = generateElementFile(parsedFile.val, RuntimeMode$1.MainTrusted);
|
|
3039
|
+
for (const validation of generatedFile.validations) {
|
|
3040
|
+
errors.push(validation);
|
|
3041
|
+
}
|
|
3042
|
+
const serverElementFile = generateServerElementFile(parsedFile.val);
|
|
3043
|
+
for (const validation of serverElementFile.validations) {
|
|
3044
|
+
errors.push(`[SSR] ${validation}`);
|
|
3045
|
+
}
|
|
3046
|
+
} catch (error) {
|
|
3047
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3048
|
+
errors.push(message);
|
|
3049
|
+
}
|
|
3050
|
+
return errors;
|
|
3051
|
+
}
|
|
3052
|
+
async function validateRouteJayHtmlTemplates(context, result) {
|
|
3053
|
+
const routes = context.manifest.routes;
|
|
3054
|
+
if (!routes || !Array.isArray(routes)) return;
|
|
3055
|
+
for (const route of routes) {
|
|
3056
|
+
if (!route.jayHtml) continue;
|
|
3057
|
+
const absolutePath = path$1.resolve(context.pluginPath, route.jayHtml);
|
|
3058
|
+
if (!fs$1.existsSync(absolutePath)) {
|
|
3059
|
+
continue;
|
|
3060
|
+
}
|
|
3061
|
+
const compileErrors = await compileValidateRouteJayHtml(absolutePath, context.pluginPath);
|
|
3062
|
+
const relPath = path$1.relative(context.pluginPath, absolutePath);
|
|
3063
|
+
for (const message of compileErrors) {
|
|
3064
|
+
result.errors.push({
|
|
3065
|
+
type: "jay-html-compile",
|
|
3066
|
+
message: `Route "${route.path}" jayHtml failed to compile: ${message}`,
|
|
3067
|
+
location: relPath,
|
|
3068
|
+
suggestion: "Fix the template syntax or run `jay-stack validate -p <dir>` on the source page during development. See agent-kit/designer/jay-html-template-syntax.md"
|
|
3069
|
+
});
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3006
3073
|
async function validatePlugin(options = {}) {
|
|
3007
3074
|
const pluginPath = options.pluginPath || process.cwd();
|
|
3008
3075
|
if (options.local) {
|
|
@@ -3072,6 +3139,7 @@ async function validatePluginPackage(pluginPath, options) {
|
|
|
3072
3139
|
await validateAddMenuCatalog(context, result);
|
|
3073
3140
|
await validateAiditorSettings(context, result);
|
|
3074
3141
|
validateNoCompilerLeak(context, result);
|
|
3142
|
+
await validateRouteJayHtmlTemplates(context, result);
|
|
3075
3143
|
result.valid = result.errors.length === 0;
|
|
3076
3144
|
return result;
|
|
3077
3145
|
}
|
|
@@ -3926,6 +3994,20 @@ async function validatePackageJson(context, result) {
|
|
|
3926
3994
|
suggestion: 'Add "./plugin.yaml": "./plugin.yaml" to exports field'
|
|
3927
3995
|
});
|
|
3928
3996
|
}
|
|
3997
|
+
if (context.manifest.commands) {
|
|
3998
|
+
for (const cmd of context.manifest.commands) {
|
|
3999
|
+
if (!cmd.command) continue;
|
|
4000
|
+
const exportKey = `./${cmd.command}`;
|
|
4001
|
+
if (!packageJson.exports || !packageJson.exports[exportKey]) {
|
|
4002
|
+
result.errors.push({
|
|
4003
|
+
type: "export-mismatch",
|
|
4004
|
+
message: `command metadata file "${cmd.command}" not exported in package.json (required for plugin resolution)`,
|
|
4005
|
+
location: packageJsonPath,
|
|
4006
|
+
suggestion: `Add "${exportKey}": "${exportKey}" to exports field`
|
|
4007
|
+
});
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
}
|
|
3929
4011
|
const agentKitDir = path$1.join(context.pluginPath, "agent-kit");
|
|
3930
4012
|
if (fs$1.existsSync(agentKitDir) && fs$1.statSync(agentKitDir).isDirectory()) {
|
|
3931
4013
|
const filesArray = packageJson.files;
|
|
@@ -4745,6 +4827,627 @@ function scopeInnerBody(block) {
|
|
|
4745
4827
|
function normalizeCssBody(body) {
|
|
4746
4828
|
return body.replace(/\s+/g, " ").replace(/\s*([{}:;,])\s*/g, "$1").replace(/;}/g, "}").trim();
|
|
4747
4829
|
}
|
|
4830
|
+
const SUPPRESS_KEY = "allow-broken-links";
|
|
4831
|
+
const SUPPRESS_HINT = 'suppress with jay-stack:\n allow-broken-links: true\nin a <script type="application/jay-validations"> block';
|
|
4832
|
+
function normalizePath(p) {
|
|
4833
|
+
let s2 = p.split("#")[0].split("?")[0];
|
|
4834
|
+
s2 = s2.replace(/\/{2,}/g, "/");
|
|
4835
|
+
if (s2.length > 1) s2 = s2.replace(/\/+$/, "");
|
|
4836
|
+
return s2 === "" ? "/" : s2;
|
|
4837
|
+
}
|
|
4838
|
+
function escapeRegExp(s2) {
|
|
4839
|
+
return s2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4840
|
+
}
|
|
4841
|
+
function routeToConcreteUrl(route) {
|
|
4842
|
+
const parts = [];
|
|
4843
|
+
for (const seg of route.segments) {
|
|
4844
|
+
if (typeof seg === "string") {
|
|
4845
|
+
parts.push(seg);
|
|
4846
|
+
} else {
|
|
4847
|
+
const v = route.inferredParams?.[seg.name];
|
|
4848
|
+
if (v === void 0) return null;
|
|
4849
|
+
parts.push(v);
|
|
4850
|
+
}
|
|
4851
|
+
}
|
|
4852
|
+
return normalizePath("/" + parts.join("/"));
|
|
4853
|
+
}
|
|
4854
|
+
function routeToRegex(route) {
|
|
4855
|
+
let pattern = "";
|
|
4856
|
+
for (const seg of route.segments) {
|
|
4857
|
+
if (typeof seg === "string") {
|
|
4858
|
+
pattern += "/" + escapeRegExp(seg);
|
|
4859
|
+
} else if (seg.type === JayRouteParamType.optional) {
|
|
4860
|
+
pattern += "(?:/[^/]+)?";
|
|
4861
|
+
} else if (seg.type === JayRouteParamType.catchAll) {
|
|
4862
|
+
pattern += "(?:/[^/]+)+";
|
|
4863
|
+
} else {
|
|
4864
|
+
pattern += "/[^/]+";
|
|
4865
|
+
}
|
|
4866
|
+
}
|
|
4867
|
+
if (pattern === "") pattern = "/";
|
|
4868
|
+
return new RegExp("^" + pattern + "$");
|
|
4869
|
+
}
|
|
4870
|
+
async function buildRouteOracle(pagesBase) {
|
|
4871
|
+
let routes = [];
|
|
4872
|
+
try {
|
|
4873
|
+
({ routes } = await scanRoutes(pagesBase, {
|
|
4874
|
+
jayHtmlFilename: "page.jay-html",
|
|
4875
|
+
compFilename: "page.ts"
|
|
4876
|
+
}));
|
|
4877
|
+
} catch {
|
|
4878
|
+
routes = [];
|
|
4879
|
+
}
|
|
4880
|
+
const staticUrls = /* @__PURE__ */ new Set();
|
|
4881
|
+
const dynamicMatchers = [];
|
|
4882
|
+
const selfUrlByPath = /* @__PURE__ */ new Map();
|
|
4883
|
+
for (const route of routes) {
|
|
4884
|
+
const concrete = routeToConcreteUrl(route);
|
|
4885
|
+
if (concrete !== null) {
|
|
4886
|
+
staticUrls.add(concrete);
|
|
4887
|
+
if (route.jayHtmlPath) selfUrlByPath.set(path$1.resolve(route.jayHtmlPath), concrete);
|
|
4888
|
+
}
|
|
4889
|
+
if (route.segments.some((s2) => typeof s2 !== "string")) {
|
|
4890
|
+
dynamicMatchers.push(routeToRegex(route));
|
|
4891
|
+
}
|
|
4892
|
+
}
|
|
4893
|
+
return { staticUrls, dynamicMatchers, selfUrlByPath };
|
|
4894
|
+
}
|
|
4895
|
+
async function collectPublicAssets(publicFolder) {
|
|
4896
|
+
const assets = /* @__PURE__ */ new Set();
|
|
4897
|
+
async function walk(dir, prefix) {
|
|
4898
|
+
let entries;
|
|
4899
|
+
try {
|
|
4900
|
+
entries = await promises.readdir(dir, { withFileTypes: true });
|
|
4901
|
+
} catch {
|
|
4902
|
+
return;
|
|
4903
|
+
}
|
|
4904
|
+
for (const entry of entries) {
|
|
4905
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
4906
|
+
if (entry.isDirectory()) {
|
|
4907
|
+
await walk(path$1.join(dir, entry.name), rel);
|
|
4908
|
+
} else {
|
|
4909
|
+
assets.add("/" + rel);
|
|
4910
|
+
}
|
|
4911
|
+
}
|
|
4912
|
+
}
|
|
4913
|
+
await walk(publicFolder, "");
|
|
4914
|
+
return assets;
|
|
4915
|
+
}
|
|
4916
|
+
function originOf(baseUrl) {
|
|
4917
|
+
try {
|
|
4918
|
+
return new URL(baseUrl).origin;
|
|
4919
|
+
} catch {
|
|
4920
|
+
return void 0;
|
|
4921
|
+
}
|
|
4922
|
+
}
|
|
4923
|
+
function classifyHref(raw, baseUrl) {
|
|
4924
|
+
if (raw.includes("{")) return { kind: "skip" };
|
|
4925
|
+
const t = raw.trim();
|
|
4926
|
+
if (t === "" || t === "#") return { kind: "degenerate" };
|
|
4927
|
+
if (t.startsWith("#")) return { kind: "skip" };
|
|
4928
|
+
if (/^(mailto:|tel:|javascript:|data:|sms:|ftp:)/i.test(t)) return { kind: "skip" };
|
|
4929
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(t) || t.startsWith("//")) {
|
|
4930
|
+
const sameOrigin = baseUrl ? originOf(baseUrl) : void 0;
|
|
4931
|
+
if (sameOrigin) {
|
|
4932
|
+
try {
|
|
4933
|
+
const u2 = new URL(t.startsWith("//") ? "https:" + t : t);
|
|
4934
|
+
if (u2.origin === sameOrigin)
|
|
4935
|
+
return { kind: "check", targetPath: normalizePath(u2.pathname) };
|
|
4936
|
+
} catch {
|
|
4937
|
+
}
|
|
4938
|
+
}
|
|
4939
|
+
return { kind: "skip" };
|
|
4940
|
+
}
|
|
4941
|
+
if (t.startsWith("/")) return { kind: "check", targetPath: normalizePath(t) };
|
|
4942
|
+
return { kind: "skip" };
|
|
4943
|
+
}
|
|
4944
|
+
function checkInternalLinks(options) {
|
|
4945
|
+
const { parsedFiles, oracle, assetUrls, baseUrl, projectRoot } = options;
|
|
4946
|
+
const findings = [];
|
|
4947
|
+
for (const { relativePath, parsed } of parsedFiles) {
|
|
4948
|
+
if (parsed.validationOverrides?.["jay-stack"]?.[SUPPRESS_KEY] === true) continue;
|
|
4949
|
+
const ownUrl = oracle.selfUrlByPath.get(path$1.resolve(projectRoot ?? ".", relativePath));
|
|
4950
|
+
for (const anchor of parsed.body.querySelectorAll("a[href]")) {
|
|
4951
|
+
const raw = anchor.getAttribute("href");
|
|
4952
|
+
if (raw === void 0 || raw === null) continue;
|
|
4953
|
+
const verdict = classifyHref(raw, baseUrl);
|
|
4954
|
+
if (verdict.kind === "skip") continue;
|
|
4955
|
+
if (verdict.kind === "degenerate") {
|
|
4956
|
+
findings.push({
|
|
4957
|
+
file: relativePath,
|
|
4958
|
+
message: `Anchor has a placeholder href "${raw}" — it links nowhere and ships as a dead link.`,
|
|
4959
|
+
suggestion: `Point the link at a real page, use <button> for a JS-driven action, or ${SUPPRESS_HINT}.`
|
|
4960
|
+
});
|
|
4961
|
+
continue;
|
|
4962
|
+
}
|
|
4963
|
+
const target = verdict.targetPath;
|
|
4964
|
+
if (ownUrl !== void 0 && target === ownUrl) {
|
|
4965
|
+
findings.push({
|
|
4966
|
+
file: relativePath,
|
|
4967
|
+
message: `Link points to the page's own route "${target}" — a self-link goes nowhere.`,
|
|
4968
|
+
suggestion: `Remove the link, use "#" within-page navigation, or point it at a different page.`
|
|
4969
|
+
});
|
|
4970
|
+
continue;
|
|
4971
|
+
}
|
|
4972
|
+
if (oracle.staticUrls.has(target)) continue;
|
|
4973
|
+
if (assetUrls.has(target)) continue;
|
|
4974
|
+
if (oracle.dynamicMatchers.some((re) => re.test(target))) continue;
|
|
4975
|
+
findings.push({
|
|
4976
|
+
file: relativePath,
|
|
4977
|
+
message: `Broken internal link "${raw}" — no page or public asset produces the URL "${target}".`,
|
|
4978
|
+
suggestion: `Create a placeholder page/route for "${target}", or remove the link. Otherwise ${SUPPRESS_HINT}.`
|
|
4979
|
+
});
|
|
4980
|
+
}
|
|
4981
|
+
}
|
|
4982
|
+
return findings;
|
|
4983
|
+
}
|
|
4984
|
+
const META_SUPPRESS_HINT = 'suppress with jay-stack:\n allow-meta-issues: true\nin the page <script type="application/jay-validations"> block';
|
|
4985
|
+
const LINK_SUPPRESS_HINT = 'suppress with jay-stack:\n allow-broken-links: true\nin the page <script type="application/jay-validations"> block';
|
|
4986
|
+
const UNRESOLVED_BINDING = /\{[A-Za-z_$][\w$]*(?:\.[\w$]+)*\}/;
|
|
4987
|
+
const TITLE_MAX = 60;
|
|
4988
|
+
const DESCRIPTION_MAX = 160;
|
|
4989
|
+
function buildUrlFromManifest(pattern, params) {
|
|
4990
|
+
return pattern.replace(/\[\[(\w+)\]\]/g, (_, name) => params[name] || "").replace(/\[\.\.\.(\w+)\]/g, (_, name) => params[name] || "").replace(/\[(\w+)\]/g, (_, name) => params[name] || "").replace(/\/\/+/g, "/").replace(/\/$/, "") || "/";
|
|
4991
|
+
}
|
|
4992
|
+
function routeInstanceUrl(route, instance) {
|
|
4993
|
+
return normalizePath(buildUrlFromManifest(route.pattern, instance.params));
|
|
4994
|
+
}
|
|
4995
|
+
async function discoverBuildBackendDir(projectRoot, explicit) {
|
|
4996
|
+
if (explicit) {
|
|
4997
|
+
const abs = path$1.resolve(projectRoot, explicit);
|
|
4998
|
+
if (await fileExists(path$1.join(abs, "route-manifest.json"))) return abs;
|
|
4999
|
+
const nested = path$1.join(abs, "backend");
|
|
5000
|
+
if (await fileExists(path$1.join(nested, "route-manifest.json"))) return nested;
|
|
5001
|
+
const fromRoot = await highestVersionBackend(abs);
|
|
5002
|
+
if (fromRoot) return fromRoot;
|
|
5003
|
+
return null;
|
|
5004
|
+
}
|
|
5005
|
+
return highestVersionBackend(path$1.join(projectRoot, "build"));
|
|
5006
|
+
}
|
|
5007
|
+
async function highestVersionBackend(buildRoot) {
|
|
5008
|
+
let entries;
|
|
5009
|
+
try {
|
|
5010
|
+
entries = (await promises.readdir(buildRoot, { withFileTypes: true })).filter((e2) => e2.isDirectory() && /^v/.test(e2.name)).map((e2) => e2.name);
|
|
5011
|
+
} catch {
|
|
5012
|
+
return null;
|
|
5013
|
+
}
|
|
5014
|
+
if (entries.length === 0) return null;
|
|
5015
|
+
entries.sort(compareVersionDesc);
|
|
5016
|
+
for (const name of entries) {
|
|
5017
|
+
const backend = path$1.join(buildRoot, name, "backend");
|
|
5018
|
+
if (await fileExists(path$1.join(backend, "route-manifest.json"))) return backend;
|
|
5019
|
+
}
|
|
5020
|
+
return null;
|
|
5021
|
+
}
|
|
5022
|
+
function compareVersionDesc(a, b) {
|
|
5023
|
+
const pa = a.replace(/^v/, "").split(".").map(Number);
|
|
5024
|
+
const pb = b.replace(/^v/, "").split(".").map(Number);
|
|
5025
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
5026
|
+
const da = pa[i] || 0;
|
|
5027
|
+
const db = pb[i] || 0;
|
|
5028
|
+
if (da !== db) return db - da;
|
|
5029
|
+
}
|
|
5030
|
+
return 0;
|
|
5031
|
+
}
|
|
5032
|
+
async function fileExists(p) {
|
|
5033
|
+
try {
|
|
5034
|
+
await promises.access(p);
|
|
5035
|
+
return true;
|
|
5036
|
+
} catch {
|
|
5037
|
+
return false;
|
|
5038
|
+
}
|
|
5039
|
+
}
|
|
5040
|
+
async function loadBuildManifest(backendDir) {
|
|
5041
|
+
let manifest;
|
|
5042
|
+
try {
|
|
5043
|
+
manifest = JSON.parse(
|
|
5044
|
+
await promises.readFile(path$1.join(backendDir, "route-manifest.json"), "utf8")
|
|
5045
|
+
);
|
|
5046
|
+
} catch {
|
|
5047
|
+
return null;
|
|
5048
|
+
}
|
|
5049
|
+
let metadata = null;
|
|
5050
|
+
try {
|
|
5051
|
+
metadata = JSON.parse(
|
|
5052
|
+
await promises.readFile(path$1.join(backendDir, "build-metadata.json"), "utf8")
|
|
5053
|
+
);
|
|
5054
|
+
} catch {
|
|
5055
|
+
metadata = null;
|
|
5056
|
+
}
|
|
5057
|
+
return { manifest, metadata };
|
|
5058
|
+
}
|
|
5059
|
+
function buildBuildOracle(manifest, backendDir) {
|
|
5060
|
+
const urls = /* @__PURE__ */ new Set();
|
|
5061
|
+
const instances = [];
|
|
5062
|
+
for (const route of manifest.routes) {
|
|
5063
|
+
const hasDynamic = route.segments.some((s2) => s2.type !== "static");
|
|
5064
|
+
if (route.instances.length === 0) {
|
|
5065
|
+
if (!hasDynamic) urls.add(normalizePath(route.pattern));
|
|
5066
|
+
continue;
|
|
5067
|
+
}
|
|
5068
|
+
for (const instance of route.instances) {
|
|
5069
|
+
const url = routeInstanceUrl(route, instance);
|
|
5070
|
+
urls.add(url);
|
|
5071
|
+
instances.push({
|
|
5072
|
+
url,
|
|
5073
|
+
cacheAbsPath: path$1.resolve(backendDir, instance.cachePath),
|
|
5074
|
+
pattern: route.pattern,
|
|
5075
|
+
headMeta: route.headMeta
|
|
5076
|
+
});
|
|
5077
|
+
}
|
|
5078
|
+
}
|
|
5079
|
+
return { urls, instances };
|
|
5080
|
+
}
|
|
5081
|
+
async function loadSlowViewState(cacheAbsPath) {
|
|
5082
|
+
try {
|
|
5083
|
+
const cache = JSON.parse(await promises.readFile(cacheAbsPath, "utf8"));
|
|
5084
|
+
return cache.slowViewState ?? {};
|
|
5085
|
+
} catch {
|
|
5086
|
+
return {};
|
|
5087
|
+
}
|
|
5088
|
+
}
|
|
5089
|
+
async function loadCarryForward(cacheAbsPath) {
|
|
5090
|
+
try {
|
|
5091
|
+
const cache = JSON.parse(await promises.readFile(cacheAbsPath, "utf8"));
|
|
5092
|
+
return cache.carryForward ?? {};
|
|
5093
|
+
} catch {
|
|
5094
|
+
return {};
|
|
5095
|
+
}
|
|
5096
|
+
}
|
|
5097
|
+
function jsonLdBodyError(body) {
|
|
5098
|
+
const text = body?.trim();
|
|
5099
|
+
if (!text) return "Empty JSON-LD block — add structured data or remove the tag.";
|
|
5100
|
+
let parsed;
|
|
5101
|
+
try {
|
|
5102
|
+
parsed = JSON.parse(text);
|
|
5103
|
+
} catch (e2) {
|
|
5104
|
+
return `Invalid JSON: ${e2.message}`;
|
|
5105
|
+
}
|
|
5106
|
+
const objs = Array.isArray(parsed) ? parsed : [parsed];
|
|
5107
|
+
for (const obj of objs) {
|
|
5108
|
+
if (!obj || typeof obj !== "object") {
|
|
5109
|
+
return "JSON-LD must be an object (or array of objects) with @context and @type.";
|
|
5110
|
+
}
|
|
5111
|
+
if (!("@context" in obj) || !("@type" in obj)) {
|
|
5112
|
+
return "JSON-LD is missing @context or @type.";
|
|
5113
|
+
}
|
|
5114
|
+
}
|
|
5115
|
+
return null;
|
|
5116
|
+
}
|
|
5117
|
+
function isLdJsonHeadTag(tag) {
|
|
5118
|
+
return tag?.tag?.toLowerCase?.() === "script" && tag?.attrs?.type === "application/ld+json";
|
|
5119
|
+
}
|
|
5120
|
+
function checkBuildJsonLd(manifest) {
|
|
5121
|
+
const findings = [];
|
|
5122
|
+
for (const tag of manifest.siteHeadTags ?? []) {
|
|
5123
|
+
if (!isLdJsonHeadTag(tag)) continue;
|
|
5124
|
+
const err = jsonLdBodyError(tag.children);
|
|
5125
|
+
if (err) {
|
|
5126
|
+
findings.push({
|
|
5127
|
+
file: ".jay (site.*)",
|
|
5128
|
+
message: `Site JSON-LD: ${err}`,
|
|
5129
|
+
severity: "error",
|
|
5130
|
+
source: "build-output"
|
|
5131
|
+
});
|
|
5132
|
+
}
|
|
5133
|
+
}
|
|
5134
|
+
for (const route of manifest.routes) {
|
|
5135
|
+
for (const script of route.scripts ?? []) {
|
|
5136
|
+
if (script.attributes?.type !== "application/ld+json") continue;
|
|
5137
|
+
const err = jsonLdBodyError(script.inline);
|
|
5138
|
+
if (err) {
|
|
5139
|
+
findings.push({
|
|
5140
|
+
file: route.pattern,
|
|
5141
|
+
message: `Template JSON-LD: ${err}`,
|
|
5142
|
+
severity: "error",
|
|
5143
|
+
source: "build-output"
|
|
5144
|
+
});
|
|
5145
|
+
}
|
|
5146
|
+
}
|
|
5147
|
+
}
|
|
5148
|
+
return findings;
|
|
5149
|
+
}
|
|
5150
|
+
function checkInstanceJsonLd(args) {
|
|
5151
|
+
const { instanceUrl, carryForward } = args;
|
|
5152
|
+
const findings = [];
|
|
5153
|
+
const slowHeadTags = carryForward?.__slowHeadTags;
|
|
5154
|
+
if (!Array.isArray(slowHeadTags)) return findings;
|
|
5155
|
+
for (const source of slowHeadTags) {
|
|
5156
|
+
if (!Array.isArray(source)) continue;
|
|
5157
|
+
for (const tag of source) {
|
|
5158
|
+
if (!isLdJsonHeadTag(tag)) continue;
|
|
5159
|
+
const err = jsonLdBodyError(tag?.children);
|
|
5160
|
+
if (err) {
|
|
5161
|
+
findings.push({
|
|
5162
|
+
file: instanceUrl,
|
|
5163
|
+
message: `Component JSON-LD: ${err}`,
|
|
5164
|
+
severity: "error",
|
|
5165
|
+
source: "build-output"
|
|
5166
|
+
});
|
|
5167
|
+
}
|
|
5168
|
+
}
|
|
5169
|
+
}
|
|
5170
|
+
return findings;
|
|
5171
|
+
}
|
|
5172
|
+
function checkTemplateLinksAgainstBuild(args) {
|
|
5173
|
+
const { parsedFiles, buildUrls, assetUrls, dynamicMatchers, baseUrl } = args;
|
|
5174
|
+
const findings = [];
|
|
5175
|
+
for (const { relativePath, parsed } of parsedFiles) {
|
|
5176
|
+
if (parsed.validationOverrides?.["jay-stack"]?.["allow-broken-links"] === true) continue;
|
|
5177
|
+
for (const anchor of parsed.body.querySelectorAll("a[href]")) {
|
|
5178
|
+
const raw = anchor.getAttribute("href");
|
|
5179
|
+
if (raw === void 0 || raw === null) continue;
|
|
5180
|
+
const verdict = classifyHref(raw, baseUrl);
|
|
5181
|
+
if (verdict.kind !== "check") continue;
|
|
5182
|
+
const target = verdict.targetPath;
|
|
5183
|
+
if (buildUrls.has(target) || assetUrls.has(target)) continue;
|
|
5184
|
+
if (!dynamicMatchers.some((re) => re.test(target))) continue;
|
|
5185
|
+
findings.push({
|
|
5186
|
+
file: relativePath,
|
|
5187
|
+
message: `Broken link "${raw}" — matches a dynamic route but "${target}" is not among the URLs this build produces.`,
|
|
5188
|
+
suggestion: `Create the slug/page, fix the path, or remove the link. Otherwise ${LINK_SUPPRESS_HINT}.`,
|
|
5189
|
+
severity: "error",
|
|
5190
|
+
source: "build-output"
|
|
5191
|
+
});
|
|
5192
|
+
}
|
|
5193
|
+
}
|
|
5194
|
+
return findings;
|
|
5195
|
+
}
|
|
5196
|
+
function resolveContentHref(raw, instanceUrl, baseUrl) {
|
|
5197
|
+
const t = raw.trim();
|
|
5198
|
+
if (t === "" || t === "#" || t.startsWith("#")) return { kind: "skip" };
|
|
5199
|
+
if (t.includes("{")) return { kind: "skip" };
|
|
5200
|
+
if (t.startsWith("/") || t.startsWith("//") || /^[a-z][a-z0-9+.-]*:/i.test(t)) {
|
|
5201
|
+
const v = classifyHref(t, baseUrl);
|
|
5202
|
+
return v.kind === "check" ? v : { kind: "skip" };
|
|
5203
|
+
}
|
|
5204
|
+
try {
|
|
5205
|
+
const u2 = new URL(t, "https://_local_" + (instanceUrl === "/" ? "/" : instanceUrl));
|
|
5206
|
+
return { kind: "check", targetPath: normalizePath(u2.pathname) };
|
|
5207
|
+
} catch {
|
|
5208
|
+
return { kind: "skip" };
|
|
5209
|
+
}
|
|
5210
|
+
}
|
|
5211
|
+
function collectContentHrefs(viewState) {
|
|
5212
|
+
const hrefs = [];
|
|
5213
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5214
|
+
const walk = (node) => {
|
|
5215
|
+
if (node == null) return;
|
|
5216
|
+
if (typeof node === "string") {
|
|
5217
|
+
if (node.includes("<a ")) {
|
|
5218
|
+
const root = parse(node);
|
|
5219
|
+
for (const a of root.querySelectorAll("a[href]")) {
|
|
5220
|
+
const h = a.getAttribute("href");
|
|
5221
|
+
if (h) hrefs.push(h);
|
|
5222
|
+
}
|
|
5223
|
+
}
|
|
5224
|
+
return;
|
|
5225
|
+
}
|
|
5226
|
+
if (typeof node !== "object") return;
|
|
5227
|
+
if (seen.has(node)) return;
|
|
5228
|
+
seen.add(node);
|
|
5229
|
+
if (Array.isArray(node)) {
|
|
5230
|
+
for (const item of node) walk(item);
|
|
5231
|
+
} else {
|
|
5232
|
+
for (const v of Object.values(node)) walk(v);
|
|
5233
|
+
}
|
|
5234
|
+
};
|
|
5235
|
+
walk(viewState);
|
|
5236
|
+
return hrefs;
|
|
5237
|
+
}
|
|
5238
|
+
function checkContentLinks(args) {
|
|
5239
|
+
const { instanceUrl, slowViewState, buildUrls, assetUrls, baseUrl } = args;
|
|
5240
|
+
const findings = [];
|
|
5241
|
+
const reported = /* @__PURE__ */ new Set();
|
|
5242
|
+
for (const raw of collectContentHrefs(slowViewState)) {
|
|
5243
|
+
const verdict = resolveContentHref(raw, instanceUrl, baseUrl);
|
|
5244
|
+
if (verdict.kind !== "check") continue;
|
|
5245
|
+
const target = verdict.targetPath;
|
|
5246
|
+
if (buildUrls.has(target) || assetUrls.has(target)) continue;
|
|
5247
|
+
if (reported.has(target)) continue;
|
|
5248
|
+
reported.add(target);
|
|
5249
|
+
findings.push({
|
|
5250
|
+
file: instanceUrl,
|
|
5251
|
+
message: `Broken link "${raw}" in rendered content of ${instanceUrl} — resolves to "${target}", which this build does not produce.`,
|
|
5252
|
+
// Content links come from authored markdown/CMS data that often doubles as repo docs (where a
|
|
5253
|
+
// repo-relative "../pkg/foo.ts" is correct), so this is a warning, not a build-failing error —
|
|
5254
|
+
// unlike hand-authored template links (Check 1), which are site-only and stay errors.
|
|
5255
|
+
suggestion: `Fix the link in the source content, point it at an absolute URL, or ${LINK_SUPPRESS_HINT}.`,
|
|
5256
|
+
severity: "warning",
|
|
5257
|
+
source: "build-output"
|
|
5258
|
+
});
|
|
5259
|
+
}
|
|
5260
|
+
return findings;
|
|
5261
|
+
}
|
|
5262
|
+
function checkInstanceMeta(args) {
|
|
5263
|
+
const { instanceUrl, headMeta, slowViewState } = args;
|
|
5264
|
+
if (!headMeta) return [];
|
|
5265
|
+
const findings = [];
|
|
5266
|
+
const tags = headMetaToHeadTags(headMeta, slowViewState);
|
|
5267
|
+
const addLeftover = (what, value) => findings.push({
|
|
5268
|
+
file: instanceUrl,
|
|
5269
|
+
message: `<${what}> for ${instanceUrl} resolves to "${value}" — an unresolved binding was not produced by slow render.`,
|
|
5270
|
+
suggestion: `Ensure the field is produced by the slow phase, or make the ${what} static.`,
|
|
5271
|
+
severity: "error",
|
|
5272
|
+
source: "build-output"
|
|
5273
|
+
});
|
|
5274
|
+
const addWarn = (message) => findings.push({
|
|
5275
|
+
file: instanceUrl,
|
|
5276
|
+
message,
|
|
5277
|
+
suggestion: `Fix the content or ${META_SUPPRESS_HINT}.`,
|
|
5278
|
+
severity: "warning",
|
|
5279
|
+
source: "build-output"
|
|
5280
|
+
});
|
|
5281
|
+
for (const tag of tags) {
|
|
5282
|
+
if (tag.tag === "title") {
|
|
5283
|
+
const value = tag.children ?? "";
|
|
5284
|
+
if (UNRESOLVED_BINDING.test(value)) addLeftover("title", value);
|
|
5285
|
+
else if (value.trim() === "") addWarn(`<title> for ${instanceUrl} is empty.`);
|
|
5286
|
+
else if (value.length > TITLE_MAX)
|
|
5287
|
+
addWarn(
|
|
5288
|
+
`<title> for ${instanceUrl} is ${value.length} chars (recommend ≤ ${TITLE_MAX}).`
|
|
5289
|
+
);
|
|
5290
|
+
} else if (tag.tag === "meta" && tag.attrs?.name === "description") {
|
|
5291
|
+
const value = tag.attrs.content ?? "";
|
|
5292
|
+
if (UNRESOLVED_BINDING.test(value)) addLeftover('meta name="description"', value);
|
|
5293
|
+
else if (value.trim() === "")
|
|
5294
|
+
addWarn(`<meta name="description"> for ${instanceUrl} is empty.`);
|
|
5295
|
+
else if (value.length > DESCRIPTION_MAX)
|
|
5296
|
+
addWarn(
|
|
5297
|
+
`<meta name="description"> for ${instanceUrl} is ${value.length} chars (recommend ≤ ${DESCRIPTION_MAX}).`
|
|
5298
|
+
);
|
|
5299
|
+
}
|
|
5300
|
+
}
|
|
5301
|
+
return findings;
|
|
5302
|
+
}
|
|
5303
|
+
function isBuildStale(metadata, newestSourceMtimeMs) {
|
|
5304
|
+
if (!metadata?.buildTimestamp) return false;
|
|
5305
|
+
const built = Date.parse(metadata.buildTimestamp);
|
|
5306
|
+
if (Number.isNaN(built)) return false;
|
|
5307
|
+
return newestSourceMtimeMs > built;
|
|
5308
|
+
}
|
|
5309
|
+
async function newestSourceMtime(dirs) {
|
|
5310
|
+
let newest = 0;
|
|
5311
|
+
const walk = async (dir) => {
|
|
5312
|
+
let entries;
|
|
5313
|
+
try {
|
|
5314
|
+
entries = await promises.readdir(dir, { withFileTypes: true });
|
|
5315
|
+
} catch {
|
|
5316
|
+
return;
|
|
5317
|
+
}
|
|
5318
|
+
for (const entry of entries) {
|
|
5319
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
5320
|
+
const full = path$1.join(dir, entry.name);
|
|
5321
|
+
if (entry.isDirectory()) {
|
|
5322
|
+
await walk(full);
|
|
5323
|
+
} else {
|
|
5324
|
+
try {
|
|
5325
|
+
const st = await promises.stat(full);
|
|
5326
|
+
if (st.mtimeMs > newest) newest = st.mtimeMs;
|
|
5327
|
+
} catch {
|
|
5328
|
+
}
|
|
5329
|
+
}
|
|
5330
|
+
}
|
|
5331
|
+
};
|
|
5332
|
+
for (const dir of dirs) await walk(dir);
|
|
5333
|
+
return newest;
|
|
5334
|
+
}
|
|
5335
|
+
const URN_PATTERN = /^urn:air:[^:\s]+:[^:\s]+:[^:\s]+$/;
|
|
5336
|
+
const MEDIA_TYPE_PATTERN = /^[a-z]+\/[A-Za-z0-9.+-]+$/;
|
|
5337
|
+
const ARD_SUGGEST = "See developer/seo-guide.md (ARD v0.91) for the schema and a worked example.";
|
|
5338
|
+
function validateArdDocument(doc) {
|
|
5339
|
+
const findings = [];
|
|
5340
|
+
if (!doc || typeof doc !== "object") {
|
|
5341
|
+
return [
|
|
5342
|
+
{
|
|
5343
|
+
message: "ard.json must be a JSON object with an `entries` array.",
|
|
5344
|
+
severity: "error",
|
|
5345
|
+
suggestion: ARD_SUGGEST
|
|
5346
|
+
}
|
|
5347
|
+
];
|
|
5348
|
+
}
|
|
5349
|
+
const entries = doc.entries;
|
|
5350
|
+
if (!Array.isArray(entries)) {
|
|
5351
|
+
return [
|
|
5352
|
+
{
|
|
5353
|
+
message: "ard.json is missing the required `entries` array.",
|
|
5354
|
+
severity: "error",
|
|
5355
|
+
suggestion: ARD_SUGGEST
|
|
5356
|
+
}
|
|
5357
|
+
];
|
|
5358
|
+
}
|
|
5359
|
+
entries.forEach((entry, i) => {
|
|
5360
|
+
const at = `ard.json entries[${i}]`;
|
|
5361
|
+
if (!entry || typeof entry !== "object") {
|
|
5362
|
+
findings.push({ message: `${at} must be an object.`, severity: "error" });
|
|
5363
|
+
return;
|
|
5364
|
+
}
|
|
5365
|
+
if (typeof entry.identifier !== "string" || !URN_PATTERN.test(entry.identifier)) {
|
|
5366
|
+
findings.push({
|
|
5367
|
+
message: `${at}.identifier must match urn:air:<publisher>:<namespace>:<name>.`,
|
|
5368
|
+
severity: "error",
|
|
5369
|
+
suggestion: ARD_SUGGEST
|
|
5370
|
+
});
|
|
5371
|
+
}
|
|
5372
|
+
if (typeof entry.displayName !== "string" || entry.displayName.trim() === "") {
|
|
5373
|
+
findings.push({
|
|
5374
|
+
message: `${at}.displayName is required.`,
|
|
5375
|
+
severity: "error"
|
|
5376
|
+
});
|
|
5377
|
+
}
|
|
5378
|
+
if (typeof entry.type !== "string" || !MEDIA_TYPE_PATTERN.test(entry.type)) {
|
|
5379
|
+
findings.push({
|
|
5380
|
+
message: `${at}.type must be an IANA media type (e.g. application/mcp-server-card+json).`,
|
|
5381
|
+
severity: "error"
|
|
5382
|
+
});
|
|
5383
|
+
}
|
|
5384
|
+
const hasUrl = entry.url !== void 0;
|
|
5385
|
+
const hasData = entry.data !== void 0;
|
|
5386
|
+
if (hasUrl === hasData) {
|
|
5387
|
+
findings.push({
|
|
5388
|
+
message: `${at} must have exactly one of \`url\` or \`data\`.`,
|
|
5389
|
+
severity: "error"
|
|
5390
|
+
});
|
|
5391
|
+
}
|
|
5392
|
+
const rq = entry.representativeQueries;
|
|
5393
|
+
if (rq === void 0) {
|
|
5394
|
+
findings.push({
|
|
5395
|
+
message: `${at} is missing representativeQueries (2–5 recommended) — agents may not surface this resource.`,
|
|
5396
|
+
severity: "warning",
|
|
5397
|
+
suggestion: ARD_SUGGEST
|
|
5398
|
+
});
|
|
5399
|
+
} else if (!Array.isArray(rq) || rq.length < 2 || rq.length > 5) {
|
|
5400
|
+
findings.push({
|
|
5401
|
+
message: `${at}.representativeQueries should list 2–5 example queries.`,
|
|
5402
|
+
severity: "warning"
|
|
5403
|
+
});
|
|
5404
|
+
}
|
|
5405
|
+
});
|
|
5406
|
+
return findings;
|
|
5407
|
+
}
|
|
5408
|
+
async function checkArdFiles(projectRoot) {
|
|
5409
|
+
const wellKnown = path$1.resolve(projectRoot, "public/.well-known");
|
|
5410
|
+
const ardPath = path$1.join(wellKnown, "ard.json");
|
|
5411
|
+
const aliasPath = path$1.join(wellKnown, "ai-catalog.json");
|
|
5412
|
+
const ardRaw = await readIfExists(ardPath);
|
|
5413
|
+
const aliasRaw = await readIfExists(aliasPath);
|
|
5414
|
+
if (ardRaw === null && aliasRaw === null) {
|
|
5415
|
+
return { findings: [], absent: true };
|
|
5416
|
+
}
|
|
5417
|
+
const findings = [];
|
|
5418
|
+
if (ardRaw === null && aliasRaw !== null) {
|
|
5419
|
+
findings.push({
|
|
5420
|
+
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).",
|
|
5421
|
+
severity: "warning",
|
|
5422
|
+
suggestion: ARD_SUGGEST
|
|
5423
|
+
});
|
|
5424
|
+
} else if (ardRaw !== null && aliasRaw !== null && ardRaw.trim() !== aliasRaw.trim()) {
|
|
5425
|
+
findings.push({
|
|
5426
|
+
message: "public/.well-known/ard.json and ai-catalog.json differ — the alias should be a copy of ard.json.",
|
|
5427
|
+
severity: "warning"
|
|
5428
|
+
});
|
|
5429
|
+
}
|
|
5430
|
+
const canonicalRaw = ardRaw ?? aliasRaw;
|
|
5431
|
+
let parsed;
|
|
5432
|
+
try {
|
|
5433
|
+
parsed = JSON.parse(canonicalRaw);
|
|
5434
|
+
} catch (e2) {
|
|
5435
|
+
findings.push({
|
|
5436
|
+
message: `${ardRaw !== null ? "ard.json" : "ai-catalog.json"} is not valid JSON: ${e2.message}`,
|
|
5437
|
+
severity: "error"
|
|
5438
|
+
});
|
|
5439
|
+
return { findings, absent: false };
|
|
5440
|
+
}
|
|
5441
|
+
findings.push(...validateArdDocument(parsed));
|
|
5442
|
+
return { findings, absent: false };
|
|
5443
|
+
}
|
|
5444
|
+
async function readIfExists(p) {
|
|
5445
|
+
try {
|
|
5446
|
+
return await promises.readFile(p, "utf8");
|
|
5447
|
+
} catch {
|
|
5448
|
+
return null;
|
|
5449
|
+
}
|
|
5450
|
+
}
|
|
4748
5451
|
async function findJayFiles(dir) {
|
|
4749
5452
|
return await glob(`${dir}/**/*${JAY_EXTENSION$1}`);
|
|
4750
5453
|
}
|
|
@@ -4946,6 +5649,17 @@ function resolveContractTag(contract, tagPath) {
|
|
|
4946
5649
|
}
|
|
4947
5650
|
return void 0;
|
|
4948
5651
|
}
|
|
5652
|
+
function checkJsonLdScripts(jayHtml) {
|
|
5653
|
+
const errors = [];
|
|
5654
|
+
for (const script of jayHtml.scripts ?? []) {
|
|
5655
|
+
if (script.attributes?.type !== "application/ld+json") continue;
|
|
5656
|
+
const err = jsonLdBodyError(script.inline);
|
|
5657
|
+
if (err) {
|
|
5658
|
+
errors.push(`<script type="application/ld+json">: ${err} See designer/seo-guide.md.`);
|
|
5659
|
+
}
|
|
5660
|
+
}
|
|
5661
|
+
return errors;
|
|
5662
|
+
}
|
|
4949
5663
|
function checkRefElementTypes(jayHtml, file) {
|
|
4950
5664
|
const imports = jayHtml.headlessImports;
|
|
4951
5665
|
const keyMap = /* @__PURE__ */ new Map();
|
|
@@ -5873,14 +6587,12 @@ async function validateJayFiles(options = {}) {
|
|
|
5873
6587
|
const reuseByTemplate = /* @__PURE__ */ new Map();
|
|
5874
6588
|
const cataloguedContractFiles = /* @__PURE__ */ new Set();
|
|
5875
6589
|
const pageJayHtmlFiles = await findJayFiles(scanDir);
|
|
5876
|
-
const componentJayHtmlFiles = await findJayFiles(componentsDir).catch(() => []);
|
|
6590
|
+
const componentJayHtmlFiles = options.compileOnly ? [] : await findJayFiles(componentsDir).catch(() => []);
|
|
5877
6591
|
const jayHtmlFiles = [...pageJayHtmlFiles, ...componentJayHtmlFiles];
|
|
5878
6592
|
const componentJayHtmlFileSet = new Set(componentJayHtmlFiles);
|
|
5879
|
-
const componentContractFiles = await findContractFiles(componentsDir).catch(
|
|
5880
|
-
() => []
|
|
5881
|
-
);
|
|
6593
|
+
const componentContractFiles = options.compileOnly ? [] : await findContractFiles(componentsDir).catch(() => []);
|
|
5882
6594
|
for (const cf of componentContractFiles) cataloguedContractFiles.add(cf);
|
|
5883
|
-
const contractFiles = [...await findContractFiles(scanDir), ...componentContractFiles];
|
|
6595
|
+
const contractFiles = options.compileOnly ? [] : [...await findContractFiles(scanDir), ...componentContractFiles];
|
|
5884
6596
|
const contractNameByFile = /* @__PURE__ */ new Map();
|
|
5885
6597
|
if (options.verbose) {
|
|
5886
6598
|
getLogger().info(chalk.gray(`Scanning directory: ${scanDir}`));
|
|
@@ -5947,6 +6659,35 @@ async function validateJayFiles(options = {}) {
|
|
|
5947
6659
|
continue;
|
|
5948
6660
|
}
|
|
5949
6661
|
parsedFiles.push({ relativePath, parsed: parsedFile.val });
|
|
6662
|
+
if (options.compileOnly) {
|
|
6663
|
+
const generatedFile2 = generateElementFile(
|
|
6664
|
+
parsedFile.val,
|
|
6665
|
+
RuntimeMode$1.MainTrusted,
|
|
6666
|
+
GenerateTarget.jay
|
|
6667
|
+
);
|
|
6668
|
+
for (const validation of generatedFile2.validations) {
|
|
6669
|
+
errors.push({
|
|
6670
|
+
file: relativePath,
|
|
6671
|
+
message: validation,
|
|
6672
|
+
stage: "generate"
|
|
6673
|
+
});
|
|
6674
|
+
}
|
|
6675
|
+
const serverElementFile2 = generateServerElementFile(parsedFile.val);
|
|
6676
|
+
for (const validation of serverElementFile2.validations) {
|
|
6677
|
+
errors.push({
|
|
6678
|
+
file: relativePath,
|
|
6679
|
+
message: `[SSR] ${validation}`,
|
|
6680
|
+
stage: "generate"
|
|
6681
|
+
});
|
|
6682
|
+
}
|
|
6683
|
+
if (options.verbose) {
|
|
6684
|
+
const failed = generatedFile2.validations.length > 0 || serverElementFile2.validations.length > 0;
|
|
6685
|
+
getLogger().info(
|
|
6686
|
+
failed ? chalk.red(`❌ ${relativePath}`) : chalk.green(`✓ ${relativePath}`)
|
|
6687
|
+
);
|
|
6688
|
+
}
|
|
6689
|
+
continue;
|
|
6690
|
+
}
|
|
5950
6691
|
if (content.includes("application/jay-params")) {
|
|
5951
6692
|
errors.push({
|
|
5952
6693
|
file: relativePath,
|
|
@@ -6038,6 +6779,9 @@ async function validateJayFiles(options = {}) {
|
|
|
6038
6779
|
suggestion: finding.suggestion
|
|
6039
6780
|
});
|
|
6040
6781
|
}
|
|
6782
|
+
for (const message of checkJsonLdScripts(parsedFile.val)) {
|
|
6783
|
+
errors.push({ file: relativePath, message, stage: "generate" });
|
|
6784
|
+
}
|
|
6041
6785
|
regionsSeen += collectRegionElements(parsedFile.val.body).length;
|
|
6042
6786
|
templateImportsSeen += parsedFile.val.headlessImports.filter(
|
|
6043
6787
|
(imp) => imp.template
|
|
@@ -6165,6 +6909,24 @@ async function validateJayFiles(options = {}) {
|
|
|
6165
6909
|
}
|
|
6166
6910
|
}
|
|
6167
6911
|
}
|
|
6912
|
+
if (options.compileOnly) {
|
|
6913
|
+
return {
|
|
6914
|
+
valid: errors.length === 0,
|
|
6915
|
+
jayHtmlFilesScanned: jayHtmlFiles.length,
|
|
6916
|
+
contractFilesScanned: contractFiles.length,
|
|
6917
|
+
errors,
|
|
6918
|
+
warnings,
|
|
6919
|
+
coverage,
|
|
6920
|
+
pluginValidators: [],
|
|
6921
|
+
designSystemCoverage,
|
|
6922
|
+
designSystemReuse: {
|
|
6923
|
+
perTemplate: {},
|
|
6924
|
+
catalogued: 0,
|
|
6925
|
+
reusedMoreThanOnce: 0,
|
|
6926
|
+
reusedOfCatalogued: 0
|
|
6927
|
+
}
|
|
6928
|
+
};
|
|
6929
|
+
}
|
|
6168
6930
|
const robotsTxtPath = path$1.resolve(projectRoot, "public/robots.txt");
|
|
6169
6931
|
if (!fs$1.existsSync(robotsTxtPath)) {
|
|
6170
6932
|
warnings.push({
|
|
@@ -6180,6 +6942,60 @@ async function validateJayFiles(options = {}) {
|
|
|
6180
6942
|
suggestion: "Add to .jay config:\n site:\n baseUrl: https://your-domain.com"
|
|
6181
6943
|
});
|
|
6182
6944
|
}
|
|
6945
|
+
if (!config.site?.name) {
|
|
6946
|
+
warnings.push({
|
|
6947
|
+
file: ".jay",
|
|
6948
|
+
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.",
|
|
6949
|
+
suggestion: "Add to .jay config:\n site:\n name: Your Site Name\n logo: /logo.png\n description: What your site is about"
|
|
6950
|
+
});
|
|
6951
|
+
}
|
|
6952
|
+
const allowNoArd = config.validations?.["allow-no-ard"] === true;
|
|
6953
|
+
const ardResult = await checkArdFiles(projectRoot);
|
|
6954
|
+
if (ardResult.absent) {
|
|
6955
|
+
if (!allowNoArd) {
|
|
6956
|
+
warnings.push({
|
|
6957
|
+
file: "public/.well-known/ard.json",
|
|
6958
|
+
message: "public/.well-known/ard.json not found — agents cannot discover this site’s resources/APIs via Agentic Resource Discovery.",
|
|
6959
|
+
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."
|
|
6960
|
+
});
|
|
6961
|
+
}
|
|
6962
|
+
} else {
|
|
6963
|
+
for (const finding of ardResult.findings) {
|
|
6964
|
+
if (finding.severity === "error") {
|
|
6965
|
+
errors.push({
|
|
6966
|
+
file: "public/.well-known/ard.json",
|
|
6967
|
+
message: finding.message,
|
|
6968
|
+
stage: "generate",
|
|
6969
|
+
suggestion: finding.suggestion
|
|
6970
|
+
});
|
|
6971
|
+
} else {
|
|
6972
|
+
warnings.push({
|
|
6973
|
+
file: "public/.well-known/ard.json",
|
|
6974
|
+
message: finding.message,
|
|
6975
|
+
suggestion: finding.suggestion
|
|
6976
|
+
});
|
|
6977
|
+
}
|
|
6978
|
+
}
|
|
6979
|
+
}
|
|
6980
|
+
const allowNoErrorPages = config.validations?.["allow-no-error-pages"] === true;
|
|
6981
|
+
if (!allowNoErrorPages) {
|
|
6982
|
+
const scanResult = await scanRoutes(scanDir, {
|
|
6983
|
+
jayHtmlFilename: "page.jay-html",
|
|
6984
|
+
compFilename: "page.ts"
|
|
6985
|
+
});
|
|
6986
|
+
const errorPages = scanResult.errorPages ?? [];
|
|
6987
|
+
const tokens = new Set(errorPages.map((ep) => ep.token));
|
|
6988
|
+
const missing = [];
|
|
6989
|
+
if (!tokens.has("404") && !tokens.has("4xx")) missing.push("404");
|
|
6990
|
+
if (!tokens.has("5xx")) missing.push("5xx");
|
|
6991
|
+
if (missing.length > 0) {
|
|
6992
|
+
warnings.push({
|
|
6993
|
+
file: "src/pages",
|
|
6994
|
+
message: `No branded error page for ${missing.join(" and ")} — these responses fall back to plain text instead of your site's layout.`,
|
|
6995
|
+
suggestion: "Add pages/404/page.jay-html and pages/5xx/page.jay-html importing the `error` headless contract, or suppress with `validations: { allow-no-error-pages: true }` in .jay. See designer/error-pages-guide.md."
|
|
6996
|
+
});
|
|
6997
|
+
}
|
|
6998
|
+
}
|
|
6183
6999
|
const allowNoTemplate = /* @__PURE__ */ new Set();
|
|
6184
7000
|
let allowNoDesignSystem = false;
|
|
6185
7001
|
for (const { parsed } of parsedFiles) {
|
|
@@ -6235,6 +7051,38 @@ async function validateJayFiles(options = {}) {
|
|
|
6235
7051
|
(p) => (reuseByTemplate.get(p) ?? 0) >= 2
|
|
6236
7052
|
).length
|
|
6237
7053
|
};
|
|
7054
|
+
const linkOracle = await buildRouteOracle(scanDir);
|
|
7055
|
+
const assetUrls = await collectPublicAssets(
|
|
7056
|
+
path$1.resolve(projectRoot, resolvedConfig.devServer.publicFolder)
|
|
7057
|
+
);
|
|
7058
|
+
const linkFindings = checkInternalLinks({
|
|
7059
|
+
parsedFiles,
|
|
7060
|
+
oracle: linkOracle,
|
|
7061
|
+
assetUrls,
|
|
7062
|
+
baseUrl: config.site?.baseUrl,
|
|
7063
|
+
projectRoot
|
|
7064
|
+
});
|
|
7065
|
+
for (const finding of linkFindings) {
|
|
7066
|
+
errors.push({
|
|
7067
|
+
file: finding.file,
|
|
7068
|
+
message: finding.message,
|
|
7069
|
+
stage: "generate",
|
|
7070
|
+
source: "internal-links",
|
|
7071
|
+
suggestion: finding.suggestion
|
|
7072
|
+
});
|
|
7073
|
+
}
|
|
7074
|
+
if (options.tier2) {
|
|
7075
|
+
await runBuildOutputValidation({
|
|
7076
|
+
projectRoot,
|
|
7077
|
+
buildDir: options.buildDir,
|
|
7078
|
+
parsedFiles,
|
|
7079
|
+
linkOracle,
|
|
7080
|
+
assetUrls,
|
|
7081
|
+
baseUrl: config.site?.baseUrl,
|
|
7082
|
+
errors,
|
|
7083
|
+
warnings
|
|
7084
|
+
});
|
|
7085
|
+
}
|
|
6238
7086
|
const pluginValidators = await runPluginValidators(projectRoot, parsedFiles, errors, warnings);
|
|
6239
7087
|
return {
|
|
6240
7088
|
valid: errors.length === 0,
|
|
@@ -6248,6 +7096,92 @@ async function validateJayFiles(options = {}) {
|
|
|
6248
7096
|
designSystemReuse
|
|
6249
7097
|
};
|
|
6250
7098
|
}
|
|
7099
|
+
async function runBuildOutputValidation(args) {
|
|
7100
|
+
const { projectRoot, buildDir, parsedFiles, linkOracle, assetUrls, baseUrl, errors, warnings } = args;
|
|
7101
|
+
const backendDir = await discoverBuildBackendDir(projectRoot, buildDir);
|
|
7102
|
+
if (!backendDir) {
|
|
7103
|
+
errors.push({
|
|
7104
|
+
file: "build",
|
|
7105
|
+
message: "Tier 2: no build found. Run `jay-stack build` first, or point --build-dir at a build.",
|
|
7106
|
+
stage: "generate",
|
|
7107
|
+
source: "build-output"
|
|
7108
|
+
});
|
|
7109
|
+
return;
|
|
7110
|
+
}
|
|
7111
|
+
const loaded = await loadBuildManifest(backendDir);
|
|
7112
|
+
if (!loaded) {
|
|
7113
|
+
errors.push({
|
|
7114
|
+
file: backendDir,
|
|
7115
|
+
message: `Tier 2: could not read route-manifest.json in ${backendDir}.`,
|
|
7116
|
+
stage: "generate",
|
|
7117
|
+
source: "build-output"
|
|
7118
|
+
});
|
|
7119
|
+
return;
|
|
7120
|
+
}
|
|
7121
|
+
const newest = await newestSourceMtime([
|
|
7122
|
+
path$1.join(projectRoot, "src"),
|
|
7123
|
+
path$1.join(projectRoot, "content")
|
|
7124
|
+
]);
|
|
7125
|
+
if (isBuildStale(loaded.metadata, newest)) {
|
|
7126
|
+
warnings.push({
|
|
7127
|
+
file: "build",
|
|
7128
|
+
message: `Tier 2: validated against a build from ${loaded.metadata?.buildTimestamp}; source has changed since.`,
|
|
7129
|
+
source: "build-output",
|
|
7130
|
+
suggestion: "Re-run `jay-stack build` for accurate results."
|
|
7131
|
+
});
|
|
7132
|
+
}
|
|
7133
|
+
const oracle = buildBuildOracle(loaded.manifest, backendDir);
|
|
7134
|
+
const findings = [];
|
|
7135
|
+
findings.push(
|
|
7136
|
+
...checkTemplateLinksAgainstBuild({
|
|
7137
|
+
parsedFiles,
|
|
7138
|
+
buildUrls: oracle.urls,
|
|
7139
|
+
assetUrls,
|
|
7140
|
+
dynamicMatchers: linkOracle.dynamicMatchers,
|
|
7141
|
+
baseUrl
|
|
7142
|
+
})
|
|
7143
|
+
);
|
|
7144
|
+
findings.push(...checkBuildJsonLd(loaded.manifest));
|
|
7145
|
+
for (const instance of oracle.instances) {
|
|
7146
|
+
const slowViewState = await loadSlowViewState(instance.cacheAbsPath);
|
|
7147
|
+
findings.push(
|
|
7148
|
+
...checkContentLinks({
|
|
7149
|
+
instanceUrl: instance.url,
|
|
7150
|
+
slowViewState,
|
|
7151
|
+
buildUrls: oracle.urls,
|
|
7152
|
+
assetUrls,
|
|
7153
|
+
baseUrl
|
|
7154
|
+
})
|
|
7155
|
+
);
|
|
7156
|
+
findings.push(
|
|
7157
|
+
...checkInstanceMeta({
|
|
7158
|
+
instanceUrl: instance.url,
|
|
7159
|
+
headMeta: instance.headMeta,
|
|
7160
|
+
slowViewState
|
|
7161
|
+
})
|
|
7162
|
+
);
|
|
7163
|
+
const carryForward = await loadCarryForward(instance.cacheAbsPath);
|
|
7164
|
+
findings.push(...checkInstanceJsonLd({ instanceUrl: instance.url, carryForward }));
|
|
7165
|
+
}
|
|
7166
|
+
for (const finding of findings) {
|
|
7167
|
+
if (finding.severity === "error") {
|
|
7168
|
+
errors.push({
|
|
7169
|
+
file: finding.file,
|
|
7170
|
+
message: finding.message,
|
|
7171
|
+
stage: "generate",
|
|
7172
|
+
source: "build-output",
|
|
7173
|
+
suggestion: finding.suggestion
|
|
7174
|
+
});
|
|
7175
|
+
} else {
|
|
7176
|
+
warnings.push({
|
|
7177
|
+
file: finding.file,
|
|
7178
|
+
message: finding.message,
|
|
7179
|
+
source: "build-output",
|
|
7180
|
+
suggestion: finding.suggestion
|
|
7181
|
+
});
|
|
7182
|
+
}
|
|
7183
|
+
}
|
|
7184
|
+
}
|
|
6251
7185
|
function printJayValidationResult(result, options) {
|
|
6252
7186
|
const logger = getLogger();
|
|
6253
7187
|
if (options.json) {
|
|
@@ -6255,9 +7189,11 @@ function printJayValidationResult(result, options) {
|
|
|
6255
7189
|
return;
|
|
6256
7190
|
}
|
|
6257
7191
|
logger.important("");
|
|
6258
|
-
const coreErrors = result.errors.filter(
|
|
7192
|
+
const coreErrors = result.errors.filter(
|
|
7193
|
+
(e2) => e2.stage !== "plugin" && e2.source !== "build-output"
|
|
7194
|
+
);
|
|
6259
7195
|
const coreWarnings = result.warnings.filter((w) => !w.source);
|
|
6260
|
-
logger.important(chalk.bold("📦 jay-stack (core)"));
|
|
7196
|
+
logger.important(chalk.bold("📦 Tier 1 — jay-stack (core)"));
|
|
6261
7197
|
if (coreErrors.length === 0) {
|
|
6262
7198
|
logger.important(
|
|
6263
7199
|
chalk.green(
|
|
@@ -6280,6 +7216,40 @@ function printJayValidationResult(result, options) {
|
|
|
6280
7216
|
logger.important(chalk.blue(` Suggestion: ${warning.suggestion}`));
|
|
6281
7217
|
}
|
|
6282
7218
|
}
|
|
7219
|
+
const buildErrors = result.errors.filter((e2) => e2.source === "build-output");
|
|
7220
|
+
const buildWarnings = result.warnings.filter((w) => w.source === "build-output");
|
|
7221
|
+
if (buildErrors.length > 0 || buildWarnings.length > 0) {
|
|
7222
|
+
logger.important("");
|
|
7223
|
+
logger.important(chalk.bold("📦 Tier 2 — build-output validation (--tier-2)"));
|
|
7224
|
+
for (const error of buildErrors) {
|
|
7225
|
+
logger.important(chalk.red(` ❌ ${error.file}`));
|
|
7226
|
+
logger.important(chalk.gray(` ${error.message}`));
|
|
7227
|
+
if (error.suggestion) {
|
|
7228
|
+
logger.important(chalk.blue(` Suggestion: ${error.suggestion}`));
|
|
7229
|
+
}
|
|
7230
|
+
}
|
|
7231
|
+
const buildWarnGroups = /* @__PURE__ */ new Map();
|
|
7232
|
+
for (const warning of buildWarnings) {
|
|
7233
|
+
const group = buildWarnGroups.get(warning.file) || [];
|
|
7234
|
+
group.push(warning);
|
|
7235
|
+
buildWarnGroups.set(warning.file, group);
|
|
7236
|
+
}
|
|
7237
|
+
for (const [file, groupWarns] of buildWarnGroups) {
|
|
7238
|
+
logger.important(chalk.yellow(` ⚠ ${file}`));
|
|
7239
|
+
for (const warning of groupWarns) {
|
|
7240
|
+
if (warning.message) {
|
|
7241
|
+
logger.important(chalk.gray(` ${warning.message}`));
|
|
7242
|
+
}
|
|
7243
|
+
}
|
|
7244
|
+
const suggestions = [...new Set(groupWarns.map((w) => w.suggestion).filter(Boolean))];
|
|
7245
|
+
if (suggestions.length > 0) {
|
|
7246
|
+
logger.important(chalk.blue(` Suggestions:`));
|
|
7247
|
+
for (const s2 of suggestions) {
|
|
7248
|
+
logger.important(chalk.blue(` ${s2}`));
|
|
7249
|
+
}
|
|
7250
|
+
}
|
|
7251
|
+
}
|
|
7252
|
+
}
|
|
6283
7253
|
const pluginErrors = result.errors.filter((e2) => e2.stage === "plugin");
|
|
6284
7254
|
const pluginWarnings = result.warnings.filter((w) => !!w.source);
|
|
6285
7255
|
for (const validatorName of result.pluginValidators) {
|
|
@@ -6422,12 +7392,23 @@ function printJayValidationResult(result, options) {
|
|
|
6422
7392
|
)
|
|
6423
7393
|
);
|
|
6424
7394
|
}
|
|
7395
|
+
if (!options.tier2) {
|
|
7396
|
+
logger.important("");
|
|
7397
|
+
logger.important(
|
|
7398
|
+
chalk.gray(
|
|
7399
|
+
"ℹ 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"
|
|
7400
|
+
)
|
|
7401
|
+
);
|
|
7402
|
+
}
|
|
6425
7403
|
}
|
|
6426
7404
|
async function runValidate(scanPath, options) {
|
|
6427
7405
|
const result = await validateJayFiles({
|
|
6428
7406
|
path: scanPath,
|
|
6429
7407
|
verbose: options.verbose,
|
|
6430
|
-
json: options.json
|
|
7408
|
+
json: options.json,
|
|
7409
|
+
tier2: options.tier2,
|
|
7410
|
+
buildDir: options.buildDir,
|
|
7411
|
+
compileOnly: options.compileOnly
|
|
6431
7412
|
});
|
|
6432
7413
|
printJayValidationResult(result, options);
|
|
6433
7414
|
if (!result.valid) {
|
|
@@ -6533,10 +7514,12 @@ async function resolveProductionContext(projectPath, versionOverride) {
|
|
|
6533
7514
|
const jayConfigPath = path__default.join(resolvedPath, ".jay");
|
|
6534
7515
|
let pagesBase = "./src/pages";
|
|
6535
7516
|
let siteBaseUrl;
|
|
7517
|
+
let site;
|
|
6536
7518
|
try {
|
|
6537
7519
|
const jayConfig = YAML.parse(await fs$2.readFile(jayConfigPath, "utf-8"));
|
|
6538
7520
|
pagesBase = jayConfig?.devServer?.pagesBase || pagesBase;
|
|
6539
7521
|
siteBaseUrl = jayConfig?.site?.baseUrl;
|
|
7522
|
+
site = jayConfig?.site;
|
|
6540
7523
|
} catch {
|
|
6541
7524
|
}
|
|
6542
7525
|
const version = versionOverride || await resolveVersionFromPackageJson(resolvedPath);
|
|
@@ -6546,7 +7529,8 @@ async function resolveProductionContext(projectPath, versionOverride) {
|
|
|
6546
7529
|
buildRoot: path__default.join(resolvedPath, "build"),
|
|
6547
7530
|
version,
|
|
6548
7531
|
tsConfigFilePath: path__default.join(resolvedPath, "tsconfig.json"),
|
|
6549
|
-
siteBaseUrl
|
|
7532
|
+
siteBaseUrl,
|
|
7533
|
+
site
|
|
6550
7534
|
};
|
|
6551
7535
|
}
|
|
6552
7536
|
async function resolveVersionFromPackageJson(projectRoot) {
|
|
@@ -6574,7 +7558,7 @@ async function runBuild(projectPath, options) {
|
|
|
6574
7558
|
}
|
|
6575
7559
|
const ctx = await resolveProductionContext(projectPath, options.version);
|
|
6576
7560
|
await generateDefinitionFiles(ctx);
|
|
6577
|
-
const { buildVersion } = await import("./index-
|
|
7561
|
+
const { buildVersion } = await import("./index-B5oavmfA.js");
|
|
6578
7562
|
await buildVersion({
|
|
6579
7563
|
version: ctx.version,
|
|
6580
7564
|
projectRoot: ctx.resolvedPath,
|
|
@@ -6583,7 +7567,8 @@ async function runBuild(projectPath, options) {
|
|
|
6583
7567
|
concurrency: 4,
|
|
6584
7568
|
tsConfigFilePath: ctx.tsConfigFilePath,
|
|
6585
7569
|
minify: options.minify,
|
|
6586
|
-
siteBaseUrl: ctx.siteBaseUrl
|
|
7570
|
+
siteBaseUrl: ctx.siteBaseUrl,
|
|
7571
|
+
site: ctx.site
|
|
6587
7572
|
});
|
|
6588
7573
|
}
|
|
6589
7574
|
async function generateDefinitionFiles(ctx) {
|
|
@@ -6650,7 +7635,8 @@ async function runRebuild(projectPath, options) {
|
|
|
6650
7635
|
version: ctx.version,
|
|
6651
7636
|
target,
|
|
6652
7637
|
tsConfigFilePath: ctx.tsConfigFilePath,
|
|
6653
|
-
siteBaseUrl: ctx.siteBaseUrl
|
|
7638
|
+
siteBaseUrl: ctx.siteBaseUrl,
|
|
7639
|
+
site: ctx.site
|
|
6654
7640
|
});
|
|
6655
7641
|
if (result.errors.length > 0) {
|
|
6656
7642
|
for (const err of result.errors) {
|
|
@@ -7694,9 +8680,24 @@ program.command("cleanup").description("Delete orphaned files from previous rebu
|
|
|
7694
8680
|
process.exit(1);
|
|
7695
8681
|
}
|
|
7696
8682
|
});
|
|
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").
|
|
8683
|
+
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(
|
|
8684
|
+
new Option(
|
|
8685
|
+
"--tier-2",
|
|
8686
|
+
"Run Tier 2: deep validation against an existing build (dynamic-slug links, rendered content, per-instance meta)"
|
|
8687
|
+
)
|
|
8688
|
+
).addOption(new Option("--t2", "Alias for --tier-2").hideHelp()).option(
|
|
8689
|
+
"--build-dir <dir>",
|
|
8690
|
+
"Build backend dir or build root for Tier 2 (default: highest build/v*)"
|
|
8691
|
+
).option(
|
|
8692
|
+
"--compile-only",
|
|
8693
|
+
"Parse and compile .jay-html only (no project rules — for plugin route source checks)"
|
|
8694
|
+
).action(async (options) => {
|
|
7698
8695
|
try {
|
|
7699
|
-
await runValidate(options.path,
|
|
8696
|
+
await runValidate(options.path, {
|
|
8697
|
+
...options,
|
|
8698
|
+
tier2: options.tier2 || options.t2,
|
|
8699
|
+
compileOnly: options.compileOnly
|
|
8700
|
+
});
|
|
7700
8701
|
} catch (error) {
|
|
7701
8702
|
if (options.json) {
|
|
7702
8703
|
getLogger().important(
|
|
@@ -7758,6 +8759,7 @@ export {
|
|
|
7758
8759
|
listContracts,
|
|
7759
8760
|
loadConfig,
|
|
7760
8761
|
materializeContracts,
|
|
8762
|
+
setWellKnownHeaders,
|
|
7761
8763
|
startDevServer,
|
|
7762
8764
|
updateConfig
|
|
7763
8765
|
};
|