@jay-framework/jay-stack-cli 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -7,8 +7,8 @@ import path from "path";
7
7
  import fs, { promises } from "fs";
8
8
  import YAML from "yaml";
9
9
  import { getLogger, setDevLogger, createDevLogger } from "@jay-framework/logger";
10
- import { JAY_IMPORT_RESOLVER, parseJayFile, generateElementDefinitionFile, parseContract, generateElementFile, generateServerElementFile, htmlElementTagNameMap, loadLinkedContract, getLinkedContractDir } from "@jay-framework/compiler-jay-html";
11
- import { scanPlugins, listContracts, materializeContracts, SetupNeedsAnswerError, discoverPluginsWithSetup, discoverPluginsWithInit, sortPluginsByDependencies, executePluginSetup, executePluginServerInits, runInitCallbacks } from "@jay-framework/stack-server-runtime";
10
+ import { parseJayFile, JAY_IMPORT_RESOLVER, generateElementDefinitionFile, parseContract, generateElementFile, generateServerElementFile, htmlElementTagNameMap, loadLinkedContract, getLinkedContractDir } from "@jay-framework/compiler-jay-html";
11
+ import { scanPlugins, listContracts, materializeContracts, SetupNeedsAnswerError, discoverPluginsWithSetup, sortPluginsByDependencies, discoverPluginsWithInit, executePluginSetup, executePluginServerInits, runInitCallbacks } from "@jay-framework/stack-server-runtime";
12
12
  import { listContracts as listContracts2, materializeContracts as materializeContracts2 } from "@jay-framework/stack-server-runtime";
13
13
  import { Command } from "commander";
14
14
  import chalk from "chalk";
@@ -19,7 +19,7 @@ import { createRequire } from "module";
19
19
  import { glob } from "glob";
20
20
  import fsSync from "node:fs";
21
21
  import { fileURLToPath } from "node:url";
22
- import { input, confirm, select } from "@inquirer/prompts";
22
+ import { select, confirm, input } from "@inquirer/prompts";
23
23
  const DEFAULT_CONFIG = {
24
24
  devServer: {
25
25
  portRange: [3e3, 3100],
@@ -41,7 +41,8 @@ function loadConfig() {
41
41
  devServer: {
42
42
  ...DEFAULT_CONFIG.devServer,
43
43
  ...userConfig.devServer
44
- }
44
+ },
45
+ site: userConfig.site
45
46
  };
46
47
  } catch (error) {
47
48
  getLogger().warn(`Failed to parse .jay YAML config file, using defaults: ${error}`);
@@ -56,7 +57,8 @@ function getConfigWithDefaults(config) {
56
57
  componentsBase: config.devServer?.componentsBase || DEFAULT_CONFIG.devServer.componentsBase,
57
58
  publicFolder: config.devServer?.publicFolder || DEFAULT_CONFIG.devServer.publicFolder,
58
59
  configBase: config.devServer?.configBase || DEFAULT_CONFIG.devServer.configBase
59
- }
60
+ },
61
+ site: config.site
60
62
  };
61
63
  }
62
64
  function updateConfig(updates) {
@@ -69,6 +71,10 @@ function updateConfig(updates) {
69
71
  devServer: {
70
72
  ...existingConfig.devServer,
71
73
  ...updates.devServer
74
+ },
75
+ site: {
76
+ ...existingConfig.site,
77
+ ...updates.site
72
78
  }
73
79
  };
74
80
  const yamlContent = YAML.stringify(updatedConfig, { indent: 2 });
@@ -146,6 +152,13 @@ async function startDevServer(options = {}) {
146
152
  });
147
153
  app.use(server);
148
154
  const publicPath = path.resolve(resolvedConfig.devServer.publicFolder);
155
+ if (!fs.existsSync(path.join(publicPath, "sitemap.xml"))) {
156
+ app.get("/sitemap.xml", (_req, res) => {
157
+ res.type("application/xml").send(
158
+ '<?xml version="1.0" encoding="UTF-8"?>\n<!-- Sitemap is generated by the production server from the route manifest. -->\n<!-- Run jay-stack build && jay-stack serve to see the full sitemap. -->\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" />\n'
159
+ );
160
+ });
161
+ }
149
162
  if (fs.existsSync(publicPath)) {
150
163
  app.use(express.static(publicPath));
151
164
  } else {
@@ -264,9 +277,11 @@ async function resolveProductionContext(projectPath, versionOverride) {
264
277
  const resolvedPath = path$1.resolve(projectPath || process.cwd());
265
278
  const jayConfigPath = path$1.join(resolvedPath, ".jay");
266
279
  let pagesBase = "./src/pages";
280
+ let siteBaseUrl;
267
281
  try {
268
282
  const jayConfig = YAML.parse(await fs$1.readFile(jayConfigPath, "utf-8"));
269
283
  pagesBase = jayConfig?.devServer?.pagesBase || pagesBase;
284
+ siteBaseUrl = jayConfig?.site?.baseUrl;
270
285
  } catch {
271
286
  }
272
287
  const version = versionOverride || await resolveVersionFromPackageJson(resolvedPath);
@@ -275,7 +290,8 @@ async function resolveProductionContext(projectPath, versionOverride) {
275
290
  pagesRoot: path$1.resolve(resolvedPath, pagesBase),
276
291
  buildRoot: path$1.join(resolvedPath, "build"),
277
292
  version,
278
- tsConfigFilePath: path$1.join(resolvedPath, "tsconfig.json")
293
+ tsConfigFilePath: path$1.join(resolvedPath, "tsconfig.json"),
294
+ siteBaseUrl
279
295
  };
280
296
  }
281
297
  async function resolveVersionFromPackageJson(projectRoot) {
@@ -305,7 +321,8 @@ async function runBuild(projectPath, options) {
305
321
  buildRoot: ctx.buildRoot,
306
322
  concurrency: 4,
307
323
  tsConfigFilePath: ctx.tsConfigFilePath,
308
- minify: options.minify
324
+ minify: options.minify,
325
+ siteBaseUrl: ctx.siteBaseUrl
309
326
  });
310
327
  }
311
328
  async function runServe(projectPath, options) {
@@ -319,7 +336,8 @@ async function runServe(projectPath, options) {
319
336
  port: parseInt(options.port, 10),
320
337
  projectRoot: ctx.resolvedPath,
321
338
  pagesRoot: ctx.pagesRoot,
322
- tsConfigFilePath: ctx.tsConfigFilePath
339
+ tsConfigFilePath: ctx.tsConfigFilePath,
340
+ siteBaseUrl: ctx.siteBaseUrl
323
341
  });
324
342
  } else {
325
343
  const { startMainServer } = await import("@jay-framework/production-server");
@@ -358,7 +376,8 @@ async function runRebuild(projectPath, options) {
358
376
  buildRoot: ctx.buildRoot,
359
377
  version: ctx.version,
360
378
  target,
361
- tsConfigFilePath: ctx.tsConfigFilePath
379
+ tsConfigFilePath: ctx.tsConfigFilePath,
380
+ siteBaseUrl: ctx.siteBaseUrl
362
381
  });
