@jay-framework/jay-stack-cli 0.25.1 → 0.25.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-kit-template/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 +428 -18
- package/package.json +14 -14
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { LogLevel } from '@jay-framework/logger';
|
|
2
2
|
export { MaterializeContractsOptions, MaterializeResult, PluginContractEntry, PluginsIndex, PluginsIndexEntry, listContracts, materializeContracts } from '@jay-framework/stack-server-build';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* `express.static` `setHeaders` callback that mirrors production for agent-readiness resources
|
|
6
|
+
* (DL#217): everything under `/.well-known/` is served CORS-open, with `SKILL.md` as
|
|
7
|
+
* `text/markdown` and skill archives (`.gz`) as `application/gzip`. Exported so the dev-serving
|
|
8
|
+
* test guards the real behavior rather than a copy.
|
|
9
|
+
*/
|
|
10
|
+
declare function setWellKnownHeaders(res: {
|
|
11
|
+
setHeader(name: string, value: string): void;
|
|
12
|
+
}, filePath: string): void;
|
|
4
13
|
interface StartDevServerOptions {
|
|
5
14
|
projectPath?: string;
|
|
6
15
|
testMode?: boolean;
|
|
@@ -19,10 +28,34 @@ interface JayConfig {
|
|
|
19
28
|
};
|
|
20
29
|
site?: {
|
|
21
30
|
baseUrl?: string;
|
|
31
|
+
/** Site/organization name — drives Organization + WebSite JSON-LD (DL#213). */
|
|
32
|
+
name?: string;
|
|
33
|
+
/** Logo URL (absolute or baseUrl-relative) for the Organization JSON-LD (DL#213). */
|
|
34
|
+
logo?: string;
|
|
35
|
+
/** Short site description for the Organization JSON-LD (DL#213). */
|
|
36
|
+
description?: string;
|
|
37
|
+
/** Social/profile URLs for the Organization `sameAs` (DL#213). */
|
|
38
|
+
sameAs?: string[];
|
|
39
|
+
};
|
|
40
|
+
/** Project-wide validation suppressions for project-level concerns (DL#214). */
|
|
41
|
+
validations?: {
|
|
42
|
+
/**
|
|
43
|
+
* Suppress the `/.well-known/ard.json` absence nudge — for sites with no agent-callable
|
|
44
|
+
* resources/APIs to advertise. Lives in `.jay` (not a page's `application/jay-validations`
|
|
45
|
+
* script) because it governs a single project-level file, not any page's markup.
|
|
46
|
+
*/
|
|
47
|
+
'allow-no-ard'?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Suppress the minimum-error-pages nudge — for sites that intentionally ship no branded
|
|
50
|
+
* `404`/`5xx` pages and accept the plain-text fallback. Lives in `.jay` (not a page's
|
|
51
|
+
* `application/jay-validations` script) because it governs the project's `pages/` tree as a
|
|
52
|
+
* whole, not any single page's markup (DL#218).
|
|
53
|
+
*/
|
|
54
|
+
'allow-no-error-pages'?: boolean;
|
|
22
55
|
};
|
|
23
56
|
}
|
|
24
57
|
declare function loadConfig(projectRoot?: string): JayConfig;
|
|
25
58
|
declare function getConfigWithDefaults(config: JayConfig): Required<Pick<JayConfig, 'devServer'>> & Pick<JayConfig, 'site'>;
|
|
26
59
|
declare function updateConfig(updates: Partial<JayConfig>): void;
|
|
27
60
|
|
|
28
|
-
export { type JayConfig, type StartDevServerOptions, getConfigWithDefaults, loadConfig, startDevServer, updateConfig };
|
|
61
|
+
export { type JayConfig, type StartDevServerOptions, getConfigWithDefaults, loadConfig, setWellKnownHeaders, startDevServer, updateConfig };
|
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}`);
|
|
@@ -199,6 +200,13 @@ const generateContractDefinitionFiles$1 = /* @__PURE__ */ Object.freeze(/* @__PU
|
|
|
199
200
|
__proto__: null,
|
|
200
201
|
generateContractDefinitionFiles
|
|
201
202
|
}, Symbol.toStringTag, { value: "Module" }));
|
|
203
|
+
function setWellKnownHeaders(res, filePath) {
|
|
204
|
+
if (filePath.includes(`${path$1.sep}.well-known${path$1.sep}`)) {
|
|
205
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
206
|
+
if (filePath.endsWith(".md")) res.setHeader("Content-Type", "text/markdown; charset=utf-8");
|
|
207
|
+
if (filePath.endsWith(".gz")) res.setHeader("Content-Type", "application/gzip");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
202
210
|
async function startDevServer(options = {}) {
|
|
203
211
|
const projectPath = options.projectPath || process.cwd();
|
|
204
212
|
if (projectPath !== process.cwd()) {
|
|
@@ -214,7 +222,7 @@ async function startDevServer(options = {}) {
|
|
|
214
222
|
const httpServer = http.createServer(app);
|
|
215
223
|
const devServerPort = await getPort({ port: resolvedConfig.devServer.portRange });
|
|
216
224
|
const log = getLogger();
|
|
217
|
-
const { server, viteServer, routes, service } = await mkDevServer({
|
|
225
|
+
const { server, viteServer, routes, service, errorPageHandler } = await mkDevServer({
|
|
218
226
|
pagesRootFolder: path$1.resolve(resolvedConfig.devServer.pagesBase),
|
|
219
227
|
projectRootFolder: process.cwd(),
|
|
220
228
|
publicBaseUrlPath: "/",
|
|
@@ -232,18 +240,20 @@ async function startDevServer(options = {}) {
|
|
|
232
240
|
});
|
|
233
241
|
}
|
|
234
242
|
if (fs$1.existsSync(publicPath)) {
|
|
235
|
-
app.use(express.static(publicPath));
|
|
243
|
+
app.use(express.static(publicPath, { dotfiles: "allow", setHeaders: setWellKnownHeaders }));
|
|
236
244
|
} else {
|
|
237
245
|
log.important(`⚠️ Public folder not found: ${resolvedConfig.devServer.publicFolder}`);
|
|
238
246
|
}
|
|
247
|
+
const pageRouter = express.Router();
|
|
239
248
|
routes.forEach((route) => {
|
|
240
|
-
|
|
249
|
+
pageRouter.get(route.path, route.handler);
|
|
241
250
|
});
|
|
242
251
|
service.attachRouteRegistrar((added) => {
|
|
243
252
|
added.forEach((route) => {
|
|
244
|
-
|
|
253
|
+
pageRouter.get(route.path, route.handler);
|
|
245
254
|
});
|
|
246
255
|
});
|
|
256
|
+
app.use(pageRouter);
|
|
247
257
|
void (async () => {
|
|
248
258
|
await generatePageDefinitionFiles(
|
|
249
259
|
routes.map((route) => route.fsRoute.jayHtmlPath),
|
|
@@ -298,6 +308,7 @@ async function startDevServer(options = {}) {
|
|
|
298
308
|
}, options.timeout * 1e3);
|
|
299
309
|
}
|
|
300
310
|
}
|
|
311
|
+
app.use(errorPageHandler(404));
|
|
301
312
|
process.on("SIGTERM", shutdown);
|
|
302
313
|
process.on("SIGINT", shutdown);
|
|
303
314
|
}
|
|
@@ -3004,6 +3015,61 @@ async function validateAiditorSettings(context, result) {
|
|
|
3004
3015
|
});
|
|
3005
3016
|
}
|
|
3006
3017
|
}
|
|
3018
|
+
async function compileValidateRouteJayHtml(absoluteJayHtmlPath, pluginRoot) {
|
|
3019
|
+
const content = fs$1.readFileSync(absoluteJayHtmlPath, "utf-8");
|
|
3020
|
+
const dirname = path$1.dirname(absoluteJayHtmlPath);
|
|
3021
|
+
const filename = path$1.basename(absoluteJayHtmlPath, JAY_EXTENSION$1);
|
|
3022
|
+
const errors = [];
|
|
3023
|
+
try {
|
|
3024
|
+
const parsedFile = await parseJayFile(
|
|
3025
|
+
content,
|
|
3026
|
+
filename,
|
|
3027
|
+
dirname,
|
|
3028
|
+
{},
|
|
3029
|
+
JAY_IMPORT_RESOLVER,
|
|
3030
|
+
pluginRoot
|
|
3031
|
+
);
|
|
3032
|
+
for (const validation of parsedFile.validations) {
|
|
3033
|
+
errors.push(validation);
|
|
3034
|
+
}
|
|
3035
|
+
if (!parsedFile.val || parsedFile.validations.length > 0) {
|
|
3036
|
+
return errors;
|
|
3037
|
+
}
|
|
3038
|
+
const generatedFile = generateElementFile(parsedFile.val, RuntimeMode$1.MainTrusted);
|
|
3039
|
+
for (const validation of generatedFile.validations) {
|
|
3040
|
+
errors.push(validation);
|
|
3041
|
+
}
|
|
3042
|
+
const serverElementFile = generateServerElementFile(parsedFile.val);
|
|
3043
|
+
for (const validation of serverElementFile.validations) {
|
|
3044
|
+
errors.push(`[SSR] ${validation}`);
|
|
3045
|
+
}
|
|
3046
|
+
} catch (error) {
|
|
3047
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3048
|
+
errors.push(message);
|
|
3049
|
+
}
|
|
3050
|
+
return errors;
|
|
3051
|
+
}
|
|
3052
|
+
async function validateRouteJayHtmlTemplates(context, result) {
|
|
3053
|
+
const routes = context.manifest.routes;
|
|
3054
|
+
if (!routes || !Array.isArray(routes)) return;
|
|
3055
|
+
for (const route of routes) {
|
|
3056
|
+
if (!route.jayHtml) continue;
|
|
3057
|
+
const absolutePath = path$1.resolve(context.pluginPath, route.jayHtml);
|
|
3058
|
+
if (!fs$1.existsSync(absolutePath)) {
|
|
3059
|
+
continue;
|
|
3060
|
+
}
|
|
3061
|
+
const compileErrors = await compileValidateRouteJayHtml(absolutePath, context.pluginPath);
|
|
3062
|
+
const relPath = path$1.relative(context.pluginPath, absolutePath);
|
|
3063
|
+
for (const message of compileErrors) {
|
|
3064
|
+
result.errors.push({
|
|
3065
|
+
type: "jay-html-compile",
|
|
3066
|
+
message: `Route "${route.path}" jayHtml failed to compile: ${message}`,
|
|
3067
|
+
location: relPath,
|
|
3068
|
+
suggestion: "Fix the template syntax or run `jay-stack validate -p <dir>` on the source page during development. See agent-kit/designer/jay-html-template-syntax.md"
|
|
3069
|
+
});
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3007
3073
|
async function validatePlugin(options = {}) {
|
|
3008
3074
|
const pluginPath = options.pluginPath || process.cwd();
|
|
3009
3075
|
if (options.local) {
|
|
@@ -3073,6 +3139,7 @@ async function validatePluginPackage(pluginPath, options) {
|
|
|
3073
3139
|
await validateAddMenuCatalog(context, result);
|
|
3074
3140
|
await validateAiditorSettings(context, result);
|
|
3075
3141
|
validateNoCompilerLeak(context, result);
|
|
3142
|
+
await validateRouteJayHtmlTemplates(context, result);
|
|
3076
3143
|
result.valid = result.errors.length === 0;
|
|
3077
3144
|
return result;
|
|
3078
3145
|
}
|
|
@@ -3927,6 +3994,20 @@ async function validatePackageJson(context, result) {
|
|
|
3927
3994
|
suggestion: 'Add "./plugin.yaml": "./plugin.yaml" to exports field'
|
|
3928
3995
|
});
|
|
3929
3996
|
}
|
|
3997
|
+
if (context.manifest.commands) {
|
|
3998
|
+
for (const cmd of context.manifest.commands) {
|
|
3999
|
+
if (!cmd.command) continue;
|
|
4000
|
+
const exportKey = `./${cmd.command}`;
|
|
4001
|
+
if (!packageJson.exports || !packageJson.exports[exportKey]) {
|
|
4002
|
+
result.errors.push({
|
|
4003
|
+
type: "export-mismatch",
|
|
4004
|
+
message: `command metadata file "${cmd.command}" not exported in package.json (required for plugin resolution)`,
|
|
4005
|
+
location: packageJsonPath,
|
|
4006
|
+
suggestion: `Add "${exportKey}": "${exportKey}" to exports field`
|
|
4007
|
+
});
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
}
|
|
3930
4011
|
const agentKitDir = path$1.join(context.pluginPath, "agent-kit");
|
|
3931
4012
|
if (fs$1.existsSync(agentKitDir) && fs$1.statSync(agentKitDir).isDirectory()) {
|
|
3932
4013
|
const filesArray = packageJson.files;
|
|
@@ -4789,10 +4870,10 @@ function routeToRegex(route) {
|
|
|
4789
4870
|
async function buildRouteOracle(pagesBase) {
|
|
4790
4871
|
let routes = [];
|
|
4791
4872
|
try {
|
|
4792
|
-
routes = await scanRoutes(pagesBase, {
|
|
4873
|
+
({ routes } = await scanRoutes(pagesBase, {
|
|
4793
4874
|
jayHtmlFilename: "page.jay-html",
|
|
4794
4875
|
compFilename: "page.ts"
|
|
4795
|
-
});
|
|
4876
|
+
}));
|
|
4796
4877
|
} catch {
|
|
4797
4878
|
routes = [];
|
|
4798
4879
|
}
|
|
@@ -5005,6 +5086,89 @@ async function loadSlowViewState(cacheAbsPath) {
|
|
|
5005
5086
|
return {};
|
|
5006
5087
|
}
|
|
5007
5088
|
}
|
|
5089
|
+
async function loadCarryForward(cacheAbsPath) {
|
|
5090
|
+
try {
|
|
5091
|
+
const cache = JSON.parse(await promises.readFile(cacheAbsPath, "utf8"));
|
|
5092
|
+
return cache.carryForward ?? {};
|
|
5093
|
+
} catch {
|
|
5094
|
+
return {};
|
|
5095
|
+
}
|
|
5096
|
+
}
|
|
5097
|
+
function jsonLdBodyError(body) {
|
|
5098
|
+
const text = body?.trim();
|
|
5099
|
+
if (!text) return "Empty JSON-LD block — add structured data or remove the tag.";
|
|
5100
|
+
let parsed;
|
|
5101
|
+
try {
|
|
5102
|
+
parsed = JSON.parse(text);
|
|
5103
|
+
} catch (e2) {
|
|
5104
|
+
return `Invalid JSON: ${e2.message}`;
|
|
5105
|
+
}
|
|
5106
|
+
const objs = Array.isArray(parsed) ? parsed : [parsed];
|
|
5107
|
+
for (const obj of objs) {
|
|
5108
|
+
if (!obj || typeof obj !== "object") {
|
|
5109
|
+
return "JSON-LD must be an object (or array of objects) with @context and @type.";
|
|
5110
|
+
}
|
|
5111
|
+
if (!("@context" in obj) || !("@type" in obj)) {
|
|
5112
|
+
return "JSON-LD is missing @context or @type.";
|
|
5113
|
+
}
|
|
5114
|
+
}
|
|
5115
|
+
return null;
|
|
5116
|
+
}
|
|
5117
|
+
function isLdJsonHeadTag(tag) {
|
|
5118
|
+
return tag?.tag?.toLowerCase?.() === "script" && tag?.attrs?.type === "application/ld+json";
|
|
5119
|
+
}
|
|
5120
|
+
function checkBuildJsonLd(manifest) {
|
|
5121
|
+
const findings = [];
|
|
5122
|
+
for (const tag of manifest.siteHeadTags ?? []) {
|
|
5123
|
+
if (!isLdJsonHeadTag(tag)) continue;
|
|
5124
|
+
const err = jsonLdBodyError(tag.children);
|
|
5125
|
+
if (err) {
|
|
5126
|
+
findings.push({
|
|
5127
|
+
file: ".jay (site.*)",
|
|
5128
|
+
message: `Site JSON-LD: ${err}`,
|
|
5129
|
+
severity: "error",
|
|
5130
|
+
source: "build-output"
|
|
5131
|
+
});
|
|
5132
|
+
}
|
|
5133
|
+
}
|
|
5134
|
+
for (const route of manifest.routes) {
|
|
5135
|
+
for (const script of route.scripts ?? []) {
|
|
5136
|
+
if (script.attributes?.type !== "application/ld+json") continue;
|
|
5137
|
+
const err = jsonLdBodyError(script.inline);
|
|
5138
|
+
if (err) {
|
|
5139
|
+
findings.push({
|
|
5140
|
+
file: route.pattern,
|
|
5141
|
+
message: `Template JSON-LD: ${err}`,
|
|
5142
|
+
severity: "error",
|
|
5143
|
+
source: "build-output"
|
|
5144
|
+
});
|
|
5145
|
+
}
|
|
5146
|
+
}
|
|
5147
|
+
}
|
|
5148
|
+
return findings;
|
|
5149
|
+
}
|
|
5150
|
+
function checkInstanceJsonLd(args) {
|
|
5151
|
+
const { instanceUrl, carryForward } = args;
|
|
5152
|
+
const findings = [];
|
|
5153
|
+
const slowHeadTags = carryForward?.__slowHeadTags;
|
|
5154
|
+
if (!Array.isArray(slowHeadTags)) return findings;
|
|
5155
|
+
for (const source of slowHeadTags) {
|
|
5156
|
+
if (!Array.isArray(source)) continue;
|
|
5157
|
+
for (const tag of source) {
|
|
5158
|
+
if (!isLdJsonHeadTag(tag)) continue;
|
|
5159
|
+
const err = jsonLdBodyError(tag?.children);
|
|
5160
|
+
if (err) {
|
|
5161
|
+
findings.push({
|
|
5162
|
+
file: instanceUrl,
|
|
5163
|
+
message: `Component JSON-LD: ${err}`,
|
|
5164
|
+
severity: "error",
|
|
5165
|
+
source: "build-output"
|
|
5166
|
+
});
|
|
5167
|
+
}
|
|
5168
|
+
}
|
|
5169
|
+
}
|
|
5170
|
+
return findings;
|
|
5171
|
+
}
|
|
5008
5172
|
function checkTemplateLinksAgainstBuild(args) {
|
|
5009
5173
|
const { parsedFiles, buildUrls, assetUrls, dynamicMatchers, baseUrl } = args;
|
|
5010
5174
|
const findings = [];
|
|
@@ -5168,6 +5332,122 @@ async function newestSourceMtime(dirs) {
|
|
|
5168
5332
|
for (const dir of dirs) await walk(dir);
|
|
5169
5333
|
return newest;
|
|
5170
5334
|
}
|
|
5335
|
+
const URN_PATTERN = /^urn:air:[^:\s]+:[^:\s]+:[^:\s]+$/;
|
|
5336
|
+
const MEDIA_TYPE_PATTERN = /^[a-z]+\/[A-Za-z0-9.+-]+$/;
|
|
5337
|
+
const ARD_SUGGEST = "See developer/seo-guide.md (ARD v0.91) for the schema and a worked example.";
|
|
5338
|
+
function validateArdDocument(doc) {
|
|
5339
|
+
const findings = [];
|
|
5340
|
+
if (!doc || typeof doc !== "object") {
|
|
5341
|
+
return [
|
|
5342
|
+
{
|
|
5343
|
+
message: "ard.json must be a JSON object with an `entries` array.",
|
|
5344
|
+
severity: "error",
|
|
5345
|
+
suggestion: ARD_SUGGEST
|
|
5346
|
+
}
|
|
5347
|
+
];
|
|
5348
|
+
}
|
|
5349
|
+
const entries = doc.entries;
|
|
5350
|
+
if (!Array.isArray(entries)) {
|
|
5351
|
+
return [
|
|
5352
|
+
{
|
|
5353
|
+
message: "ard.json is missing the required `entries` array.",
|
|
5354
|
+
severity: "error",
|
|
5355
|
+
suggestion: ARD_SUGGEST
|
|
5356
|
+
}
|
|
5357
|
+
];
|
|
5358
|
+
}
|
|
5359
|
+
entries.forEach((entry, i) => {
|
|
5360
|
+
const at = `ard.json entries[${i}]`;
|
|
5361
|
+
if (!entry || typeof entry !== "object") {
|
|
5362
|
+
findings.push({ message: `${at} must be an object.`, severity: "error" });
|
|
5363
|
+
return;
|
|
5364
|
+
}
|
|
5365
|
+
if (typeof entry.identifier !== "string" || !URN_PATTERN.test(entry.identifier)) {
|
|
5366
|
+
findings.push({
|
|
5367
|
+
message: `${at}.identifier must match urn:air:<publisher>:<namespace>:<name>.`,
|
|
5368
|
+
severity: "error",
|
|
5369
|
+
suggestion: ARD_SUGGEST
|
|
5370
|
+
});
|
|
5371
|
+
}
|
|
5372
|
+
if (typeof entry.displayName !== "string" || entry.displayName.trim() === "") {
|
|
5373
|
+
findings.push({
|
|
5374
|
+
message: `${at}.displayName is required.`,
|
|
5375
|
+
severity: "error"
|
|
5376
|
+
});
|
|
5377
|
+
}
|
|
5378
|
+
if (typeof entry.type !== "string" || !MEDIA_TYPE_PATTERN.test(entry.type)) {
|
|
5379
|
+
findings.push({
|
|
5380
|
+
message: `${at}.type must be an IANA media type (e.g. application/mcp-server-card+json).`,
|
|
5381
|
+
severity: "error"
|
|
5382
|
+
});
|
|
5383
|
+
}
|
|
5384
|
+
const hasUrl = entry.url !== void 0;
|
|
5385
|
+
const hasData = entry.data !== void 0;
|
|
5386
|
+
if (hasUrl === hasData) {
|
|
5387
|
+
findings.push({
|
|
5388
|
+
message: `${at} must have exactly one of \`url\` or \`data\`.`,
|
|
5389
|
+
severity: "error"
|
|
5390
|
+
});
|
|
5391
|
+
}
|
|
5392
|
+
const rq = entry.representativeQueries;
|
|
5393
|
+
if (rq === void 0) {
|
|
5394
|
+
findings.push({
|
|
5395
|
+
message: `${at} is missing representativeQueries (2–5 recommended) — agents may not surface this resource.`,
|
|
5396
|
+
severity: "warning",
|
|
5397
|
+
suggestion: ARD_SUGGEST
|
|
5398
|
+
});
|
|
5399
|
+
} else if (!Array.isArray(rq) || rq.length < 2 || rq.length > 5) {
|
|
5400
|
+
findings.push({
|
|
5401
|
+
message: `${at}.representativeQueries should list 2–5 example queries.`,
|
|
5402
|
+
severity: "warning"
|
|
5403
|
+
});
|
|
5404
|
+
}
|
|
5405
|
+
});
|
|
5406
|
+
return findings;
|
|
5407
|
+
}
|
|
5408
|
+
async function checkArdFiles(projectRoot) {
|
|
5409
|
+
const wellKnown = path$1.resolve(projectRoot, "public/.well-known");
|
|
5410
|
+
const ardPath = path$1.join(wellKnown, "ard.json");
|
|
5411
|
+
const aliasPath = path$1.join(wellKnown, "ai-catalog.json");
|
|
5412
|
+
const ardRaw = await readIfExists(ardPath);
|
|
5413
|
+
const aliasRaw = await readIfExists(aliasPath);
|
|
5414
|
+
if (ardRaw === null && aliasRaw === null) {
|
|
5415
|
+
return { findings: [], absent: true };
|
|
5416
|
+
}
|
|
5417
|
+
const findings = [];
|
|
5418
|
+
if (ardRaw === null && aliasRaw !== null) {
|
|
5419
|
+
findings.push({
|
|
5420
|
+
message: "Only public/.well-known/ai-catalog.json found — ARD renamed this to ard.json. Add ard.json (keep ai-catalog.json as a copy for older clients).",
|
|
5421
|
+
severity: "warning",
|
|
5422
|
+
suggestion: ARD_SUGGEST
|
|
5423
|
+
});
|
|
5424
|
+
} else if (ardRaw !== null && aliasRaw !== null && ardRaw.trim() !== aliasRaw.trim()) {
|
|
5425
|
+
findings.push({
|
|
5426
|
+
message: "public/.well-known/ard.json and ai-catalog.json differ — the alias should be a copy of ard.json.",
|
|
5427
|
+
severity: "warning"
|
|
5428
|
+
});
|
|
5429
|
+
}
|
|
5430
|
+
const canonicalRaw = ardRaw ?? aliasRaw;
|
|
5431
|
+
let parsed;
|
|
5432
|
+
try {
|
|
5433
|
+
parsed = JSON.parse(canonicalRaw);
|
|
5434
|
+
} catch (e2) {
|
|
5435
|
+
findings.push({
|
|
5436
|
+
message: `${ardRaw !== null ? "ard.json" : "ai-catalog.json"} is not valid JSON: ${e2.message}`,
|
|
5437
|
+
severity: "error"
|
|
5438
|
+
});
|
|
5439
|
+
return { findings, absent: false };
|
|
5440
|
+
}
|
|
5441
|
+
findings.push(...validateArdDocument(parsed));
|
|
5442
|
+
return { findings, absent: false };
|
|
5443
|
+
}
|
|
5444
|
+
async function readIfExists(p) {
|
|
5445
|
+
try {
|
|
5446
|
+
return await promises.readFile(p, "utf8");
|
|
5447
|
+
} catch {
|
|
5448
|
+
return null;
|
|
5449
|
+
}
|
|
5450
|
+
}
|
|
5171
5451
|
async function findJayFiles(dir) {
|
|
5172
5452
|
return await glob(`${dir}/**/*${JAY_EXTENSION$1}`);
|
|
5173
5453
|
}
|
|
@@ -5369,6 +5649,17 @@ function resolveContractTag(contract, tagPath) {
|
|
|
5369
5649
|
}
|
|
5370
5650
|
return void 0;
|
|
5371
5651
|
}
|
|
5652
|
+
function checkJsonLdScripts(jayHtml) {
|
|
5653
|
+
const errors = [];
|
|
5654
|
+
for (const script of jayHtml.scripts ?? []) {
|
|
5655
|
+
if (script.attributes?.type !== "application/ld+json") continue;
|
|
5656
|
+
const err = jsonLdBodyError(script.inline);
|
|
5657
|
+
if (err) {
|
|
5658
|
+
errors.push(`<script type="application/ld+json">: ${err} See designer/seo-guide.md.`);
|
|
5659
|
+
}
|
|
5660
|
+
}
|
|
5661
|
+
return errors;
|
|
5662
|
+
}
|
|
5372
5663
|
function checkRefElementTypes(jayHtml, file) {
|
|
5373
5664
|
const imports = jayHtml.headlessImports;
|
|
5374
5665
|
const keyMap = /* @__PURE__ */ new Map();
|
|
@@ -6296,14 +6587,12 @@ async function validateJayFiles(options = {}) {
|
|
|
6296
6587
|
const reuseByTemplate = /* @__PURE__ */ new Map();
|
|
6297
6588
|
const cataloguedContractFiles = /* @__PURE__ */ new Set();
|
|
6298
6589
|
const pageJayHtmlFiles = await findJayFiles(scanDir);
|
|
6299
|
-
const componentJayHtmlFiles = await findJayFiles(componentsDir).catch(() => []);
|
|
6590
|
+
const componentJayHtmlFiles = options.compileOnly ? [] : await findJayFiles(componentsDir).catch(() => []);
|
|
6300
6591
|
const jayHtmlFiles = [...pageJayHtmlFiles, ...componentJayHtmlFiles];
|
|
6301
6592
|
const componentJayHtmlFileSet = new Set(componentJayHtmlFiles);
|
|
6302
|
-
const componentContractFiles = await findContractFiles(componentsDir).catch(
|
|
6303
|
-
() => []
|
|
6304
|
-
);
|
|
6593
|
+
const componentContractFiles = options.compileOnly ? [] : await findContractFiles(componentsDir).catch(() => []);
|
|
6305
6594
|
for (const cf of componentContractFiles) cataloguedContractFiles.add(cf);
|
|
6306
|
-
const contractFiles = [...await findContractFiles(scanDir), ...componentContractFiles];
|
|
6595
|
+
const contractFiles = options.compileOnly ? [] : [...await findContractFiles(scanDir), ...componentContractFiles];
|
|
6307
6596
|
const contractNameByFile = /* @__PURE__ */ new Map();
|
|
6308
6597
|
if (options.verbose) {
|
|
6309
6598
|
getLogger().info(chalk.gray(`Scanning directory: ${scanDir}`));
|
|
@@ -6370,6 +6659,35 @@ async function validateJayFiles(options = {}) {
|
|
|
6370
6659
|
continue;
|
|
6371
6660
|
}
|
|
6372
6661
|
parsedFiles.push({ relativePath, parsed: parsedFile.val });
|
|
6662
|
+
if (options.compileOnly) {
|
|
6663
|
+
const generatedFile2 = generateElementFile(
|
|
6664
|
+
parsedFile.val,
|
|
6665
|
+
RuntimeMode$1.MainTrusted,
|
|
6666
|
+
GenerateTarget.jay
|
|
6667
|
+
);
|
|
6668
|
+
for (const validation of generatedFile2.validations) {
|
|
6669
|
+
errors.push({
|
|
6670
|
+
file: relativePath,
|
|
6671
|
+
message: validation,
|
|
6672
|
+
stage: "generate"
|
|
6673
|
+
});
|
|
6674
|
+
}
|
|
6675
|
+
const serverElementFile2 = generateServerElementFile(parsedFile.val);
|
|
6676
|
+
for (const validation of serverElementFile2.validations) {
|
|
6677
|
+
errors.push({
|
|
6678
|
+
file: relativePath,
|
|
6679
|
+
message: `[SSR] ${validation}`,
|
|
6680
|
+
stage: "generate"
|
|
6681
|
+
});
|
|
6682
|
+
}
|
|
6683
|
+
if (options.verbose) {
|
|
6684
|
+
const failed = generatedFile2.validations.length > 0 || serverElementFile2.validations.length > 0;
|
|
6685
|
+
getLogger().info(
|
|
6686
|
+
failed ? chalk.red(`❌ ${relativePath}`) : chalk.green(`✓ ${relativePath}`)
|
|
6687
|
+
);
|
|
6688
|
+
}
|
|
6689
|
+
continue;
|
|
6690
|
+
}
|
|
6373
6691
|
if (content.includes("application/jay-params")) {
|
|
6374
6692
|
errors.push({
|
|
6375
6693
|
file: relativePath,
|
|
@@ -6461,6 +6779,9 @@ async function validateJayFiles(options = {}) {
|
|
|
6461
6779
|
suggestion: finding.suggestion
|
|
6462
6780
|
});
|
|
6463
6781
|
}
|
|
6782
|
+
for (const message of checkJsonLdScripts(parsedFile.val)) {
|
|
6783
|
+
errors.push({ file: relativePath, message, stage: "generate" });
|
|
6784
|
+
}
|
|
6464
6785
|
regionsSeen += collectRegionElements(parsedFile.val.body).length;
|
|
6465
6786
|
templateImportsSeen += parsedFile.val.headlessImports.filter(
|
|
6466
6787
|
(imp) => imp.template
|
|
@@ -6588,6 +6909,24 @@ async function validateJayFiles(options = {}) {
|
|
|
6588
6909
|
}
|
|
6589
6910
|
}
|
|
6590
6911
|
}
|
|
6912
|
+
if (options.compileOnly) {
|
|
6913
|
+
return {
|
|
6914
|
+
valid: errors.length === 0,
|
|
6915
|
+
jayHtmlFilesScanned: jayHtmlFiles.length,
|
|
6916
|
+
contractFilesScanned: contractFiles.length,
|
|
6917
|
+
errors,
|
|
6918
|
+
warnings,
|
|
6919
|
+
coverage,
|
|
6920
|
+
pluginValidators: [],
|
|
6921
|
+
designSystemCoverage,
|
|
6922
|
+
designSystemReuse: {
|
|
6923
|
+
perTemplate: {},
|
|
6924
|
+
catalogued: 0,
|
|
6925
|
+
reusedMoreThanOnce: 0,
|
|
6926
|
+
reusedOfCatalogued: 0
|
|
6927
|
+
}
|
|
6928
|
+
};
|
|
6929
|
+
}
|
|
6591
6930
|
const robotsTxtPath = path$1.resolve(projectRoot, "public/robots.txt");
|
|
6592
6931
|
if (!fs$1.existsSync(robotsTxtPath)) {
|
|
6593
6932
|
warnings.push({
|
|
@@ -6603,6 +6942,60 @@ async function validateJayFiles(options = {}) {
|
|
|
6603
6942
|
suggestion: "Add to .jay config:\n site:\n baseUrl: https://your-domain.com"
|
|
6604
6943
|
});
|
|
6605
6944
|
}
|
|
6945
|
+
if (!config.site?.name) {
|
|
6946
|
+
warnings.push({
|
|
6947
|
+
file: ".jay",
|
|
6948
|
+
message: "site.name not configured — Organization/WebSite JSON-LD and llms.txt will not be emitted, reducing how well search engines and agents understand the site.",
|
|
6949
|
+
suggestion: "Add to .jay config:\n site:\n name: Your Site Name\n logo: /logo.png\n description: What your site is about"
|
|
6950
|
+
});
|
|
6951
|
+
}
|
|
6952
|
+
const allowNoArd = config.validations?.["allow-no-ard"] === true;
|
|
6953
|
+
const ardResult = await checkArdFiles(projectRoot);
|
|
6954
|
+
if (ardResult.absent) {
|
|
6955
|
+
if (!allowNoArd) {
|
|
6956
|
+
warnings.push({
|
|
6957
|
+
file: "public/.well-known/ard.json",
|
|
6958
|
+
message: "public/.well-known/ard.json not found — agents cannot discover this site’s resources/APIs via Agentic Resource Discovery.",
|
|
6959
|
+
suggestion: "Author public/.well-known/ard.json (ARD v0.91), or suppress with `validations: { allow-no-ard: true }` in .jay if the site has no agent resources to advertise. See developer/seo-guide.md."
|
|
6960
|
+
});
|
|
6961
|
+
}
|
|
6962
|
+
} else {
|
|
6963
|
+
for (const finding of ardResult.findings) {
|
|
6964
|
+
if (finding.severity === "error") {
|
|
6965
|
+
errors.push({
|
|
6966
|
+
file: "public/.well-known/ard.json",
|
|
6967
|
+
message: finding.message,
|
|
6968
|
+
stage: "generate",
|
|
6969
|
+
suggestion: finding.suggestion
|
|
6970
|
+
});
|
|
6971
|
+
} else {
|
|
6972
|
+
warnings.push({
|
|
6973
|
+
file: "public/.well-known/ard.json",
|
|
6974
|
+
message: finding.message,
|
|
6975
|
+
suggestion: finding.suggestion
|
|
6976
|
+
});
|
|
6977
|
+
}
|
|
6978
|
+
}
|
|
6979
|
+
}
|
|
6980
|
+
const allowNoErrorPages = config.validations?.["allow-no-error-pages"] === true;
|
|
6981
|
+
if (!allowNoErrorPages) {
|
|
6982
|
+
const scanResult = await scanRoutes(scanDir, {
|
|
6983
|
+
jayHtmlFilename: "page.jay-html",
|
|
6984
|
+
compFilename: "page.ts"
|
|
6985
|
+
});
|
|
6986
|
+
const errorPages = scanResult.errorPages ?? [];
|
|
6987
|
+
const tokens = new Set(errorPages.map((ep) => ep.token));
|
|
6988
|
+
const missing = [];
|
|
6989
|
+
if (!tokens.has("404") && !tokens.has("4xx")) missing.push("404");
|
|
6990
|
+
if (!tokens.has("5xx")) missing.push("5xx");
|
|
6991
|
+
if (missing.length > 0) {
|
|
6992
|
+
warnings.push({
|
|
6993
|
+
file: "src/pages",
|
|
6994
|
+
message: `No branded error page for ${missing.join(" and ")} — these responses fall back to plain text instead of your site's layout.`,
|
|
6995
|
+
suggestion: "Add pages/404/page.jay-html and pages/5xx/page.jay-html importing the `error` headless contract, or suppress with `validations: { allow-no-error-pages: true }` in .jay. See designer/error-pages-guide.md."
|
|
6996
|
+
});
|
|
6997
|
+
}
|
|
6998
|
+
}
|
|
6606
6999
|
const allowNoTemplate = /* @__PURE__ */ new Set();
|
|
6607
7000
|
let allowNoDesignSystem = false;
|
|
6608
7001
|
for (const { parsed } of parsedFiles) {
|
|
@@ -6748,6 +7141,7 @@ async function runBuildOutputValidation(args) {
|
|
|
6748
7141
|
baseUrl
|
|
6749
7142
|
})
|
|
6750
7143
|
);
|
|
7144
|
+
findings.push(...checkBuildJsonLd(loaded.manifest));
|
|
6751
7145
|
for (const instance of oracle.instances) {
|
|
6752
7146
|
const slowViewState = await loadSlowViewState(instance.cacheAbsPath);
|
|
6753
7147
|
findings.push(
|
|
@@ -6766,6 +7160,8 @@ async function runBuildOutputValidation(args) {
|
|
|
6766
7160
|
slowViewState
|
|
6767
7161
|
})
|
|
6768
7162
|
);
|
|
7163
|
+
const carryForward = await loadCarryForward(instance.cacheAbsPath);
|
|
7164
|
+
findings.push(...checkInstanceJsonLd({ instanceUrl: instance.url, carryForward }));
|
|
6769
7165
|
}
|
|
6770
7166
|
for (const finding of findings) {
|
|
6771
7167
|
if (finding.severity === "error") {
|
|
@@ -7011,7 +7407,8 @@ async function runValidate(scanPath, options) {
|
|
|
7011
7407
|
verbose: options.verbose,
|
|
7012
7408
|
json: options.json,
|
|
7013
7409
|
tier2: options.tier2,
|
|
7014
|
-
buildDir: options.buildDir
|
|
7410
|
+
buildDir: options.buildDir,
|
|
7411
|
+
compileOnly: options.compileOnly
|
|
7015
7412
|
});
|
|
7016
7413
|
printJayValidationResult(result, options);
|
|
7017
7414
|
if (!result.valid) {
|
|
@@ -7117,10 +7514,12 @@ async function resolveProductionContext(projectPath, versionOverride) {
|
|
|
7117
7514
|
const jayConfigPath = path__default.join(resolvedPath, ".jay");
|
|
7118
7515
|
let pagesBase = "./src/pages";
|
|
7119
7516
|
let siteBaseUrl;
|
|
7517
|
+
let site;
|
|
7120
7518
|
try {
|
|
7121
7519
|
const jayConfig = YAML.parse(await fs$2.readFile(jayConfigPath, "utf-8"));
|
|
7122
7520
|
pagesBase = jayConfig?.devServer?.pagesBase || pagesBase;
|
|
7123
7521
|
siteBaseUrl = jayConfig?.site?.baseUrl;
|
|
7522
|
+
site = jayConfig?.site;
|
|
7124
7523
|
} catch {
|
|
7125
7524
|
}
|
|
7126
7525
|
const version = versionOverride || await resolveVersionFromPackageJson(resolvedPath);
|
|
@@ -7130,7 +7529,8 @@ async function resolveProductionContext(projectPath, versionOverride) {
|
|
|
7130
7529
|
buildRoot: path__default.join(resolvedPath, "build"),
|
|
7131
7530
|
version,
|
|
7132
7531
|
tsConfigFilePath: path__default.join(resolvedPath, "tsconfig.json"),
|
|
7133
|
-
siteBaseUrl
|
|
7532
|
+
siteBaseUrl,
|
|
7533
|
+
site
|
|
7134
7534
|
};
|
|
7135
7535
|
}
|
|
7136
7536
|
async function resolveVersionFromPackageJson(projectRoot) {
|
|
@@ -7158,7 +7558,7 @@ async function runBuild(projectPath, options) {
|
|
|
7158
7558
|
}
|
|
7159
7559
|
const ctx = await resolveProductionContext(projectPath, options.version);
|
|
7160
7560
|
await generateDefinitionFiles(ctx);
|
|
7161
|
-
const { buildVersion } = await import("./index-
|
|
7561
|
+
const { buildVersion } = await import("./index-B5oavmfA.js");
|
|
7162
7562
|
await buildVersion({
|
|
7163
7563
|
version: ctx.version,
|
|
7164
7564
|
projectRoot: ctx.resolvedPath,
|
|
@@ -7167,7 +7567,8 @@ async function runBuild(projectPath, options) {
|
|
|
7167
7567
|
concurrency: 4,
|
|
7168
7568
|
tsConfigFilePath: ctx.tsConfigFilePath,
|
|
7169
7569
|
minify: options.minify,
|
|
7170
|
-
siteBaseUrl: ctx.siteBaseUrl
|
|
7570
|
+
siteBaseUrl: ctx.siteBaseUrl,
|
|
7571
|
+
site: ctx.site
|
|
7171
7572
|
});
|
|
7172
7573
|
}
|
|
7173
7574
|
async function generateDefinitionFiles(ctx) {
|
|
@@ -7234,7 +7635,8 @@ async function runRebuild(projectPath, options) {
|
|
|
7234
7635
|
version: ctx.version,
|
|
7235
7636
|
target,
|
|
7236
7637
|
tsConfigFilePath: ctx.tsConfigFilePath,
|
|
7237
|
-
siteBaseUrl: ctx.siteBaseUrl
|
|
7638
|
+
siteBaseUrl: ctx.siteBaseUrl,
|
|
7639
|
+
site: ctx.site
|
|
7238
7640
|
});
|
|
7239
7641
|
if (result.errors.length > 0) {
|
|
7240
7642
|
for (const err of result.errors) {
|
|
@@ -8286,9 +8688,16 @@ program.command("validate").description("Validate all .jay-html and .jay-contrac
|
|
|
8286
8688
|
).addOption(new Option("--t2", "Alias for --tier-2").hideHelp()).option(
|
|
8287
8689
|
"--build-dir <dir>",
|
|
8288
8690
|
"Build backend dir or build root for Tier 2 (default: highest build/v*)"
|
|
8691
|
+
).option(
|
|
8692
|
+
"--compile-only",
|
|
8693
|
+
"Parse and compile .jay-html only (no project rules — for plugin route source checks)"
|
|
8289
8694
|
).action(async (options) => {
|
|
8290
8695
|
try {
|
|
8291
|
-
await runValidate(options.path, {
|
|
8696
|
+
await runValidate(options.path, {
|
|
8697
|
+
...options,
|
|
8698
|
+
tier2: options.tier2 || options.t2,
|
|
8699
|
+
compileOnly: options.compileOnly
|
|
8700
|
+
});
|
|
8292
8701
|
} catch (error) {
|
|
8293
8702
|
if (options.json) {
|
|
8294
8703
|
getLogger().important(
|
|
@@ -8350,6 +8759,7 @@ export {
|
|
|
8350
8759
|
listContracts,
|
|
8351
8760
|
loadConfig,
|
|
8352
8761
|
materializeContracts,
|
|
8762
|
+
setWellKnownHeaders,
|
|
8353
8763
|
startDevServer,
|
|
8354
8764
|
updateConfig
|
|
8355
8765
|
};
|