@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/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
- const generateContractDefinitionFiles$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
199
- __proto__: null,
200
- generateContractDefinitionFiles
201
- }, Symbol.toStringTag, { value: "Module" }));
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
- app.get(route.path, route.handler);
241
+ pageRouter.get(route.path, route.handler);
241
242
  });
242
243
  service.attachRouteRegistrar((added) => {
243
244
  added.forEach((route) => {
244
- app.get(route.path, route.handler);
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: `Link points to the page's own route "${target}" — a self-link goes nowhere.`,
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 "${raw}" — no page or public asset produces the URL "${target}".`,
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 { surfaceValidationIssues: surfaceValidationIssues2 } = await Promise.resolve().then(() => runValidate$1);
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-BQbkAEhi.js");
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 generatePageDefinitionFiles2(jayHtmlPaths, ctx.tsConfigFilePath, ctx.resolvedPath);
7183
- await generateContractDefinitionFiles2(path__default.resolve(ctx.resolvedPath, "src"));
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, { ...options, tier2: options.tier2 || options.t2 });
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
  };