363
382
  if (result.errors.length > 0) {
364
383
  for (const err of result.errors) {
@@ -464,17 +483,13 @@ function collectLocalInterfaces(sourceFile) {
464
483
  function collectContractImportedTypes(sourceFile) {
465
484
  const contractTypes = /* @__PURE__ */ new Set();
466
485
  for (const statement of sourceFile.statements) {
467
- if (!u.isImportDeclaration(statement))
468
- continue;
486
+ if (!u.isImportDeclaration(statement)) continue;
469
487
  const moduleSpecifier = statement.moduleSpecifier;
470
- if (!u.isStringLiteral(moduleSpecifier))
471
- continue;
488
+ if (!u.isStringLiteral(moduleSpecifier)) continue;
472
489
  const modulePath = moduleSpecifier.text;
473
- if (!modulePath.includes(".jay-contract"))
474
- continue;
490
+ if (!modulePath.includes(".jay-contract")) continue;
475
491
  const importClause = statement.importClause;
476
- if (!importClause)
477
- continue;
492
+ if (!importClause) continue;
478
493
  const namedBindings = importClause.namedBindings;
479
494
  if (namedBindings && u.isNamedImports(namedBindings)) {
480
495
  for (const element of namedBindings.elements) {
@@ -510,8 +525,7 @@ function visitNode(node, result) {
510
525
  }
511
526
  if (node.arguments.length > 0) {
512
527
  const arg = node.arguments[0];
513
- if (u.isIdentifier(arg))
514
- ;
528
+ if (u.isIdentifier(arg)) ;
515
529
  }
516
530
  }
517
531
  }
@@ -533,10 +547,8 @@ function extractTypeNames(typeNode) {
533
547
  return [];
534
548
  }
535
549
  function checkPropsConsistency(propsTypeName, localInterfaces, contractImportedTypes, contract, contractName, contractPath, sourcePath, errors, warnings) {
536
- if (FRAMEWORK_PROP_TYPES.has(propsTypeName))
537
- return;
538
- if (contractImportedTypes.has(propsTypeName))
539
- return;
550
+ if (FRAMEWORK_PROP_TYPES.has(propsTypeName)) return;
551
+ if (contractImportedTypes.has(propsTypeName)) return;
540
552
  const prefix = `[${contractName}]`;
541
553
  const iface = localInterfaces.get(propsTypeName);
542
554
  if (!iface) {
@@ -551,8 +563,7 @@ function checkPropsConsistency(propsTypeName, localInterfaces, contractImportedT
551
563
  return;
552
564
  }
553
565
  const ownProperties = iface.properties;
554
- if (ownProperties.length === 0)
555
- return;
566
+ if (ownProperties.length === 0) return;
556
567
  if (!contract.props || contract.props.length === 0) {
557
568
  errors.push({
558
569
  type: "contract-invalid",
@@ -596,10 +607,8 @@ function checkParamsConsistency(paramsTypeNames, localInterfaces, contractImport
596
607
  suggestion: `Add a params section to the contract (e.g., params: { slug: string })`
597
608
  });
598
609
  for (const typeName of paramsTypeNames) {
599
- if (FRAMEWORK_PROP_TYPES.has(typeName))
600
- continue;
601
- if (contractImportedTypes.has(typeName))
602
- continue;
610
+ if (FRAMEWORK_PROP_TYPES.has(typeName)) continue;
611
+ if (contractImportedTypes.has(typeName)) continue;
603
612
  const iface = localInterfaces.get(typeName);
604
613
  if (iface) {
605
614
  const ownProps = iface.properties;
@@ -612,13 +621,10 @@ function checkParamsConsistency(paramsTypeNames, localInterfaces, contractImport
612
621
  return;
613
622
  }
614
623
  for (const typeName of paramsTypeNames) {
615
- if (FRAMEWORK_PROP_TYPES.has(typeName))
616
- continue;
617
- if (contractImportedTypes.has(typeName))
618
- continue;
624
+ if (FRAMEWORK_PROP_TYPES.has(typeName)) continue;
625
+ if (contractImportedTypes.has(typeName)) continue;
619
626
  const iface = localInterfaces.get(typeName);
620
- if (!iface)
621
- continue;
627
+ if (!iface) continue;
622
628
  const ownProperties = iface.properties;
623
629
  const contractParamNames = new Set(contract.params.map((p) => p.name));
624
630
  for (const prop of ownProperties) {
@@ -710,16 +716,13 @@ function requiredString(obj, field, itemPath, errors, code) {
710
716
  }
711
717
  function optionalString(obj, field) {
712
718
  const value = obj[field];
713
- if (value === void 0)
714
- return void 0;
715
- if (typeof value !== "string")
716
- return void 0;
719
+ if (value === void 0) return void 0;
720
+ if (typeof value !== "string") return void 0;
717
721
  const trimmed = value.trim();
718
722
  return trimmed.length > 0 ? trimmed : void 0;
719
723
  }
720
724
  function validateInteraction(raw, itemPath, errors) {
721
- if (raw === void 0)
722
- return void 0;
725
+ if (raw === void 0) return void 0;
723
726
  if (!isRecord$1(raw)) {
724
727
  errors.push({
725
728
  path: itemPath,
@@ -743,8 +746,7 @@ function validateInteraction(raw, itemPath, errors) {
743
746
  };
744
747
  }
745
748
  function validatePresentation(raw, itemPath, errors) {
746
- if (raw === void 0)
747
- return void 0;
749
+ if (raw === void 0) return void 0;
748
750
  if (!isRecord$1(raw)) {
749
751
  errors.push({
750
752
  path: itemPath,
@@ -764,14 +766,12 @@ function validatePresentation(raw, itemPath, errors) {
764
766
  }
765
767
  if (type === "image") {
766
768
  const src = requiredString(raw, "src", itemPath, errors, "presentation-missing-src");
767
- if (!src)
768
- return void 0;
769
+ if (!src) return void 0;
769
770
  return { type: "image", src };
770
771
  }
771
772
  if (type === "gif") {
772
773
  const src = requiredString(raw, "src", itemPath, errors, "presentation-missing-src");
773
- if (!src)
774
- return void 0;
774
+ if (!src) return void 0;
775
775
  return {
776
776
  type: "gif",
777
777
  src,
@@ -816,8 +816,7 @@ function validatePresentation(raw, itemPath, errors) {
816
816
  }
817
817
  const BROWSE_SIZES = /* @__PURE__ */ new Set(["large", "medium", "small"]);
818
818
  function validateBrowse(raw, itemPath, errors) {
819
- if (raw === void 0)
820
- return void 0;
819
+ if (raw === void 0) return void 0;
821
820
  if (!isRecord$1(raw)) {
822
821
  errors.push({
823
822
  path: itemPath,
@@ -841,8 +840,7 @@ function validateBrowse(raw, itemPath, errors) {
841
840
  return { size: sizeRaw };
842
841
  }
843
842
  function validateFolderPath(raw, itemPath, errors) {
844
- if (raw === void 0)
845
- return void 0;
843
+ if (raw === void 0) return void 0;
846
844
  if (!Array.isArray(raw)) {
847
845
  errors.push({
848
846
  path: `${itemPath}.folderPath`,
@@ -980,8 +978,7 @@ function validateAddMenuCatalogFile(raw, sourcePath) {
980
978
  raw.items.forEach((entry, index) => {
981
979
  const result = validateAddMenuItem(entry, `${sourcePath}.items[${index}]`);
982
980
  errors.push(...result.errors);
983
- if (result.item)
984
- items.push(result.item);
981
+ if (result.item) items.push(result.item);
985
982
  });
986
983
  if (items.length === 0 && errors.length > 0) {
987
984
  return { file: null, errors };
@@ -989,11 +986,9 @@ function validateAddMenuCatalogFile(raw, sourcePath) {
989
986
  return { file: { items }, errors };
990
987
  }
991
988
  function normalizeAddMenuPresentation(item) {
992
- if (item.presentation)
993
- return item.presentation;
989
+ if (item.presentation) return item.presentation;
994
990
  const thumbnail = item.thumbnail?.trim();
995
- if (!thumbnail)
996
- return void 0;
991
+ if (!thumbnail) return void 0;
997
992
  if (/\.gif$/i.test(thumbnail)) {
998
993
  return { type: "gif", src: thumbnail };
999
994
  }
@@ -1004,15 +999,12 @@ function normalizeAddMenuBrowseSize(item) {
1004
999
  }
1005
1000
  function hasSingleRootDiv(html) {
1006
1001
  const trimmed = html.trim();
1007
- if (!trimmed.startsWith("<div"))
1008
- return false;
1002
+ if (!trimmed.startsWith("<div")) return false;
1009
1003
  const openMatch = trimmed.match(/^<div\b[^>]*>/i);
1010
- if (!openMatch)
1011
- return false;
1004
+ if (!openMatch) return false;
1012
1005
  const afterOpen = trimmed.slice(openMatch[0].length);
1013
1006
  const closeIdx = afterOpen.lastIndexOf("</div>");
1014
- if (closeIdx < 0)
1015
- return false;
1007
+ if (closeIdx < 0) return false;
1016
1008
  const tail = afterOpen.slice(closeIdx + "</div>".length).trim();
1017
1009
  return tail.length === 0;
1018
1010
  }
@@ -1088,8 +1080,7 @@ function lintHtmlFragment(item, sourcePath) {
1088
1080
  }
1089
1081
  function lintGifPoster(item, sourcePath) {
1090
1082
  const presentation = normalizeAddMenuPresentation(item);
1091
- if (presentation?.type !== "gif" || presentation.poster?.trim())
1092
- return [];
1083
+ if (presentation?.type !== "gif" || presentation.poster?.trim()) return [];
1093
1084
  return [
1094
1085
  catalogWarning(
1095
1086
  "gif-missing-poster",
@@ -1100,10 +1091,8 @@ function lintGifPoster(item, sourcePath) {
1100
1091
  ];
1101
1092
  }
1102
1093
  function lintBrowseLargeWithoutPresentation(item, sourcePath) {
1103
- if (normalizeAddMenuBrowseSize(item) !== "large")
1104
- return [];
1105
- if (normalizeAddMenuPresentation(item))
1106
- return [];
1094
+ if (normalizeAddMenuBrowseSize(item) !== "large") return [];
1095
+ if (normalizeAddMenuPresentation(item)) return [];
1107
1096
  return [
1108
1097
  catalogWarning(
1109
1098
  "browse-large-without-presentation",
@@ -1176,8 +1165,7 @@ function resolveModulePath$1(basePath) {
1176
1165
  return void 0;
1177
1166
  }
1178
1167
  function collectTypeScriptFiles(dir, depth = 0) {
1179
- if (depth > 4)
1180
- return [];
1168
+ if (depth > 4) return [];
1181
1169
  const entries = fs.readdirSync(dir, { withFileTypes: true });
1182
1170
  const files = [];
1183
1171
  for (const entry of entries) {
@@ -1196,8 +1184,7 @@ function resolveHandlerSourceFile(pluginPath, handlerRef, isNpmPackage) {
1196
1184
  }
1197
1185
  const searchRoots = isNpmPackage ? [path.join(pluginPath, "lib"), path.join(pluginPath, "dist")] : [pluginPath];
1198
1186
  for (const root of searchRoots) {
1199
- if (!fs.existsSync(root))
1200
- continue;
1187
+ if (!fs.existsSync(root)) continue;
1201
1188
  for (const file of collectTypeScriptFiles(root)) {
1202
1189
  const content = fs.readFileSync(file, "utf-8");
1203
1190
  const definesHandler = new RegExp(
@@ -1217,8 +1204,7 @@ function resolveHandlerSourceFile(pluginPath, handlerRef, isNpmPackage) {
1217
1204
  if (reExportMatch) {
1218
1205
  const importSpec = reExportMatch[1].replace(/\.js$/, "");
1219
1206
  const resolved = resolveModulePath$1(path.resolve(path.dirname(file), importSpec));
1220
- if (resolved)
1221
- return resolved;
1207
+ if (resolved) return resolved;
1222
1208
  }
1223
1209
  }
1224
1210
  }
@@ -1228,8 +1214,7 @@ function extractBalancedBlock(source, openBraceIndex) {
1228
1214
  let depth = 0;
1229
1215
  for (let index = openBraceIndex; index < source.length; index++) {
1230
1216
  const char = source[index];
1231
- if (char === "{")
1232
- depth++;
1217
+ if (char === "{") depth++;
1233
1218
  else if (char === "}") {
1234
1219
  depth--;
1235
1220
  if (depth === 0) {
@@ -1249,8 +1234,7 @@ function findFunctionBodyOpenBrace(source, searchFrom) {
1249
1234
  if (parenMatch?.index !== void 0) {
1250
1235
  candidates.push(searchFrom + parenMatch.index + parenMatch[0].length - 1);
1251
1236
  }
1252
- if (candidates.length === 0)
1253
- return -1;
1237
+ if (candidates.length === 0) return -1;
1254
1238
  return Math.min(...candidates);
1255
1239
  }
1256
1240
  function extractFunctionBody(source, functionName) {
@@ -1267,11 +1251,9 @@ function extractFunctionBody(source, functionName) {
1267
1251
  ];
1268
1252
  for (const pattern of patterns) {
1269
1253
  const match = pattern.exec(source);
1270
- if (!match)
1271
- continue;
1254
+ if (!match) continue;
1272
1255
  const braceIndex = findFunctionBodyOpenBrace(source, match.index);
1273
- if (braceIndex === -1)
1274
- continue;
1256
+ if (braceIndex === -1) continue;
1275
1257
  return extractBalancedBlock(source, braceIndex);
1276
1258
  }
1277
1259
  return null;
@@ -1284,11 +1266,9 @@ function extractDefaultExportFunctionBody(source) {
1284
1266
  ];
1285
1267
  for (const pattern of patterns) {
1286
1268
  const match = pattern.exec(source);
1287
- if (!match)
1288
- continue;
1269
+ if (!match) continue;
1289
1270
  const braceIndex = findFunctionBodyOpenBrace(source, match.index);
1290
- if (braceIndex === -1)
1291
- continue;
1271
+ if (braceIndex === -1) continue;
1292
1272
  return extractBalancedBlock(source, braceIndex);
1293
1273
  }
1294
1274
  return null;
@@ -1298,8 +1278,7 @@ function handlerBodyWritesAddMenuCatalog(body) {
1298
1278
  }
1299
1279
  function resolveSetupHandlerFunctionBody(pluginPath, handlerRef, isNpmPackage) {
1300
1280
  const sourceFile = resolveHandlerSourceFile(pluginPath, handlerRef, isNpmPackage);
1301
- if (!sourceFile)
1302
- return null;
1281
+ if (!sourceFile) return null;
1303
1282
  const source = fs.readFileSync(sourceFile, "utf-8");
1304
1283
  if (isRelativeHandlerRef(handlerRef)) {
1305
1284
  return extractDefaultExportFunctionBody(source) ?? extractFunctionBody(source, "setup") ?? extractFunctionBody(source, handlerRef);
@@ -1307,8 +1286,7 @@ function resolveSetupHandlerFunctionBody(pluginPath, handlerRef, isNpmPackage) {
1307
1286
  return extractFunctionBody(source, handlerRef);
1308
1287
  }
1309
1288
  function suggestionForCode(code) {
1310
- if (!code)
1311
- return `See ${CONTRIBUTOR_GUIDE} for schema and validation rules`;
1289
+ if (!code) return `See ${CONTRIBUTOR_GUIDE} for schema and validation rules`;
1312
1290
  return ADD_MENU_VALIDATION_SUGGESTIONS[code] ?? `See ${CONTRIBUTOR_GUIDE} for schema and validation rules`;
1313
1291
  }
1314
1292
  function mapSchemaError$1(error, catalogPath) {
@@ -1338,8 +1316,7 @@ function pluginShipsAddMenuCatalog(context) {
1338
1316
  );
1339
1317
  }
1340
1318
  function validateAddMenuAgentKitHandler(context, result) {
1341
- if (!pluginShipsAddMenuCatalog(context))
1342
- return;
1319
+ if (!pluginShipsAddMenuCatalog(context)) return;
1343
1320
  const agentKitHandler = context.manifest.agentkit;
1344
1321
  if (!agentKitHandler) {
1345
1322
  result.warnings.push({
@@ -1351,15 +1328,13 @@ function validateAddMenuAgentKitHandler(context, result) {
1351
1328
  });
1352
1329
  }
1353
1330
  const setupHandler = typeof context.manifest.setup === "string" ? context.manifest.setup : void 0;
1354
- if (!setupHandler)
1355
- return;
1331
+ if (!setupHandler) return;
1356
1332
  const setupBody = resolveSetupHandlerFunctionBody(
1357
1333
  context.pluginPath,
1358
1334
  setupHandler,
1359
1335
  context.isNpmPackage
1360
1336
  );
1361
- if (!setupBody || !handlerBodyWritesAddMenuCatalog(setupBody))
1362
- return;
1337
+ if (!setupBody || !handlerBodyWritesAddMenuCatalog(setupBody)) return;
1363
1338
  result.warnings.push({
1364
1339
  type: "add-menu-catalog",
1365
1340
  code: "add-menu-legacy-setup-handler",
@@ -1401,8 +1376,7 @@ async function validateAddMenuCatalog(context, result) {
1401
1376
  validateAddMenuAgentKitHandler(context, result);
1402
1377
  for (const relPath of ADD_MENU_CATALOG_REL_PATHS) {
1403
1378
  const catalogPath = path.join(context.pluginPath, relPath);
1404
- if (!fs.existsSync(catalogPath))
1405
- continue;
1379
+ if (!fs.existsSync(catalogPath)) continue;
1406
1380
  await validateAddMenuCatalogFileAtPath(catalogPath, relPath, result);
1407
1381
  }
1408
1382
  }
@@ -2026,16 +2000,14 @@ async function validateSchema(context, result) {
2026
2000
  }
2027
2001
  function checkExportExists(exportName, context) {
2028
2002
  const packageJsonPath = path.join(context.pluginPath, "package.json");
2029
- if (!fs.existsSync(packageJsonPath))
2030
- return true;
2003
+ if (!fs.existsSync(packageJsonPath)) return true;
2031
2004
  let mainPath;
2032
2005
  try {
2033
2006
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
2034
2007
  if (packageJson.exports?.["."]) {
2035
2008
  const entry = packageJson.exports["."];
2036
2009
  const entryPath = typeof entry === "string" ? entry : entry.default || entry.import;
2037
- if (entryPath)
2038
- mainPath = path.join(context.pluginPath, entryPath);
2010
+ if (entryPath) mainPath = path.join(context.pluginPath, entryPath);
2039
2011
  }
2040
2012
  if (!mainPath && packageJson.main) {
2041
2013
  mainPath = path.join(context.pluginPath, packageJson.main);
@@ -2043,8 +2015,7 @@ function checkExportExists(exportName, context) {
2043
2015
  } catch {
2044
2016
  return true;
2045
2017
  }
2046
- if (!mainPath || !fs.existsSync(mainPath))
2047
- return true;
2018
+ if (!mainPath || !fs.existsSync(mainPath)) return true;
2048
2019
  try {
2049
2020
  const content = fs.readFileSync(mainPath, "utf-8");
2050
2021
  const patterns = [
@@ -2104,8 +2075,7 @@ function resolveContractFile(contractSpec, context) {
2104
2075
  const resolvedPath = typeof exportValue === "string" ? exportValue : exportValue.default || exportValue.import || exportValue.require;
2105
2076
  if (resolvedPath) {
2106
2077
  const fullPath = path.join(context.pluginPath, resolvedPath);
2107
- if (fs.existsSync(fullPath))
2108
- return fullPath;
2078
+ if (fs.existsSync(fullPath)) return fullPath;
2109
2079
  }
2110
2080
  }
2111
2081
  }
@@ -2114,8 +2084,7 @@ function resolveContractFile(contractSpec, context) {
2114
2084
  }
2115
2085
  for (const dir of ["dist", "lib", ""]) {
2116
2086
  const candidate = path.join(context.pluginPath, dir, contractSpec);
2117
- if (fs.existsSync(candidate))
2118
- return candidate;
2087
+ if (fs.existsSync(candidate)) return candidate;
2119
2088
  }
2120
2089
  return void 0;
2121
2090
  } else {
@@ -2203,8 +2172,7 @@ function hasExportModifier(node) {
2203
2172
  function resolveModulePath(basePath) {
2204
2173
  for (const ext of ["", ".ts", ".js", "/index.ts", "/index.js"]) {
2205
2174
  const candidate = basePath + ext;
2206
- if (fs.existsSync(candidate))
2207
- return candidate;
2175
+ if (fs.existsSync(candidate)) return candidate;
2208
2176
  }
2209
2177
  return void 0;
2210
2178
  }
@@ -2214,10 +2182,8 @@ function resolveComponentSourcePath(componentName, context) {
2214
2182
  const entryFile = resolveModulePath(entryBase);
2215
2183
  const libEntryFile = !entryFile ? resolveModulePath(path.join(context.pluginPath, "lib", modulePath)) : void 0;
2216
2184
  const sourceEntry = entryFile || libEntryFile;
2217
- if (!sourceEntry)
2218
- return void 0;
2219
- if (!sourceEntry.endsWith(".ts"))
2220
- return void 0;
2185
+ if (!sourceEntry) return void 0;
2186
+ if (!sourceEntry.endsWith(".ts")) return void 0;
2221
2187
  let sourceCode;
2222
2188
  try {
2223
2189
  sourceCode = fs.readFileSync(sourceEntry, "utf-8");
@@ -2233,12 +2199,9 @@ function resolveComponentSourcePath(componentName, context) {
2233
2199
  );
2234
2200
  const starReexportModules = [];
2235
2201
  for (const statement of sourceFile.statements) {
2236
- if (!u.isExportDeclaration(statement))
2237
- continue;
2238
- if (!statement.moduleSpecifier)
2239
- continue;
2240
- if (!u.isStringLiteral(statement.moduleSpecifier))
2241
- continue;
2202
+ if (!u.isExportDeclaration(statement)) continue;
2203
+ if (!statement.moduleSpecifier) continue;
2204
+ if (!u.isStringLiteral(statement.moduleSpecifier)) continue;
2242
2205
  const moduleSpec = statement.moduleSpecifier.text;
2243
2206
  const exportClause = statement.exportClause;
2244
2207
  if (!exportClause) {
@@ -2256,12 +2219,10 @@ function resolveComponentSourcePath(componentName, context) {
2256
2219
  }
2257
2220
  }
2258
2221
  for (const moduleSpec of starReexportModules) {
2259
- if (!moduleSpec.startsWith("."))
2260
- continue;
2222
+ if (!moduleSpec.startsWith(".")) continue;
2261
2223
  const resolvedBase = path.resolve(path.dirname(sourceEntry), moduleSpec);
2262
2224
  const resolvedPath = resolveModulePath(resolvedBase);
2263
- if (!resolvedPath || !resolvedPath.endsWith(".ts"))
2264
- continue;
2225
+ if (!resolvedPath || !resolvedPath.endsWith(".ts")) continue;
2265
2226
  try {
2266
2227
  const modSource = fs.readFileSync(resolvedPath, "utf-8");
2267
2228
  const modFile = u.createSourceFile(
@@ -2294,16 +2255,12 @@ function resolveContractPath(contract, context) {
2294
2255
  }
2295
2256
  async function checkComponentContractConsistency(contract, context, result) {
2296
2257
  const componentName = contract.component;
2297
- if (!componentName)
2298
- return;
2258
+ if (!componentName) return;
2299
2259
  const sourcePath = resolveComponentSourcePath(componentName, context);
2300
- if (!sourcePath)
2301
- return;
2302
- if (!sourcePath.endsWith(".ts"))
2303
- return;
2260
+ if (!sourcePath) return;
2261
+ if (!sourcePath.endsWith(".ts")) return;
2304
2262
  const contractPath = resolveContractPath(contract, context);
2305
- if (!contractPath)
2306
- return;
2263
+ if (!contractPath) return;
2307
2264
  let contractContent;
2308
2265
  try {
2309
2266
  contractContent = await fs.promises.readFile(contractPath, "utf-8");
@@ -2311,8 +2268,7 @@ async function checkComponentContractConsistency(contract, context, result) {
2311
2268
  return;
2312
2269
  }
2313
2270
  const parsed = parseContract(contractContent, path.basename(contractPath));
2314
- if (parsed.validations.length > 0)
2315
- return;
2271
+ if (parsed.validations.length > 0) return;
2316
2272
  let sourceCode;
2317
2273
  try {
2318
2274
  sourceCode = await fs.promises.readFile(sourcePath, "utf-8");
@@ -2429,10 +2385,99 @@ async function validatePackageJson(context, result) {
2429
2385
  });
2430
2386
  }
2431
2387
  }
2388
+ function isBareFunctionExport(exportName, context) {
2389
+ const sourcePath = resolveExportSourceFile(exportName, context);
2390
+ if (!sourcePath) return false;
2391
+ let sourceCode;
2392
+ try {
2393
+ sourceCode = fs.readFileSync(sourcePath, "utf-8");
2394
+ } catch {
2395
+ return false;
2396
+ }
2397
+ const sourceFile = u.createSourceFile(
2398
+ sourcePath,
2399
+ sourceCode,
2400
+ u.ScriptTarget.Latest,
2401
+ true,
2402
+ u.ScriptKind.TS
2403
+ );
2404
+ for (const statement of sourceFile.statements) {
2405
+ if (u.isFunctionDeclaration(statement) && hasExportModifier(statement) && statement.name?.text === exportName) {
2406
+ return true;
2407
+ }
2408
+ }
2409
+ return false;
2410
+ }
2411
+ function resolveModulePathWithJsToTs(basePath) {
2412
+ const result = resolveModulePath(basePath);
2413
+ if (result) return result;
2414
+ if (basePath.endsWith(".js")) {
2415
+ return resolveModulePath(basePath.slice(0, -3) + ".ts");
2416
+ }
2417
+ return void 0;
2418
+ }
2419
+ function resolveExportSourceFile(exportName, context) {
2420
+ const modulePath = context.manifest.module || "index";
2421
+ const entryBase = path.join(context.pluginPath, modulePath);
2422
+ const libEntryBase = path.join(context.pluginPath, "lib", modulePath);
2423
+ const sourceEntry = resolveModulePath(entryBase) || resolveModulePath(libEntryBase);
2424
+ if (!sourceEntry || !sourceEntry.endsWith(".ts")) return void 0;
2425
+ return followExportChain(exportName, sourceEntry);
2426
+ }
2427
+ function followExportChain(exportName, filePath) {
2428
+ let sourceCode;
2429
+ try {
2430
+ sourceCode = fs.readFileSync(filePath, "utf-8");
2431
+ } catch {
2432
+ return void 0;
2433
+ }
2434
+ const sourceFile = u.createSourceFile(
2435
+ filePath,
2436
+ sourceCode,
2437
+ u.ScriptTarget.Latest,
2438
+ true,
2439
+ u.ScriptKind.TS
2440
+ );
2441
+ const starReexportModules = [];
2442
+ for (const statement of sourceFile.statements) {
2443
+ if (u.isExportDeclaration(statement) && statement.moduleSpecifier) {
2444
+ if (!u.isStringLiteral(statement.moduleSpecifier)) continue;
2445
+ const moduleSpec = statement.moduleSpecifier.text;
2446
+ if (!statement.exportClause) {
2447
+ starReexportModules.push(moduleSpec);
2448
+ continue;
2449
+ }
2450
+ if (u.isNamedExports(statement.exportClause)) {
2451
+ for (const element of statement.exportClause.elements) {
2452
+ if (element.name.text === exportName) {
2453
+ const resolvedBase = path.resolve(path.dirname(filePath), moduleSpec);
2454
+ return resolveModulePathWithJsToTs(resolvedBase);
2455
+ }
2456
+ }
2457
+ }
2458
+ }
2459
+ if (u.isFunctionDeclaration(statement) && hasExportModifier(statement)) {
2460
+ if (statement.name?.text === exportName) return filePath;
2461
+ }
2462
+ if (u.isVariableStatement(statement) && hasExportModifier(statement)) {
2463
+ for (const decl of statement.declarationList.declarations) {
2464
+ if (u.isIdentifier(decl.name) && decl.name.text === exportName) return filePath;
2465
+ }
2466
+ }
2467
+ }
2468
+ for (const moduleSpec of starReexportModules) {
2469
+ if (!moduleSpec.startsWith(".")) continue;
2470
+ const resolvedBase = path.resolve(path.dirname(filePath), moduleSpec);
2471
+ const resolved = resolveModulePathWithJsToTs(resolvedBase);
2472
+ if (!resolved) continue;
2473
+ const found = followExportChain(exportName, resolved);
2474
+ if (found) return found;
2475
+ }
2476
+ return void 0;
2477
+ }
2432
2478
  async function validateDynamicContracts(context, result) {
2433
2479
  const { dynamic_contracts } = context.manifest;
2434
- if (!dynamic_contracts)
2435
- return;
2480
+ if (!dynamic_contracts) return;
2436
2481
  const dynamicConfigs = Array.isArray(dynamic_contracts) ? dynamic_contracts : [dynamic_contracts];
2437
2482
  for (const config of dynamicConfigs) {
2438
2483
  const prefix = config.prefix || "(unknown)";
@@ -2456,6 +2501,13 @@ async function validateDynamicContracts(context, result) {
2456
2501
  suggestion: `Create generator file at ${generatorPath}.ts`
2457
2502
  });
2458
2503
  }
2504
+ } else if (isBareFunctionExport(config.generator, context)) {
2505
+ result.errors.push({
2506
+ type: "export-mismatch",
2507
+ message: `Generator "${config.generator}" for ${prefix} is a bare function — it must be a DynamicContractGenerator object`,
2508
+ location: "plugin.yaml dynamic_contracts",
2509
+ suggestion: `Use makeContractGenerator().generateWith(...) from @jay-framework/fullstack-component instead of exporting a plain function`
2510
+ });
2459
2511
  }
2460
2512
  }
2461
2513
  if (config.component) {
@@ -2511,8 +2563,7 @@ function extractExpressions(text) {
2511
2563
  function extractTagPath(expr) {
2512
2564
  let cleaned = expr.replace(/^!/, "").trim();
2513
2565
  cleaned = cleaned.split(/\s*[!=]==?\s*/)[0].trim();
2514
- if (cleaned === "." || cleaned === "")
2515
- return null;
2566
+ if (cleaned === "." || cleaned === "") return null;
2516
2567
  if (/^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*$/.test(cleaned)) {
2517
2568
  return cleaned;
2518
2569
  }
@@ -2611,8 +2662,7 @@ function collectUsedTags(jayHtml) {
2611
2662
  const ifVal = element.getAttribute?.("if");
2612
2663
  if (ifVal) {
2613
2664
  const ifPath = extractTagPath(ifVal);
2614
- if (ifPath)
2615
- resolvePath(ifPath, scopes);
2665
+ if (ifPath) resolvePath(ifPath, scopes);
2616
2666
  }
2617
2667
  const refVal = element.getAttribute?.("ref");
2618
2668
  if (refVal) {
@@ -2620,12 +2670,10 @@ function collectUsedTags(jayHtml) {
2620
2670
  }
2621
2671
  const attrs = element.attributes ?? {};
2622
2672
  for (const [name, value] of Object.entries(attrs)) {
2623
- if (SKIP_ATTRS.has(name))
2624
- continue;
2673
+ if (SKIP_ATTRS.has(name)) continue;
2625
2674
  for (const expr of extractExpressions(value)) {
2626
2675
  const p = extractTagPath(expr);
2627
- if (p)
2628
- resolvePath(p, scopes);
2676
+ if (p) resolvePath(p, scopes);
2629
2677
  }
2630
2678
  }
2631
2679
  for (const child of element.childNodes ?? []) {
@@ -2633,8 +2681,7 @@ function collectUsedTags(jayHtml) {
2633
2681
  const text = child.rawText ?? child.text ?? "";
2634
2682
  for (const expr of extractExpressions(text)) {
2635
2683
  const p = extractTagPath(expr);
2636
- if (p)
2637
- resolvePath(p, childScopes);
2684
+ if (p) resolvePath(p, childScopes);
2638
2685
  }
2639
2686
  } else if (child.nodeType === 1) {
2640
2687
  walkElement(child, childScopes);
@@ -2647,14 +2694,12 @@ function collectUsedTags(jayHtml) {
2647
2694
  function analyzeTagCoverage(jayHtml, file) {
2648
2695
  const imports = jayHtml.headlessImports;
2649
2696
  const withContracts = imports.filter((imp) => imp.contract);
2650
- if (withContracts.length === 0)
2651
- return null;
2697
+ if (withContracts.length === 0) return null;
2652
2698
  const usedTagsMap = collectUsedTags(jayHtml);
2653
2699
  const contracts = [];
2654
2700
  for (let i = 0; i < imports.length; i++) {
2655
2701
  const imp = imports[i];
2656
- if (!imp.contract)
2657
- continue;
2702
+ if (!imp.contract) continue;
2658
2703
  const allTags = flattenContractTags(imp.contract.tags);
2659
2704
  const usedSet = usedTagsMap.get(i) ?? /* @__PURE__ */ new Set();
2660
2705
  const expanded = new Set(usedSet);
@@ -2683,12 +2728,9 @@ function resolveContractTag(contract, tagPath) {
2683
2728
  let tags = contract.tags;
2684
2729
  for (let i = 0; i < segments.length; i++) {
2685
2730
  const tag = tags.find((t) => t.tag === segments[i]);
2686
- if (!tag)
2687
- return void 0;
2688
- if (i === segments.length - 1)
2689
- return tag;
2690
- if (!tag.tags)
2691
- return void 0;
2731
+ if (!tag) return void 0;
2732
+ if (i === segments.length - 1) return tag;
2733
+ if (!tag.tags) return void 0;
2692
2734
  tags = tag.tags;
2693
2735
  }
2694
2736
  return void 0;
@@ -2720,17 +2762,13 @@ function checkRefElementTypes(jayHtml, file) {
2720
2762
  importIndex = scope.importIndex;
2721
2763
  tagPath = scope.prefix ? `${scope.prefix}.${refPath}` : refPath;
2722
2764
  }
2723
- if (importIndex === void 0)
2724
- return;
2765
+ if (importIndex === void 0) return;
2725
2766
  const imp = imports[importIndex];
2726
- if (!imp.contract)
2727
- return;
2767
+ if (!imp.contract) return;
2728
2768
  const contractTag = resolveContractTag(imp.contract, tagPath);
2729
- if (!contractTag || !contractTag.elementType)
2730
- return;
2769
+ if (!contractTag || !contractTag.elementType) return;
2731
2770
  const contractTypes = contractTag.elementType;
2732
- if (contractTypes.includes("HTMLElement"))
2733
- return;
2771
+ if (contractTypes.includes("HTMLElement")) return;
2734
2772
  if (!contractTypes.includes(ref.actualType)) {
2735
2773
  const label = imp.key ? `${imp.key}.${tagPath}` : tagPath;
2736
2774
  warnings.push(
@@ -2799,6 +2837,65 @@ function checkRefElementTypes(jayHtml, file) {
2799
2837
  walkElement(jayHtml.body, []);
2800
2838
  return warnings;
2801
2839
  }
2840
+ function checkPageComponentExport(jayHtmlPath) {
2841
+ const dirname = path.dirname(jayHtmlPath);
2842
+ const compPath = path.join(dirname, "page.ts");
2843
+ if (!fs.existsSync(compPath)) return null;
2844
+ let content;
2845
+ try {
2846
+ content = fs.readFileSync(compPath, "utf-8");
2847
+ } catch {
2848
+ return null;
2849
+ }
2850
+ const exportName = "page";
2851
+ const patterns = [
2852
+ new RegExp(`export\\s*\\{[^}]*\\b${exportName}\\b[^}]*\\}`, "m"),
2853
+ new RegExp(`export\\s+(?:async\\s+)?function\\s+${exportName}\\b`),
2854
+ new RegExp(`export\\s+(?:const|let|var)\\s+${exportName}\\b`)
2855
+ ];
2856
+ if (patterns.some((p) => p.test(content))) return null;
2857
+ return `${path.relative(dirname, compPath)} exists but does not export "${exportName}". Remove the file or add the export.`;
2858
+ }
2859
+ const DOCUMENT_ACCESS_PATTERNS = [
2860
+ /document\.getElementById\b/,
2861
+ /document\.querySelector\b/,
2862
+ /document\.querySelectorAll\b/,
2863
+ /document\.getElementsBy\w+/,
2864
+ /document\.createElement\b/,
2865
+ /document\.body\.appendChild\b/,
2866
+ /document\.addEventListener\b/
2867
+ ];
2868
+ const DOM_SUPPRESS_COMMENT = "jay-dom: allow";
2869
+ function checkDirectDocumentAccess(jayHtmlPath) {
2870
+ const dirname = path.dirname(jayHtmlPath);
2871
+ const basename = path.basename(jayHtmlPath, JAY_EXTENSION);
2872
+ const candidates = [path.join(dirname, `${basename}.ts`), path.join(dirname, "page.ts")];
2873
+ const compPath = candidates.find((p) => fs.existsSync(p));
2874
+ if (!compPath) return [];
2875
+ const compName = path.basename(compPath);
2876
+ let content;
2877
+ try {
2878
+ content = fs.readFileSync(compPath, "utf-8");
2879
+ } catch {
2880
+ return [];
2881
+ }
2882
+ const warnings = [];
2883
+ const lines = content.split("\n");
2884
+ for (let i = 0; i < lines.length; i++) {
2885
+ const line = lines[i];
2886
+ if (line.includes(DOM_SUPPRESS_COMMENT)) continue;
2887
+ for (const pattern of DOCUMENT_ACCESS_PATTERNS) {
2888
+ const match = pattern.exec(line);
2889
+ if (match) {
2890
+ warnings.push(
2891
+ `${compName}:${i + 1} — Direct DOM access "${match[0]}" — use Jay refs instead. Suppress with // ${DOM_SUPPRESS_COMMENT} on the same line. See agent-kit/developer/component-refs.md`
2892
+ );
2893
+ break;
2894
+ }
2895
+ }
2896
+ }
2897
+ return warnings;
2898
+ }
2802
2899
  const PARSE_PARAM = /^\[(\[)?(\.\.\.)?([^\]]+)\]?\]$/;
2803
2900
  function extractRouteParams(filePath, pagesBase) {
2804
2901
  const relative = path.relative(pagesBase, filePath);
@@ -2840,8 +2937,7 @@ function checkRouteParams(parsedFile, filePath, pagesBase) {
2840
2937
  collectParams(imp.contract.params);
2841
2938
  }
2842
2939
  }
2843
- if (requiredParams.size === 0)
2844
- return [];
2940
+ if (requiredParams.size === 0) return [];
2845
2941
  const routeParams = extractRouteParams(filePath, pagesBase);
2846
2942
  const headlessProps = extractHeadlessPropsParamNames(parsedFile);
2847
2943
  const availableParams = /* @__PURE__ */ new Set([...routeParams, ...headlessProps]);
@@ -2857,11 +2953,9 @@ function checkRouteParams(parsedFile, filePath, pagesBase) {
2857
2953
  }
2858
2954
  function checkRouteToContractParams(parsedFile, filePath, pagesBase) {
2859
2955
  const routeParams = extractRouteParams(filePath, pagesBase);
2860
- if (routeParams.size === 0)
2861
- return [];
2956
+ if (routeParams.size === 0) return [];
2862
2957
  const hasAnyContract = !!parsedFile.contract || parsedFile.headlessImports.some((imp) => !!imp.contract);
2863
- if (!hasAnyContract)
2864
- return [];
2958
+ if (!hasAnyContract) return [];
2865
2959
  const declaredParams = /* @__PURE__ */ new Set();
2866
2960
  if (parsedFile.contract?.params) {
2867
2961
  for (const p of parsedFile.contract.params) {
@@ -2913,17 +3007,14 @@ function resolveBindingPhase(bindingPath, jayHtml) {
2913
3007
  const keyedImport = jayHtml.headlessImports.find((i) => i.key === root && i.contract);
2914
3008
  if (keyedImport?.contract) {
2915
3009
  const tagPath = segments.slice(1).join(".");
2916
- if (!tagPath)
2917
- return void 0;
3010
+ if (!tagPath) return void 0;
2918
3011
  const tag = resolveContractTag(keyedImport.contract, tagPath);
2919
- if (!tag)
2920
- return void 0;
3012
+ if (!tag) return void 0;
2921
3013
  return tag.phase || "slow";
2922
3014
  }
2923
3015
  if (jayHtml.contract) {
2924
3016
  const tag = resolveContractTag(jayHtml.contract, bindingPath);
2925
- if (!tag)
2926
- return void 0;
3017
+ if (!tag) return void 0;
2927
3018
  return tag.phase || "slow";
2928
3019
  }
2929
3020
  return void 0;
@@ -2974,15 +3065,12 @@ function checkHeadlessInstanceProps(jayHtml, file) {
2974
3065
  }
2975
3066
  for (const contractProp of contract.props) {
2976
3067
  const attrValue = lowerAttrs[contractProp.name.toLowerCase()];
2977
- if (!attrValue)
2978
- continue;
3068
+ if (!attrValue) continue;
2979
3069
  const bindingMatch = attrValue.match(/^\{(.+)\}$/);
2980
- if (!bindingMatch)
2981
- continue;
3070
+ if (!bindingMatch) continue;
2982
3071
  const bindingPath = bindingMatch[1];
2983
3072
  const sourcePhase = resolveBindingPhase(bindingPath, jayHtml);
2984
- if (!sourcePhase)
2985
- continue;
3073
+ if (!sourcePhase) continue;
2986
3074
  const propPhase = contractProp.phase ?? "slow";
2987
3075
  const sourceOrder = PHASE_ORDER[sourcePhase] ?? 0;
2988
3076
  const propOrder = PHASE_ORDER[propPhase] ?? 0;
@@ -3020,8 +3108,7 @@ function resolveLinkedTags(tags, contractDir) {
3020
3108
  });
3021
3109
  }
3022
3110
  function resolveContractLinks(contract, contractPath) {
3023
- if (!contractPath)
3024
- return contract;
3111
+ if (!contractPath) return contract;
3025
3112
  const contractDir = path.dirname(contractPath);
3026
3113
  return { ...contract, tags: resolveLinkedTags(contract.tags, contractDir) };
3027
3114
  }
@@ -3029,8 +3116,7 @@ async function runPluginValidators(projectRoot, parsedFiles, errors, warnings) {
3029
3116
  const scannedPlugins = await scanPlugins({ projectRoot, includeDevDeps: true });
3030
3117
  const loadedValidators = [];
3031
3118
  for (const [, plugin] of scannedPlugins) {
3032
- if (!plugin.manifest.validators)
3033
- continue;
3119
+ if (!plugin.manifest.validators) continue;
3034
3120
  for (const validatorDef of plugin.manifest.validators) {
3035
3121
  const source = `${plugin.name}/${validatorDef.name}`;
3036
3122
  let validatorFn;
@@ -3232,6 +3318,18 @@ async function validateJayFiles(options = {}) {
3232
3318
  message: '<script type="application/jay-params"> is deprecated. Move the values into the YAML body of the headless component that uses them. See agent-kit/developer/routing.md for details.'
3233
3319
  });
3234
3320
  }
3321
+ const pageExportError = checkPageComponentExport(jayFile);
3322
+ if (pageExportError) {
3323
+ errors.push({
3324
+ file: relativePath,
3325
+ message: pageExportError,
3326
+ stage: "generate"
3327
+ });
3328
+ }
3329
+ const domWarnings = checkDirectDocumentAccess(jayFile);
3330
+ for (const msg of domWarnings) {
3331
+ warnings.push({ file: relativePath, message: msg });
3332
+ }
3235
3333
  const routeParamWarnings = checkRouteParams(parsedFile.val, jayFile, scanDir);
3236
3334
  for (const msg of routeParamWarnings) {
3237
3335
  warnings.push({ file: relativePath, message: msg });
@@ -3296,6 +3394,21 @@ async function validateJayFiles(options = {}) {
3296
3394
  }
3297
3395
  }
3298
3396
  }
3397
+ const robotsTxtPath = path.resolve(projectRoot, "public/robots.txt");
3398
+ if (!fs.existsSync(robotsTxtPath)) {
3399
+ warnings.push({
3400
+ file: "public/robots.txt",
3401
+ message: "public/robots.txt not found — search engines may crawl pages you don't intend to expose.",
3402
+ suggestion: "Create public/robots.txt with: User-agent: *\nAllow: /\nSitemap: https://your-domain.com/sitemap.xml"
3403
+ });
3404
+ }
3405
+ if (!config.site?.baseUrl) {
3406
+ warnings.push({
3407
+ file: ".jay",
3408
+ message: "site.baseUrl not configured — sitemap.xml will not be generated in production.",
3409
+ suggestion: "Add to .jay config: site:\n baseUrl: https://your-domain.com"
3410
+ });
3411
+ }
3299
3412
  const pluginValidators = await runPluginValidators(projectRoot, parsedFiles, errors, warnings);
3300
3413
  return {
3301
3414
  valid: errors.length === 0,
@@ -3609,8 +3722,7 @@ async function ensureAgentKitDocs(projectRoot, _force, mode) {
3609
3722
  const sharedDirs = ["contracts"];
3610
3723
  for (const dir of sharedDirs) {
3611
3724
  const srcDir = path$1.join(templateDir, dir);
3612
- if (!fsSync.existsSync(srcDir))
3613
- continue;
3725
+ if (!fsSync.existsSync(srcDir)) continue;
3614
3726
  await copyDirRecursive(srcDir, path$1.join(agentKitDir, dir));
3615
3727
  getLogger().info(chalk.gray(` Created agent-kit/${dir}/`));
3616
3728
  }
@@ -3635,8 +3747,7 @@ async function mergePluginAgentKitGuides(projectRoot, mode) {
3635
3747
  const copiedPerRole = /* @__PURE__ */ new Map();
3636
3748
  for (const [, plugin] of plugins) {
3637
3749
  const pluginAgentKitDir = path$1.join(plugin.pluginPath, "agent-kit");
3638
- if (!fsSync.existsSync(pluginAgentKitDir))
3639
- continue;
3750
+ if (!fsSync.existsSync(pluginAgentKitDir)) continue;
3640
3751
  for (const role of roles) {
3641
3752
  const roleSourceDir = path$1.join(pluginAgentKitDir, role);
3642
3753
  let files;
@@ -3647,8 +3758,7 @@ async function mergePluginAgentKitGuides(projectRoot, mode) {
3647
3758
  } catch {
3648
3759
  continue;
3649
3760
  }
3650
- if (files.length === 0)
3651
- continue;
3761
+ if (files.length === 0) continue;
3652
3762
  const roleOutputDir = path$1.join(agentKitDir, role);
3653
3763
  await fs$1.mkdir(roleOutputDir, { recursive: true });
3654
3764
  for (const filename of files) {
@@ -3671,8 +3781,7 @@ async function mergePluginAgentKitGuides(projectRoot, mode) {
3671
3781
  }
3672
3782
  } catch {
3673
3783
  }
3674
- if (!copiedPerRole.has(role))
3675
- copiedPerRole.set(role, []);
3784
+ if (!copiedPerRole.has(role)) copiedPerRole.set(role, []);
3676
3785
  copiedPerRole.get(role).push({ filename, pluginName: plugin.name, description });
3677
3786
  getLogger().info(
3678
3787
  chalk.gray(
@@ -3684,8 +3793,7 @@ async function mergePluginAgentKitGuides(projectRoot, mode) {
3684
3793
  }
3685
3794
  for (const [role, entries] of copiedPerRole) {
3686
3795
  const instructionsPath = path$1.join(agentKitDir, role, "INSTRUCTIONS.md");
3687
- if (!fsSync.existsSync(instructionsPath))
3688
- continue;
3796
+ if (!fsSync.existsSync(instructionsPath)) continue;
3689
3797
  const lines = [
3690
3798
  "",
3691
3799
  "## Plugin-Contributed Guides",
@@ -3707,8 +3815,7 @@ async function generatePluginAgentKit(projectRoot, options, initErrors, viteServ
3707
3815
  verbose: options.verbose,
3708
3816
  pluginFilter: options.plugin
3709
3817
  });
3710
- if (plugins.length === 0)
3711
- return;
3818
+ if (plugins.length === 0) return;
3712
3819
  const logger = getLogger();
3713
3820
  logger.important("");
3714
3821
  logger.important(chalk.bold("Generating plugin agent-kit data..."));
@@ -3937,20 +4044,17 @@ function createAnswersFilePrompt(answers, pluginName) {
3937
4044
  return {
3938
4045
  async input(options) {
3939
4046
  const value = answers[options.key];
3940
- if (value !== void 0)
3941
- return value;
4047
+ if (value !== void 0) return value;
3942
4048
  throw new SetupNeedsAnswerError(pluginName, options.key, "input", options.message);
3943
4049
  },
3944
4050
  async confirm(options) {
3945
4051
  const value = answers[options.key];
3946
- if (value !== void 0)
3947
- return value === "true" || value === "yes";
4052
+ if (value !== void 0) return value === "true" || value === "yes";
3948
4053
  throw new SetupNeedsAnswerError(pluginName, options.key, "confirm", options.message);
3949
4054
  },
3950
4055
  async select(options) {
3951
4056
  const value = answers[options.key];
3952
- if (value !== void 0)
3953
- return value;
4057
+ if (value !== void 0) return value;
3954
4058
  throw new SetupNeedsAnswerError(
3955
4059
  pluginName,
3956
4060
  options.key,
@@ -4111,12 +4215,9 @@ async function runSetup(pluginFilter, options, projectRoot) {
4111
4215
  }
4112
4216
  await runProjectInit(projectRoot, viteServer);
4113
4217
  const parts = [];
4114
- if (configured > 0)
4115
- parts.push(`${configured} configured`);
4116
- if (needsConfig > 0)
4117
- parts.push(`${needsConfig} needs config`);
4118
- if (errors > 0)
4119
- parts.push(`${errors} error(s)`);
4218
+ if (configured > 0) parts.push(`${configured} configured`);
4219
+ if (needsConfig > 0) parts.push(`${needsConfig} needs config`);
4220
+ if (errors > 0) parts.push(`${errors} error(s)`);
4120
4221
  logger.important(`Setup complete: ${parts.join(", ")}`);
4121
4222
  if (errors > 0) {
4122
4223
  process.exit(1);
@@ -4135,8 +4236,7 @@ async function runSetup(pluginFilter, options, projectRoot) {
4135
4236
  }
4136
4237
  async function initPlugin(pluginName, allPluginsWithInit, viteServer, logger) {
4137
4238
  const pluginInit = allPluginsWithInit.filter((p) => p.name === pluginName);
4138
- if (pluginInit.length === 0)
4139
- return;
4239
+ if (pluginInit.length === 0) return;
4140
4240
  const initErrors = await executePluginServerInits(pluginInit, viteServer, false, true);
4141
4241
  if (initErrors.size > 0) {
4142
4242
  for (const [, err] of initErrors) {
@@ -4276,8 +4376,7 @@ function printCommandList(commands) {
4276
4376
  logger.important("\nAvailable plugin commands:\n");
4277
4377
  const byPlugin = /* @__PURE__ */ new Map();
4278
4378
  for (const cmd of commands) {
4279
- if (!byPlugin.has(cmd.pluginName))
4280
- byPlugin.set(cmd.pluginName, []);
4379
+ if (!byPlugin.has(cmd.pluginName)) byPlugin.set(cmd.pluginName, []);
4281
4380
  byPlugin.get(cmd.pluginName).push(cmd);
4282
4381
  }
4283
4382
  for (const [pluginName, cmds] of byPlugin) {
@@ -4333,8 +4432,7 @@ program.command("build").description("Build production artifacts").option("-p, -
4333
4432
  await runBuild(options.path, options);
4334
4433
  } catch (error) {
4335
4434
  getLogger().error(chalk.red("Build failed:") + " " + error.message);
4336
- if (error.stack)
4337
- getLogger().error(error.stack);
4435
+ if (error.stack) getLogger().error(error.stack);
4338
4436
  process.exit(1);
4339
4437
  }
4340
4438
  });
@@ -4343,8 +4441,7 @@ program.command("serve").description("Start production server").option("-p, --pa
4343
4441
  await runServe(options.path, options);
4344
4442
  } catch (error) {
4345
4443
  getLogger().error(chalk.red("Server failed:") + " " + error.message);
4346
- if (error.stack)
4347
- getLogger().error(error.stack);
4444
+ if (error.stack) getLogger().error(error.stack);
4348
4445
  process.exit(1);
4349
4446
  }
4350
4447
  });
@@ -4353,8 +4450,7 @@ program.command("rebuild").description("Rebuild instances by contract, route, or
4353
4450
  await runRebuild(options.path, options);
4354
4451
  } catch (error) {
4355
4452
  getLogger().error(chalk.red("Rebuild failed:") + " " + error.message);
4356
- if (error.stack)
4357
- getLogger().error(error.stack);
4453
+ if (error.stack) getLogger().error(error.stack);
4358
4454
  process.exit(1);
4359
4455
  }
4360
4456
  });