@jay-framework/jay-stack-cli 0.25.1 → 0.25.3
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/INSTRUCTIONS.md +1 -0
- package/agent-kit-template/designer/error-pages-guide.md +109 -0
- package/agent-kit-template/designer/routing.md +7 -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-BQbkAEhi.js → index-B5oavmfA.js} +739 -552
- package/dist/index.d.ts +34 -1
- package/dist/index.js +439 -41
- package/package.json +14 -14
package/dist/index.js
CHANGED
|
@@ -50,7 +50,8 @@ function loadConfig(projectRoot) {
|
|
|
50
50
|
...DEFAULT_CONFIG.devServer,
|
|
51
51
|
...userConfig.devServer
|
|
52
52
|
},
|
|
53
|
-
site: userConfig.site
|
|
53
|
+
site: userConfig.site,
|
|
54
|
+
validations: userConfig.validations
|
|
54
55
|
};
|
|
55
56
|
} catch (error) {
|
|
56
57
|
getLogger().warn(`Failed to parse .jay YAML config file, using defaults: ${error}`);
|
|
@@ -134,10 +135,6 @@ async function generatePageDefinitionFiles(jayHtmlPaths, tsConfigPath, projectRo
|
|
|
134
135
|
}
|
|
135
136
|
}
|
|
136
137
|
}
|
|
137
|
-
const generatePageDefinitionFiles$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
138
|
-
__proto__: null,
|
|
139
|
-
generatePageDefinitionFiles
|
|
140
|
-
}, Symbol.toStringTag, { value: "Module" }));
|
|
141
138
|
const IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", "build", "dist", ".git", ".cache"]);
|
|
142
139
|
async function generateContractDefinitionFiles(root) {
|
|
143
140
|
const contractFiles = await collectContractFiles(root);
|
|
@@ -195,10 +192,13 @@ async function collectContractFiles(root) {
|
|
|
195
192
|
await walk(root);
|
|
196
193
|
return results;
|
|
197
194
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
195
|
+
function setWellKnownHeaders(res, filePath) {
|
|
196
|
+
if (filePath.includes(`${path$1.sep}.well-known${path$1.sep}`)) {
|
|
197
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
198
|
+
if (filePath.endsWith(".md")) res.setHeader("Content-Type", "text/markdown; charset=utf-8");
|
|
199
|
+
if (filePath.endsWith(".gz")) res.setHeader("Content-Type", "application/gzip");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
202
|
async function startDevServer(options = {}) {
|
|
203
203
|
const projectPath = options.projectPath || process.cwd();
|
|
204
204
|
if (projectPath !== process.cwd()) {
|
|
@@ -214,7 +214,7 @@ async function startDevServer(options = {}) {
|
|
|
214
214
|
const httpServer = http.createServer(app);
|
|
215
215
|
const devServerPort = await getPort({ port: resolvedConfig.devServer.portRange });
|
|
216
216
|
const log = getLogger();
|
|
217
|
-
const { server, viteServer, routes, service } = await mkDevServer({
|
|
217
|
+
const { server, viteServer, routes, service, errorPageHandler } = await mkDevServer({
|
|
218
218
|
pagesRootFolder: path$1.resolve(resolvedConfig.devServer.pagesBase),
|
|
219
219
|
projectRootFolder: process.cwd(),
|
|
220
220
|
publicBaseUrlPath: "/",
|
|
@@ -232,18 +232,20 @@ async function startDevServer(options = {}) {
|
|
|
232
232
|
});
|
|
233
233
|
}
|
|
234
234
|
if (fs$1.existsSync(publicPath)) {
|
|
235
|
-
app.use(express.static(publicPath));
|
|
235
|
+
app.use(express.static(publicPath, { dotfiles: "allow", setHeaders: setWellKnownHeaders }));
|
|
236
236
|
} else {
|
|
237
237
|
log.important(`⚠️ Public folder not found: ${resolvedConfig.devServer.publicFolder}`);
|
|
238
238
|
}
|
|
239
|
+
const pageRouter = express.Router();
|
|
239
240
|
routes.forEach((route) => {
|
|
240
|
-
|
|
241
|
+
pageRouter.get(route.path, route.handler);
|
|
241
242
|
});
|
|
242
243
|
service.attachRouteRegistrar((added) => {
|
|
243
244
|
added.forEach((route) => {
|
|
244
|
-
|
|
245
|
+
pageRouter.get(route.path, route.handler);
|
|
245
246
|
});
|
|
246
247
|
});
|
|
248
|
+
app.use(pageRouter);
|
|
247
249
|
void (async () => {
|
|
248
250
|
await generatePageDefinitionFiles(
|
|
249
251
|
routes.map((route) => route.fsRoute.jayHtmlPath),
|
|
@@ -298,6 +300,7 @@ async function startDevServer(options = {}) {
|
|
|
298
300
|
}, options.timeout * 1e3);
|
|
299
301
|
}
|
|
300
302
|
}
|
|
303
|
+
app.use(errorPageHandler(404));
|
|
301
304
|
process.on("SIGTERM", shutdown);
|
|
302
305
|
process.on("SIGINT", shutdown);
|
|
303
306
|
}
|
|
@@ -3004,6 +3007,61 @@ async function validateAiditorSettings(context, result) {
|
|
|
3004
3007
|
});
|
|
3005
3008
|
}
|
|
3006
3009
|
}
|
|
3010
|
+
async function compileValidateRouteJayHtml(absoluteJayHtmlPath, pluginRoot) {
|
|
3011
|
+
const content = fs$1.readFileSync(absoluteJayHtmlPath, "utf-8");
|
|
3012
|
+
const dirname = path$1.dirname(absoluteJayHtmlPath);
|
|
3013
|
+
const filename = path$1.basename(absoluteJayHtmlPath, JAY_EXTENSION$1);
|
|
3014
|
+
const errors = [];
|
|
3015
|
+
try {
|
|
3016
|
+
const parsedFile = await parseJayFile(
|
|
3017
|
+
content,
|
|
3018
|
+
filename,
|
|
3019
|
+
dirname,
|
|
3020
|
+
{},
|
|
3021
|
+
JAY_IMPORT_RESOLVER,
|
|
3022
|
+
pluginRoot
|
|
3023
|
+
);
|
|
3024
|
+
for (const validation of parsedFile.validations) {
|
|
3025
|
+
errors.push(validation);
|
|
3026
|
+
}
|
|
3027
|
+
if (!parsedFile.val || parsedFile.validations.length > 0) {
|
|
3028
|
+
return errors;
|
|
3029
|
+
}
|
|
3030
|
+
const generatedFile = generateElementFile(parsedFile.val, RuntimeMode$1.MainTrusted);
|
|
3031
|
+
for (const validation of generatedFile.validations) {
|
|
3032
|
+
errors.push(validation);
|
|
3033
|
+
}
|
|
3034
|
+
const serverElementFile = generateServerElementFile(parsedFile.val);
|
|
3035
|
+
for (const validation of serverElementFile.validations) {
|
|
3036
|
+
errors.push(`[SSR] ${validation}`);
|
|
3037
|
+
}
|
|
3038
|
+
} catch (error) {
|
|
3039
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3040
|
+
errors.push(message);
|
|
3041
|
+
}
|
|
3042
|
+
return errors;
|
|
3043
|
+
}
|
|
3044
|
+
async function validateRouteJayHtmlTemplates(context, result) {
|
|
3045
|
+
const routes = context.manifest.routes;
|
|
3046
|
+
if (!routes || !Array.isArray(routes)) return;
|
|
3047
|
+
for (const route of routes) {
|
|
3048
|
+
if (!route.jayHtml) continue;
|
|
3049
|
+
const absolutePath = path$1.resolve(context.pluginPath, route.jayHtml);
|
|
3050
|
+
if (!fs$1.existsSync(absolutePath)) {
|
|
3051
|
+
continue;
|
|
3052
|
+
}
|
|
3053
|
+
const compileErrors = await compileValidateRouteJayHtml(absolutePath, context.pluginPath);
|
|
3054
|
+
const relPath = path$1.relative(context.pluginPath, absolutePath);
|
|
3055
|
+
for (const message of compileErrors) {
|
|
3056
|
+
result.errors.push({
|
|
3057
|
+
type: "jay-html-compile",
|
|
3058
|
+
message: `Route "${route.path}" jayHtml failed to compile: ${message}`,
|
|
3059
|
+
location: relPath,
|
|
3060
|
+
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"
|
|
3061
|
+
});
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3007
3065
|
async function validatePlugin(options = {}) {
|
|
3008
3066
|
const pluginPath = options.pluginPath || process.cwd();
|
|
3009
3067
|
if (options.local) {
|
|
@@ -3073,6 +3131,7 @@ async function validatePluginPackage(pluginPath, options) {
|
|
|
3073
3131
|
await validateAddMenuCatalog(context, result);
|
|
3074
3132
|
await validateAiditorSettings(context, result);
|
|
3075
3133
|
validateNoCompilerLeak(context, result);
|
|
3134
|
+
await validateRouteJayHtmlTemplates(context, result);
|
|
3076
3135
|
result.valid = result.errors.length === 0;
|
|
3077
3136
|
return result;
|
|
3078
3137
|
}
|
|
@@ -3927,6 +3986,20 @@ async function validatePackageJson(context, result) {
|
|
|
3927
3986
|
suggestion: 'Add "./plugin.yaml": "./plugin.yaml" to exports field'
|
|
3928
3987
|
});
|
|
3929
3988
|
}
|
|
3989
|
+
if (context.manifest.commands) {
|
|
3990
|
+
for (const cmd of context.manifest.commands) {
|
|
3991
|
+
if (!cmd.command) continue;
|
|
3992
|
+
const exportKey = `./${cmd.command}`;
|
|
3993
|
+
if (!packageJson.exports || !packageJson.exports[exportKey]) {
|
|
3994
|
+
result.errors.push({
|
|
3995
|
+
type: "export-mismatch",
|
|
3996
|
+
message: `command metadata file "${cmd.command}" not exported in package.json (required for plugin resolution)`,
|
|
3997
|
+
location: packageJsonPath,
|
|
3998
|
+
suggestion: `Add "${exportKey}": "${exportKey}" to exports field`
|
|
3999
|
+
});
|
|
4000
|
+
}
|
|
4001
|
+
}
|
|
4002
|
+
}
|
|
3930
4003
|
const agentKitDir = path$1.join(context.pluginPath, "agent-kit");
|
|
3931
4004
|
if (fs$1.existsSync(agentKitDir) && fs$1.statSync(agentKitDir).isDirectory()) {
|
|
3932
4005
|
const filesArray = packageJson.files;
|
|
@@ -4789,10 +4862,10 @@ function routeToRegex(route) {
|
|
|
4789
4862
|
async function buildRouteOracle(pagesBase) {
|
|
4790
4863
|
let routes = [];
|
|
4791
4864
|
try {
|
|
4792
|
-
routes = await scanRoutes(pagesBase, {
|
|
4865
|
+
({ routes } = await scanRoutes(pagesBase, {
|
|
4793
4866
|
jayHtmlFilename: "page.jay-html",
|
|
4794
4867
|
compFilename: "page.ts"
|
|
4795
|
-
});
|
|
4868
|
+
}));
|
|
4796
4869
|
} catch {
|
|
4797
4870
|
routes = [];
|
|
4798
4871
|
}
|
|
@@ -4832,6 +4905,10 @@ async function collectPublicAssets(publicFolder) {
|
|
|
4832
4905
|
await walk(publicFolder, "");
|
|
4833
4906
|
return assets;
|
|
4834
4907
|
}
|
|
4908
|
+
function openingTag(el) {
|
|
4909
|
+
const attrs = el.rawAttrs?.replace(/\s+jay-[\w-]+(="[^"]*"|='[^']*')?/g, "").replace(/\s+/g, " ").trim();
|
|
4910
|
+
return attrs ? `<${el.rawTagName} ${attrs}>` : `<${el.rawTagName}>`;
|
|
4911
|
+
}
|
|
4835
4912
|
function originOf(baseUrl) {
|
|
4836
4913
|
try {
|
|
4837
4914
|
return new URL(baseUrl).origin;
|
|
@@ -4871,10 +4948,11 @@ function checkInternalLinks(options) {
|
|
|
4871
4948
|
if (raw === void 0 || raw === null) continue;
|
|
4872
4949
|
const verdict = classifyHref(raw, baseUrl);
|
|
4873
4950
|
if (verdict.kind === "skip") continue;
|
|
4951
|
+
const tag = openingTag(anchor);
|
|
4874
4952
|
if (verdict.kind === "degenerate") {
|
|
4875
4953
|
findings.push({
|
|
4876
4954
|
file: relativePath,
|
|
4877
|
-
message: `Anchor has a placeholder href "${raw}" — it links nowhere and ships as a dead link.`,
|
|
4955
|
+
message: `Anchor ${tag} has a placeholder href "${raw}" — it links nowhere and ships as a dead link.`,
|
|
4878
4956
|
suggestion: `Point the link at a real page, use <button> for a JS-driven action, or ${SUPPRESS_HINT}.`
|
|
4879
4957
|
});
|
|
4880
4958
|
continue;
|
|
@@ -4883,7 +4961,7 @@ function checkInternalLinks(options) {
|
|
|
4883
4961
|
if (ownUrl !== void 0 && target === ownUrl) {
|
|
4884
4962
|
findings.push({
|
|
4885
4963
|
file: relativePath,
|
|
4886
|
-
message: `
|
|
4964
|
+
message: `Anchor ${tag} points to the page's own route "${target}" — a self-link goes nowhere.`,
|
|
4887
4965
|
suggestion: `Remove the link, use "#" within-page navigation, or point it at a different page.`
|
|
4888
4966
|
});
|
|
4889
4967
|
continue;
|
|
@@ -4893,7 +4971,7 @@ function checkInternalLinks(options) {
|
|
|
4893
4971
|
if (oracle.dynamicMatchers.some((re) => re.test(target))) continue;
|
|
4894
4972
|
findings.push({
|
|
4895
4973
|
file: relativePath,
|
|
4896
|
-
message: `Broken internal link
|
|
4974
|
+
message: `Broken internal link in anchor ${tag} — no page or public asset produces the URL "${target}".`,
|
|
4897
4975
|
suggestion: `Create a placeholder page/route for "${target}", or remove the link. Otherwise ${SUPPRESS_HINT}.`
|
|
4898
4976
|
});
|
|
4899
4977
|
}
|
|
@@ -5005,6 +5083,89 @@ async function loadSlowViewState(cacheAbsPath) {
|
|
|
5005
5083
|
return {};
|
|
5006
5084
|
}
|
|
5007
5085
|
}
|
|
5086
|
+
async function loadCarryForward(cacheAbsPath) {
|
|
5087
|
+
try {
|
|
5088
|
+
const cache = JSON.parse(await promises.readFile(cacheAbsPath, "utf8"));
|
|
5089
|
+
return cache.carryForward ?? {};
|
|
5090
|
+
} catch {
|
|
5091
|
+
return {};
|
|
5092
|
+
}
|
|
5093
|
+
}
|
|
5094
|
+
function jsonLdBodyError(body) {
|
|
5095
|
+
const text = body?.trim();
|
|
5096
|
+
if (!text) return "Empty JSON-LD block — add structured data or remove the tag.";
|
|
5097
|
+
let parsed;
|
|
5098
|
+
try {
|
|
5099
|
+
parsed = JSON.parse(text);
|
|
5100
|
+
} catch (e2) {
|
|
5101
|
+
return `Invalid JSON: ${e2.message}`;
|
|
5102
|
+
}
|
|
5103
|
+
const objs = Array.isArray(parsed) ? parsed : [parsed];
|
|
5104
|
+
for (const obj of objs) {
|
|
5105
|
+
if (!obj || typeof obj !== "object") {
|
|
5106
|
+
return "JSON-LD must be an object (or array of objects) with @context and @type.";
|
|
5107
|
+
}
|
|
5108
|
+
if (!("@context" in obj) || !("@type" in obj)) {
|
|
5109
|
+
return "JSON-LD is missing @context or @type.";
|
|
5110
|
+
}
|
|
5111
|
+
}
|
|
5112
|
+
return null;
|
|
5113
|
+
}
|
|
5114
|
+
function isLdJsonHeadTag(tag) {
|
|
5115
|
+
return tag?.tag?.toLowerCase?.() === "script" && tag?.attrs?.type === "application/ld+json";
|
|
5116
|
+
}
|
|
5117
|
+
function checkBuildJsonLd(manifest) {
|
|
5118
|
+
const findings = [];
|
|
5119
|
+
for (const tag of manifest.siteHeadTags ?? []) {
|
|
5120
|
+
if (!isLdJsonHeadTag(tag)) continue;
|
|
5121
|
+
const err = jsonLdBodyError(tag.children);
|
|
5122
|
+
if (err) {
|
|
5123
|
+
findings.push({
|
|
5124
|
+
file: ".jay (site.*)",
|
|
5125
|
+
message: `Site JSON-LD: ${err}`,
|
|
5126
|
+
severity: "error",
|
|
5127
|
+
source: "build-output"
|
|
5128
|
+
});
|
|
5129
|
+
}
|
|
5130
|
+
}
|
|
5131
|
+
for (const route of manifest.routes) {
|
|
5132
|
+
for (const script of route.scripts ?? []) {
|
|
5133
|
+
if (script.attributes?.type !== "application/ld+json") continue;
|
|
5134
|
+
const err = jsonLdBodyError(script.inline);
|
|
5135
|
+
if (err) {
|
|
5136
|
+
findings.push({
|
|
5137
|
+
file: route.pattern,
|
|
5138
|
+
message: `Template JSON-LD: ${err}`,
|
|
5139
|
+
severity: "error",
|
|
5140
|
+
source: "build-output"
|
|
5141
|
+
});
|
|
5142
|
+
}
|
|
5143
|
+
}
|
|
5144
|
+
}
|
|
5145
|
+
return findings;
|
|
5146
|
+
}
|
|
5147
|
+
function checkInstanceJsonLd(args) {
|
|
5148
|
+
const { instanceUrl, carryForward } = args;
|
|
5149
|
+
const findings = [];
|
|
5150
|
+
const slowHeadTags = carryForward?.__slowHeadTags;
|
|
5151
|
+
if (!Array.isArray(slowHeadTags)) return findings;
|
|
5152
|
+
for (const source of slowHeadTags) {
|
|
5153
|
+
if (!Array.isArray(source)) continue;
|
|
5154
|
+
for (const tag of source) {
|
|
5155
|
+
if (!isLdJsonHeadTag(tag)) continue;
|
|
5156
|
+
const err = jsonLdBodyError(tag?.children);
|
|
5157
|
+
if (err) {
|
|
5158
|
+
findings.push({
|
|
5159
|
+
file: instanceUrl,
|
|
5160
|
+
message: `Component JSON-LD: ${err}`,
|
|
5161
|
+
severity: "error",
|
|
5162
|
+
source: "build-output"
|
|
5163
|
+
});
|
|
5164
|
+
}
|
|
5165
|
+
}
|
|
5166
|
+
}
|
|
5167
|
+
return findings;
|
|
5168
|
+
}
|
|
5008
5169
|
function checkTemplateLinksAgainstBuild(args) {
|
|
5009
5170
|
const { parsedFiles, buildUrls, assetUrls, dynamicMatchers, baseUrl } = args;
|
|
5010
5171
|
const findings = [];
|
|
@@ -5168,6 +5329,122 @@ async function newestSourceMtime(dirs) {
|
|
|
5168
5329
|
for (const dir of dirs) await walk(dir);
|
|
5169
5330
|
return newest;
|
|
5170
5331
|
}
|
|
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
|
+
}
|
|
5171
5448
|
async function findJayFiles(dir) {
|
|
5172
5449
|
return await glob(`${dir}/**/*${JAY_EXTENSION$1}`);
|
|
5173
5450
|
}
|
|
@@ -5369,6 +5646,17 @@ function resolveContractTag(contract, tagPath) {
|
|
|
5369
5646
|
}
|
|
5370
5647
|
return void 0;
|
|
5371
5648
|
}
|
|
5649
|
+
function checkJsonLdScripts(jayHtml) {
|
|
5650
|
+
const errors = [];
|
|
5651
|
+
for (const script of jayHtml.scripts ?? []) {
|
|
5652
|
+
if (script.attributes?.type !== "application/ld+json") continue;
|
|
5653
|
+
const err = jsonLdBodyError(script.inline);
|
|
5654
|
+
if (err) {
|
|
5655
|
+
errors.push(`<script type="application/ld+json">: ${err} See designer/seo-guide.md.`);
|
|
5656
|
+
}
|
|
5657
|
+
}
|
|
5658
|
+
return errors;
|
|
5659
|
+
}
|
|
5372
5660
|
function checkRefElementTypes(jayHtml, file) {
|
|
5373
5661
|
const imports = jayHtml.headlessImports;
|
|
5374
5662
|
const keyMap = /* @__PURE__ */ new Map();
|
|
@@ -6296,14 +6584,12 @@ async function validateJayFiles(options = {}) {
|
|
|
6296
6584
|
const reuseByTemplate = /* @__PURE__ */ new Map();
|
|
6297
6585
|
const cataloguedContractFiles = /* @__PURE__ */ new Set();
|
|
6298
6586
|
const pageJayHtmlFiles = await findJayFiles(scanDir);
|
|
6299
|
-
const componentJayHtmlFiles = await findJayFiles(componentsDir).catch(() => []);
|
|
6587
|
+
const componentJayHtmlFiles = options.compileOnly ? [] : await findJayFiles(componentsDir).catch(() => []);
|
|
6300
6588
|
const jayHtmlFiles = [...pageJayHtmlFiles, ...componentJayHtmlFiles];
|
|
6301
6589
|
const componentJayHtmlFileSet = new Set(componentJayHtmlFiles);
|
|
6302
|
-
const componentContractFiles = await findContractFiles(componentsDir).catch(
|
|
6303
|
-
() => []
|
|
6304
|
-
);
|
|
6590
|
+
const componentContractFiles = options.compileOnly ? [] : await findContractFiles(componentsDir).catch(() => []);
|
|
6305
6591
|
for (const cf of componentContractFiles) cataloguedContractFiles.add(cf);
|
|
6306
|
-
const contractFiles = [...await findContractFiles(scanDir), ...componentContractFiles];
|
|
6592
|
+
const contractFiles = options.compileOnly ? [] : [...await findContractFiles(scanDir), ...componentContractFiles];
|
|
6307
6593
|
const contractNameByFile = /* @__PURE__ */ new Map();
|
|
6308
6594
|
if (options.verbose) {
|
|
6309
6595
|
getLogger().info(chalk.gray(`Scanning directory: ${scanDir}`));
|
|
@@ -6370,6 +6656,35 @@ async function validateJayFiles(options = {}) {
|
|
|
6370
6656
|
continue;
|
|
6371
6657
|
}
|
|
6372
6658
|
parsedFiles.push({ relativePath, parsed: parsedFile.val });
|
|
6659
|
+
if (options.compileOnly) {
|
|
6660
|
+
const generatedFile2 = generateElementFile(
|
|
6661
|
+
parsedFile.val,
|
|
6662
|
+
RuntimeMode$1.MainTrusted,
|
|
6663
|
+
GenerateTarget.jay
|
|
6664
|
+
);
|
|
6665
|
+
for (const validation of generatedFile2.validations) {
|
|
6666
|
+
errors.push({
|
|
6667
|
+
file: relativePath,
|
|
6668
|
+
message: validation,
|
|
6669
|
+
stage: "generate"
|
|
6670
|
+
});
|
|
6671
|
+
}
|
|
6672
|
+
const serverElementFile2 = generateServerElementFile(parsedFile.val);
|
|
6673
|
+
for (const validation of serverElementFile2.validations) {
|
|
6674
|
+
errors.push({
|
|
6675
|
+
file: relativePath,
|
|
6676
|
+
message: `[SSR] ${validation}`,
|
|
6677
|
+
stage: "generate"
|
|
6678
|
+
});
|
|
6679
|
+
}
|
|
6680
|
+
if (options.verbose) {
|
|
6681
|
+
const failed = generatedFile2.validations.length > 0 || serverElementFile2.validations.length > 0;
|
|
6682
|
+
getLogger().info(
|
|
6683
|
+
failed ? chalk.red(`❌ ${relativePath}`) : chalk.green(`✓ ${relativePath}`)
|
|
6684
|
+
);
|
|
6685
|
+
}
|
|
6686
|
+
continue;
|
|
6687
|
+
}
|
|
6373
6688
|
if (content.includes("application/jay-params")) {
|
|
6374
6689
|
errors.push({
|
|
6375
6690
|
file: relativePath,
|
|
@@ -6461,6 +6776,9 @@ async function validateJayFiles(options = {}) {
|
|
|
6461
6776
|
suggestion: finding.suggestion
|
|
6462
6777
|
});
|
|
6463
6778
|
}
|
|
6779
|
+
for (const message of checkJsonLdScripts(parsedFile.val)) {
|
|
6780
|
+
errors.push({ file: relativePath, message, stage: "generate" });
|
|
6781
|
+
}
|
|
6464
6782
|
regionsSeen += collectRegionElements(parsedFile.val.body).length;
|
|
6465
6783
|
templateImportsSeen += parsedFile.val.headlessImports.filter(
|
|
6466
6784
|
(imp) => imp.template
|
|
@@ -6588,6 +6906,24 @@ async function validateJayFiles(options = {}) {
|
|
|
6588
6906
|
}
|
|
6589
6907
|
}
|
|
6590
6908
|
}
|
|
6909
|
+
if (options.compileOnly) {
|
|
6910
|
+
return {
|
|
6911
|
+
valid: errors.length === 0,
|
|
6912
|
+
jayHtmlFilesScanned: jayHtmlFiles.length,
|
|
6913
|
+
contractFilesScanned: contractFiles.length,
|
|
6914
|
+
errors,
|
|
6915
|
+
warnings,
|
|
6916
|
+
coverage,
|
|
6917
|
+
pluginValidators: [],
|
|
6918
|
+
designSystemCoverage,
|
|
6919
|
+
designSystemReuse: {
|
|
6920
|
+
perTemplate: {},
|
|
6921
|
+
catalogued: 0,
|
|
6922
|
+
reusedMoreThanOnce: 0,
|
|
6923
|
+
reusedOfCatalogued: 0
|
|
6924
|
+
}
|
|
6925
|
+
};
|
|
6926
|
+
}
|
|
6591
6927
|
const robotsTxtPath = path$1.resolve(projectRoot, "public/robots.txt");
|
|
6592
6928
|
if (!fs$1.existsSync(robotsTxtPath)) {
|
|
6593
6929
|
warnings.push({
|
|
@@ -6603,6 +6939,60 @@ async function validateJayFiles(options = {}) {
|
|
|
6603
6939
|
suggestion: "Add to .jay config:\n site:\n baseUrl: https://your-domain.com"
|
|
6604
6940
|
});
|
|
6605
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
|
+
const allowNoErrorPages = config.validations?.["allow-no-error-pages"] === true;
|
|
6978
|
+
if (!allowNoErrorPages) {
|
|
6979
|
+
const scanResult = await scanRoutes(scanDir, {
|
|
6980
|
+
jayHtmlFilename: "page.jay-html",
|
|
6981
|
+
compFilename: "page.ts"
|
|
6982
|
+
});
|
|
6983
|
+
const errorPages = scanResult.errorPages ?? [];
|
|
6984
|
+
const tokens = new Set(errorPages.map((ep) => ep.token));
|
|
6985
|
+
const missing = [];
|
|
6986
|
+
if (!tokens.has("404") && !tokens.has("4xx")) missing.push("404");
|
|
6987
|
+
if (!tokens.has("5xx")) missing.push("5xx");
|
|
6988
|
+
if (missing.length > 0) {
|
|
6989
|
+
warnings.push({
|
|
6990
|
+
file: "src/pages",
|
|
6991
|
+
message: `No branded error page for ${missing.join(" and ")} — these responses fall back to plain text instead of your site's layout.`,
|
|
6992
|
+
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."
|
|
6993
|
+
});
|
|
6994
|
+
}
|
|
6995
|
+
}
|
|
6606
6996
|
const allowNoTemplate = /* @__PURE__ */ new Set();
|
|
6607
6997
|
let allowNoDesignSystem = false;
|
|
6608
6998
|
for (const { parsed } of parsedFiles) {
|
|
@@ -6748,6 +7138,7 @@ async function runBuildOutputValidation(args) {
|
|
|
6748
7138
|
baseUrl
|
|
6749
7139
|
})
|
|
6750
7140
|
);
|
|
7141
|
+
findings.push(...checkBuildJsonLd(loaded.manifest));
|
|
6751
7142
|
for (const instance of oracle.instances) {
|
|
6752
7143
|
const slowViewState = await loadSlowViewState(instance.cacheAbsPath);
|
|
6753
7144
|
findings.push(
|
|
@@ -6766,6 +7157,8 @@ async function runBuildOutputValidation(args) {
|
|
|
6766
7157
|
slowViewState
|
|
6767
7158
|
})
|
|
6768
7159
|
);
|
|
7160
|
+
const carryForward = await loadCarryForward(instance.cacheAbsPath);
|
|
7161
|
+
findings.push(...checkInstanceJsonLd({ instanceUrl: instance.url, carryForward }));
|
|
6769
7162
|
}
|
|
6770
7163
|
for (const finding of findings) {
|
|
6771
7164
|
if (finding.severity === "error") {
|
|
@@ -7011,7 +7404,8 @@ async function runValidate(scanPath, options) {
|
|
|
7011
7404
|
verbose: options.verbose,
|
|
7012
7405
|
json: options.json,
|
|
7013
7406
|
tier2: options.tier2,
|
|
7014
|
-
buildDir: options.buildDir
|
|
7407
|
+
buildDir: options.buildDir,
|
|
7408
|
+
compileOnly: options.compileOnly
|
|
7015
7409
|
});
|
|
7016
7410
|
printJayValidationResult(result, options);
|
|
7017
7411
|
if (!result.valid) {
|
|
@@ -7092,12 +7486,6 @@ function printPluginValidationResult(result, verbose) {
|
|
|
7092
7486
|
logger.important(chalk.red(`${result.errors.length} errors found.`));
|
|
7093
7487
|
}
|
|
7094
7488
|
}
|
|
7095
|
-
const runValidate$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
7096
|
-
__proto__: null,
|
|
7097
|
-
runValidate,
|
|
7098
|
-
runValidatePlugin,
|
|
7099
|
-
surfaceValidationIssues
|
|
7100
|
-
}, Symbol.toStringTag, { value: "Module" }));
|
|
7101
7489
|
async function runDev(projectPath, options) {
|
|
7102
7490
|
const logLevel = options.quiet ? "silent" : options.verbose ? "verbose" : "info";
|
|
7103
7491
|
setDevLogger(createDevLogger(logLevel));
|
|
@@ -7117,10 +7505,12 @@ async function resolveProductionContext(projectPath, versionOverride) {
|
|
|
7117
7505
|
const jayConfigPath = path__default.join(resolvedPath, ".jay");
|
|
7118
7506
|
let pagesBase = "./src/pages";
|
|
7119
7507
|
let siteBaseUrl;
|
|
7508
|
+
let site;
|
|
7120
7509
|
try {
|
|
7121
7510
|
const jayConfig = YAML.parse(await fs$2.readFile(jayConfigPath, "utf-8"));
|
|
7122
7511
|
pagesBase = jayConfig?.devServer?.pagesBase || pagesBase;
|
|
7123
7512
|
siteBaseUrl = jayConfig?.site?.baseUrl;
|
|
7513
|
+
site = jayConfig?.site;
|
|
7124
7514
|
} catch {
|
|
7125
7515
|
}
|
|
7126
7516
|
const version = versionOverride || await resolveVersionFromPackageJson(resolvedPath);
|
|
@@ -7130,7 +7520,8 @@ async function resolveProductionContext(projectPath, versionOverride) {
|
|
|
7130
7520
|
buildRoot: path__default.join(resolvedPath, "build"),
|
|
7131
7521
|
version,
|
|
7132
7522
|
tsConfigFilePath: path__default.join(resolvedPath, "tsconfig.json"),
|
|
7133
|
-
siteBaseUrl
|
|
7523
|
+
siteBaseUrl,
|
|
7524
|
+
site
|
|
7134
7525
|
};
|
|
7135
7526
|
}
|
|
7136
7527
|
async function resolveVersionFromPackageJson(projectRoot) {
|
|
@@ -7151,14 +7542,13 @@ function initLogger(verbose) {
|
|
|
7151
7542
|
}
|
|
7152
7543
|
async function runBuild(projectPath, options) {
|
|
7153
7544
|
initLogger(options.verbose);
|
|
7154
|
-
const
|
|
7155
|
-
const valid = await surfaceValidationIssues2(projectPath, { verbose: options.verbose });
|
|
7545
|
+
const valid = await surfaceValidationIssues(projectPath, { verbose: options.verbose });
|
|
7156
7546
|
if (!valid) {
|
|
7157
7547
|
process.exit(1);
|
|
7158
7548
|
}
|
|
7159
7549
|
const ctx = await resolveProductionContext(projectPath, options.version);
|
|
7160
7550
|
await generateDefinitionFiles(ctx);
|
|
7161
|
-
const { buildVersion } = await import("./index-
|
|
7551
|
+
const { buildVersion } = await import("./index-B5oavmfA.js");
|
|
7162
7552
|
await buildVersion({
|
|
7163
7553
|
version: ctx.version,
|
|
7164
7554
|
projectRoot: ctx.resolvedPath,
|
|
@@ -7167,20 +7557,19 @@ async function runBuild(projectPath, options) {
|
|
|
7167
7557
|
concurrency: 4,
|
|
7168
7558
|
tsConfigFilePath: ctx.tsConfigFilePath,
|
|
7169
7559
|
minify: options.minify,
|
|
7170
|
-
siteBaseUrl: ctx.siteBaseUrl
|
|
7560
|
+
siteBaseUrl: ctx.siteBaseUrl,
|
|
7561
|
+
site: ctx.site
|
|
7171
7562
|
});
|
|
7172
7563
|
}
|
|
7173
7564
|
async function generateDefinitionFiles(ctx) {
|
|
7174
7565
|
const { glob: glob2 } = await import("glob");
|
|
7175
|
-
const { generatePageDefinitionFiles: generatePageDefinitionFiles2 } = await Promise.resolve().then(() => generatePageDefinitionFiles$1);
|
|
7176
|
-
const { generateContractDefinitionFiles: generateContractDefinitionFiles2 } = await Promise.resolve().then(() => generateContractDefinitionFiles$1);
|
|
7177
7566
|
const jayHtmlPaths = await glob2("**/page.jay-html", {
|
|
7178
7567
|
cwd: ctx.pagesRoot,
|
|
7179
7568
|
absolute: true,
|
|
7180
7569
|
ignore: ["**/node_modules/**", "**/build/**", "**/dist/**"]
|
|
7181
7570
|
});
|
|
7182
|
-
await
|
|
7183
|
-
await
|
|
7571
|
+
await generatePageDefinitionFiles(jayHtmlPaths, ctx.tsConfigFilePath, ctx.resolvedPath);
|
|
7572
|
+
await generateContractDefinitionFiles(path__default.resolve(ctx.resolvedPath, "src"));
|
|
7184
7573
|
}
|
|
7185
7574
|
async function runServe(projectPath, options) {
|
|
7186
7575
|
initLogger(options.verbose);
|
|
@@ -7234,7 +7623,8 @@ async function runRebuild(projectPath, options) {
|
|
|
7234
7623
|
version: ctx.version,
|
|
7235
7624
|
target,
|
|
7236
7625
|
tsConfigFilePath: ctx.tsConfigFilePath,
|
|
7237
|
-
siteBaseUrl: ctx.siteBaseUrl
|
|
7626
|
+
siteBaseUrl: ctx.siteBaseUrl,
|
|
7627
|
+
site: ctx.site
|
|
7238
7628
|
});
|
|
7239
7629
|
if (result.errors.length > 0) {
|
|
7240
7630
|
for (const err of result.errors) {
|
|
@@ -8286,9 +8676,16 @@ program.command("validate").description("Validate all .jay-html and .jay-contrac
|
|
|
8286
8676
|
).addOption(new Option("--t2", "Alias for --tier-2").hideHelp()).option(
|
|
8287
8677
|
"--build-dir <dir>",
|
|
8288
8678
|
"Build backend dir or build root for Tier 2 (default: highest build/v*)"
|
|
8679
|
+
).option(
|
|
8680
|
+
"--compile-only",
|
|
8681
|
+
"Parse and compile .jay-html only (no project rules — for plugin route source checks)"
|
|
8289
8682
|
).action(async (options) => {
|
|
8290
8683
|
try {
|
|
8291
|
-
await runValidate(options.path, {
|
|
8684
|
+
await runValidate(options.path, {
|
|
8685
|
+
...options,
|
|
8686
|
+
tier2: options.tier2 || options.t2,
|
|
8687
|
+
compileOnly: options.compileOnly
|
|
8688
|
+
});
|
|
8292
8689
|
} catch (error) {
|
|
8293
8690
|
if (options.json) {
|
|
8294
8691
|
getLogger().important(
|
|
@@ -8350,6 +8747,7 @@ export {
|
|
|
8350
8747
|
listContracts,
|
|
8351
8748
|
loadConfig,
|
|
8352
8749
|
materializeContracts,
|
|
8750
|
+
setWellKnownHeaders,
|
|
8353
8751
|
startDevServer,
|
|
8354
8752
|
updateConfig
|
|
8355
8753
|
};
|