@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.
@@ -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('mousemove'|'mouseup')` for drags (use pointer capture instead)
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
- ### Rare `document` exceptions
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
- Use only when no ref can exist, with an inline comment:
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
- | Case | Example |
146
- | ---------------------- | ----------------------------------------------------- |
147
- | Offscreen processing | `document.createElement('canvas')` for image export |
148
- | Coordinate hit-testing | `document.elementFromPoint` during cross-overlay drag |
149
- | Tests | `document.dispatchEvent` in Vitest |
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
- Global shortcuts or paste: prefer a root shell ref (`ref="appRoot"`) with capture listeners.
264
+ Use only when no ref can exist, with `// jay-dom: allow` to suppress the validation warning:
152
265
 
153
- See also: `.cursor/skills/jay-dom-refs/SKILL.md` in the jay monorepo.
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.23.1",
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.23.1",
29
- "@jay-framework/compiler-shared": "^0.23.1",
30
- "@jay-framework/dev-server": "^0.23.1",
31
- "@jay-framework/fullstack-component": "^0.23.1",
32
- "@jay-framework/logger": "^0.23.1",
33
- "@jay-framework/plugin-validator": "^0.23.1",
34
- "@jay-framework/production-server": "^0.23.1",
35
- "@jay-framework/stack-server-runtime": "^0.23.1",
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.23.1",
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",