@jay-framework/jay-stack-cli 0.23.1 → 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/agent-kit-template/developer/component-refs.md +129 -12
- package/dist/index.d.ts +4 -1
- package/dist/index.js +208 -6
- package/package.json +10 -10
|
@@ -124,6 +124,8 @@ This generates both a ViewState field and a ref.
|
|
|
124
124
|
|
|
125
125
|
Refs are the **only supported path** from TypeScript to elements Jay renders. Direct `document` access bypasses the framework and can break rendering, updates, and performance.
|
|
126
126
|
|
|
127
|
+
`jay-stack validate` warns on `document.querySelector`, `document.getElementById`, `document.createElement`, and `document.addEventListener` in page and component `.ts` files. Suppress with `// jay-dom: allow` on the same line when an exception is genuinely needed.
|
|
128
|
+
|
|
127
129
|
### Do
|
|
128
130
|
|
|
129
131
|
- Declare elements in **jay-html** with `ref="..."`.
|
|
@@ -134,20 +136,135 @@ Refs are the **only supported path** from TypeScript to elements Jay renders. Di
|
|
|
134
136
|
|
|
135
137
|
### Avoid
|
|
136
138
|
|
|
137
|
-
- `document.querySelector` / `getElementById` to find template elements
|
|
138
|
-
- `document.createElement` + `appendChild` for UI that belongs in jay-html
|
|
139
|
-
- `document.addEventListener
|
|
139
|
+
- `document.querySelector` / `getElementById` to find template elements — use refs
|
|
140
|
+
- `document.createElement` + `appendChild` for UI that belongs in jay-html — use `forEach` with ViewState
|
|
141
|
+
- `document.addEventListener` for global events — use the root ref pattern (below)
|
|
140
142
|
|
|
141
|
-
|
|
143
|
+
## Root ref pattern — replacing `document.addEventListener`
|
|
144
|
+
|
|
145
|
+
Wrap the page content in a shell element with a ref. Use capture-phase listeners on the shell to intercept events before they reach children — functionally equivalent to `document.addEventListener`.
|
|
146
|
+
|
|
147
|
+
### Setup
|
|
148
|
+
|
|
149
|
+
```html
|
|
150
|
+
<!-- jay-html -->
|
|
151
|
+
<div ref="shell" class="page-shell">... entire page content ...</div>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
```yaml
|
|
155
|
+
# Contract
|
|
156
|
+
- tag: shell
|
|
157
|
+
type: interactive
|
|
158
|
+
elementType: HTMLDivElement
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Global keyboard navigation
|
|
162
|
+
|
|
163
|
+
Instead of `document.addEventListener('keydown', ...)`:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
.withInteractive(function Page(_props, refs) {
|
|
167
|
+
let keyboardNav = false;
|
|
168
|
+
|
|
169
|
+
refs.shell.onkeydown(({ event }) => {
|
|
170
|
+
if (event.key === 'Tab') keyboardNav = true;
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
refs.shell.onmousedown(() => {
|
|
174
|
+
keyboardNav = false;
|
|
175
|
+
});
|
|
176
|
+
})
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Events bubble up from children to the shell — a handler on the shell sees all keyboard and mouse events from the entire page.
|
|
180
|
+
|
|
181
|
+
Use `refs.shell.addEventListener(type, handler, { capture: true })` only when you need to intercept events _before_ children handle them (e.g., preventing default on specific keys).
|
|
182
|
+
|
|
183
|
+
### Focus management (scroll into view)
|
|
184
|
+
|
|
185
|
+
Instead of `document.addEventListener('focusin', ...)`:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
refs.shell.onfocusin(({ event }) => {
|
|
189
|
+
if (!keyboardNav) return;
|
|
190
|
+
const el = event.target as HTMLElement;
|
|
191
|
+
refs.shell.exec$(() => {
|
|
192
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Detecting pointer leaving the page
|
|
198
|
+
|
|
199
|
+
Instead of `document.addEventListener('mouseleave', ...)`:
|
|
142
200
|
|
|
143
|
-
|
|
201
|
+
```typescript
|
|
202
|
+
refs.shell.onpointerleave(({ event }) => {
|
|
203
|
+
// Pointer left the shell — equivalent to leaving the viewport
|
|
204
|
+
// if the shell covers the full viewport
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Ensure the shell has no margin/padding gap so it covers the full viewport. `pointer-events: auto` (the default) is sufficient.
|
|
209
|
+
|
|
210
|
+
### Finding elements by class → use refs
|
|
211
|
+
|
|
212
|
+
Instead of `document.querySelector('.site-header')`:
|
|
213
|
+
|
|
214
|
+
```html
|
|
215
|
+
<!-- jay-html -->
|
|
216
|
+
<header ref="siteHeader" class="site-header">...</header>
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
refs.siteHeader.onclick(() => {
|
|
221
|
+
/* ... */
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
refs.siteHeader.exec$((el) => {
|
|
225
|
+
el.classList.add('is-hidden');
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Dynamic lists → use forEach
|
|
144
230
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
231
|
+
Instead of creating elements with `document.createElement` in a loop:
|
|
232
|
+
|
|
233
|
+
```html
|
|
234
|
+
<!-- jay-html -->
|
|
235
|
+
<div forEach="cards" trackBy="id" class="card-grid">
|
|
236
|
+
<div class="card">
|
|
237
|
+
<img src="{imageUrl}" alt="{title}" />
|
|
238
|
+
<span>{title}</span>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Update the ViewState to add/remove cards — the framework handles DOM creation.
|
|
244
|
+
|
|
245
|
+
### Using `exec$` for native DOM APIs
|
|
246
|
+
|
|
247
|
+
For DOM operations that refs don't wrap (scroll, focus, measurements), use `exec$` inside an event handler:
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
refs.myInput.onclick(() => {
|
|
251
|
+
refs.myInput.exec$((el) => {
|
|
252
|
+
el.focus();
|
|
253
|
+
el.select();
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
refs.scrollContainer.exec$((el) => {
|
|
258
|
+
el.scrollTo({ top: 0, behavior: 'smooth' });
|
|
259
|
+
});
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Rare `document` exceptions
|
|
150
263
|
|
|
151
|
-
|
|
264
|
+
Use only when no ref can exist, with `// jay-dom: allow` to suppress the validation warning:
|
|
152
265
|
|
|
153
|
-
|
|
266
|
+
| Case | Example |
|
|
267
|
+
| ---------------------- | --------------------------------------------------------------------- |
|
|
268
|
+
| Offscreen processing | `document.createElement('canvas') // jay-dom: allow` for image export |
|
|
269
|
+
| Coordinate hit-testing | `document.elementFromPoint(...) // jay-dom: allow` during drag |
|
|
270
|
+
| Tests | `document.dispatchEvent // jay-dom: allow` in Vitest |
|
package/dist/index.d.ts
CHANGED
|
@@ -17,9 +17,12 @@ interface JayConfig {
|
|
|
17
17
|
publicFolder?: string;
|
|
18
18
|
configBase?: string;
|
|
19
19
|
};
|
|
20
|
+
site?: {
|
|
21
|
+
baseUrl?: string;
|
|
22
|
+
};
|
|
20
23
|
}
|
|
21
24
|
declare function loadConfig(): JayConfig;
|
|
22
|
-
declare function getConfigWithDefaults(config: JayConfig): Required<JayConfig>;
|
|
25
|
+
declare function getConfigWithDefaults(config: JayConfig): Required<Pick<JayConfig, 'devServer'>> & Pick<JayConfig, 'site'>;
|
|
23
26
|
declare function updateConfig(updates: Partial<JayConfig>): void;
|
|
24
27
|
|
|
25
28
|
export { type JayConfig, type StartDevServerOptions, getConfigWithDefaults, loadConfig, startDevServer, updateConfig };
|
package/dist/index.js
CHANGED
|
@@ -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) {
|
|
@@ -2366,6 +2385,96 @@ async function validatePackageJson(context, result) {
|
|
|
2366
2385
|
});
|
|
2367
2386
|
}
|
|
2368
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
|
+
}
|
|
2369
2478
|
async function validateDynamicContracts(context, result) {
|
|
2370
2479
|
const { dynamic_contracts } = context.manifest;
|
|
2371
2480
|
if (!dynamic_contracts) return;
|
|
@@ -2392,6 +2501,13 @@ async function validateDynamicContracts(context, result) {
|
|
|
2392
2501
|
suggestion: `Create generator file at ${generatorPath}.ts`
|
|
2393
2502
|
});
|
|
2394
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
|
+
});
|
|
2395
2511
|
}
|
|
2396
2512
|
}
|
|
2397
2513
|
if (config.component) {
|
|
@@ -2721,6 +2837,65 @@ function checkRefElementTypes(jayHtml, file) {
|
|
|
2721
2837
|
walkElement(jayHtml.body, []);
|
|
2722
2838
|
return warnings;
|
|
2723
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
|
+
}
|
|
2724
2899
|
const PARSE_PARAM = /^\[(\[)?(\.\.\.)?([^\]]+)\]?\]$/;
|
|
2725
2900
|
function extractRouteParams(filePath, pagesBase) {
|
|
2726
2901
|
const relative = path.relative(pagesBase, filePath);
|
|
@@ -3143,6 +3318,18 @@ async function validateJayFiles(options = {}) {
|
|
|
3143
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.'
|
|
3144
3319
|
});
|
|
3145
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
|
+
}
|
|
3146
3333
|
const routeParamWarnings = checkRouteParams(parsedFile.val, jayFile, scanDir);
|
|
3147
3334
|
for (const msg of routeParamWarnings) {
|
|
3148
3335
|
warnings.push({ file: relativePath, message: msg });
|
|
@@ -3207,6 +3394,21 @@ async function validateJayFiles(options = {}) {
|
|
|
3207
3394
|
}
|
|
3208
3395
|
}
|
|
3209
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
|
+
}
|
|
3210
3412
|
const pluginValidators = await runPluginValidators(projectRoot, parsedFiles, errors, warnings);
|
|
3211
3413
|
return {
|
|
3212
3414
|
valid: errors.length === 0,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jay-framework/jay-stack-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -25,14 +25,14 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@inquirer/prompts": "^8.5.2",
|
|
28
|
-
"@jay-framework/compiler-jay-html": "^0.
|
|
29
|
-
"@jay-framework/compiler-shared": "^0.
|
|
30
|
-
"@jay-framework/dev-server": "^0.
|
|
31
|
-
"@jay-framework/fullstack-component": "^0.
|
|
32
|
-
"@jay-framework/logger": "^0.
|
|
33
|
-
"@jay-framework/plugin-validator": "^0.
|
|
34
|
-
"@jay-framework/production-server": "^0.
|
|
35
|
-
"@jay-framework/stack-server-runtime": "^0.
|
|
28
|
+
"@jay-framework/compiler-jay-html": "^0.24.0",
|
|
29
|
+
"@jay-framework/compiler-shared": "^0.24.0",
|
|
30
|
+
"@jay-framework/dev-server": "^0.24.0",
|
|
31
|
+
"@jay-framework/fullstack-component": "^0.24.0",
|
|
32
|
+
"@jay-framework/logger": "^0.24.0",
|
|
33
|
+
"@jay-framework/plugin-validator": "^0.24.0",
|
|
34
|
+
"@jay-framework/production-server": "^0.24.0",
|
|
35
|
+
"@jay-framework/stack-server-runtime": "^0.24.0",
|
|
36
36
|
"chalk": "^4.1.2",
|
|
37
37
|
"commander": "^14.0.0",
|
|
38
38
|
"express": "^5.0.1",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"yaml": "^2.3.4"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@jay-framework/dev-environment": "^0.
|
|
46
|
+
"@jay-framework/dev-environment": "^0.24.0",
|
|
47
47
|
"@types/express": "^5.0.2",
|
|
48
48
|
"@types/node": "^22.15.21",
|
|
49
49
|
"nodemon": "^3.0.3",
|