@jay-framework/dev-server 0.25.0 → 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/dist/index.d.ts CHANGED
@@ -186,6 +186,13 @@ interface DevServer {
186
186
  freezeStore?: FreezeStore;
187
187
  /** Public API for design board applications and CLI (DL#128) */
188
188
  service: DevServerService;
189
+ /**
190
+ * Express handler that renders the designed error page for a status (DL#218). Mount
191
+ * `errorPageHandler(404)` as a catch-all **after** all route registrations so unmatched paths
192
+ * get the branded 404 instead of Express's default. Falls back to plain text when no error page
193
+ * matches the status.
194
+ */
195
+ errorPageHandler: (status: number) => RequestHandler;
189
196
  }
190
197
  declare function mkDevServer(rawOptions: DevServerOptions): Promise<DevServer>;
191
198
 
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ import { createRequire as createRequire$1 } from "node:module";
13
13
  import * as fs from "node:fs";
14
14
  import fs__default$1 from "node:fs";
15
15
  import { scanRoutes, createRoute, routeToExpressRoute } from "@jay-framework/stack-route-scanner";
16
- import { makePassthroughInstanceComponent, actionRegistry, scanPlugins, serializeHeadTags, mergeHeadTags, discoverPluginsWithInit, sortPluginsByDependencies, executePluginServerInits, runInitCallbacks, runShutdownCallbacks, clearLifecycleCallbacks, clearServiceRegistry, clearClientInitData, runLoadParams, DevSlowlyChangingPhase, preparePluginClientInits, registerService, getServiceRegistry, parseCookies, renderFastChangingData, getClientInitData, slowRenderInstances, normalizeInstancePropNames } from "@jay-framework/stack-server-runtime";
16
+ import { makePassthroughInstanceComponent, actionRegistry, scanPlugins, serializeHeadTags, mergeHeadTags, serializeScripts, discoverPluginsWithInit, sortPluginsByDependencies, executePluginServerInits, runInitCallbacks, runShutdownCallbacks, clearLifecycleCallbacks, clearServiceRegistry, clearClientInitData, runLoadParams, DevSlowlyChangingPhase, preparePluginClientInits, registerService, getServiceRegistry, parseCookies, renderFastChangingData, getClientInitData, slowRenderInstances, normalizeInstancePropNames, resolveErrorPageToken, buildErrorViewState } from "@jay-framework/stack-server-runtime";
17
17
  import * as fs$1 from "node:fs/promises";
18
18
  import fs__default from "node:fs/promises";
19
19
  import jsBeautify from "js-beautify";
@@ -2307,8 +2307,7 @@ async function generateSSRPageHtml(vite, jayHtmlContent, jayHtmlFilename, jayHtm
2307
2307
  const tagSources = [];
2308
2308
  if (headTags && headTags.length > 0) tagSources.push(headTags);
2309
2309
  const headMetaTags = headMetaToHeadTags(cached.headMeta, viewState);
2310
- if (headMetaTags.length > 0) tagSources.push(headMetaTags);
2311
- const mergedTags = mergeHeadTags(tagSources);
2310
+ const mergedTags = mergeHeadTags(tagSources, headMetaTags);
2312
2311
  const hasCustomTitle = mergedTags.some((t) => t.tag.toLowerCase() === "title");
2313
2312
  const titleTag = hasCustomTitle ? "" : " <title>Vite + TS</title>\n";
2314
2313
  const headTagsHtml = mergedTags.length > 0 ? serializeHeadTags(mergedTags) : "";
@@ -2403,19 +2402,6 @@ ${headExtras ? headExtras + "\n" : ""} <style>
2403
2402
  </body>
2404
2403
  </html>`;
2405
2404
  }
2406
- function serializeScripts(scripts, position) {
2407
- if (!scripts) return "";
2408
- const filtered = scripts.filter((s2) => s2.position === position);
2409
- if (filtered.length === 0) return "";
2410
- return filtered.map((s2) => {
2411
- const attrs = Object.entries(s2.attributes).map(([k, v]) => v === "" ? k : `${k}="${v}"`).join(" ");
2412
- const attrStr = attrs ? " " + attrs : "";
2413
- if (s2.src) {
2414
- return ` <script src="${s2.src}"${attrStr}><\/script>`;
2415
- }
2416
- return ` <script${attrStr}>${s2.inline}<\/script>`;
2417
- }).join("\n");
2418
- }
2419
2405
  function rebaseRelativeImports(code, fromDir, toDir) {
2420
2406
  return code.replace(/from "(\.\.\/[^"]+)"/g, (_match, relPath) => {
2421
2407
  const absolutePath = path__default.resolve(fromDir, relPath);
@@ -4419,10 +4405,22 @@ class DevServerService {
4419
4405
  let _watchLinkedFiles = () => {
4420
4406
  };
4421
4407
  async function initRoutes(pagesBaseFolder) {
4422
- return await scanRoutes(pagesBaseFolder, {
4408
+ const { routes } = await scanRoutes(pagesBaseFolder, {
4409
+ jayHtmlFilename: "page.jay-html",
4410
+ compFilename: "page.ts"
4411
+ });
4412
+ return routes;
4413
+ }
4414
+ async function initErrorPages(pagesBaseFolder) {
4415
+ const { errorPages } = await scanRoutes(pagesBaseFolder, {
4423
4416
  jayHtmlFilename: "page.jay-html",
4424
4417
  compFilename: "page.ts"
4425
4418
  });
4419
+ const map = /* @__PURE__ */ new Map();
4420
+ for (const ep of errorPages) {
4421
+ map.set(ep.token, createRoute(`/_errors/${ep.token}`, ep.jayHtmlPath, ep.compPath));
4422
+ }
4423
+ return map;
4426
4424
  }
4427
4425
  async function scanPluginRoutes(projectRoot, projectRoutes) {
4428
4426
  const plugins = await scanPlugins({ projectRoot, includeDevDeps: true });
@@ -4552,12 +4550,81 @@ function defaults(options) {
4552
4550
  httpServer: options.httpServer
4553
4551
  };
4554
4552
  }
4555
- function handleOtherResponseCodes(res, renderedResult) {
4556
- if (renderedResult.kind === `ServerError`)
4557
- res.status(renderedResult.status).end("server error");
4558
- else if (renderedResult.kind === `ClientError`)
4559
- res.status(renderedResult.status).end("client error");
4560
- else res.status(renderedResult.status).end("redirect to " + renderedResult.location);
4553
+ let _renderDevErrorPage = async (res, info) => {
4554
+ if (!res.headersSent)
4555
+ res.status(info.status).type("text/plain").send(info.message || info.title || "Error");
4556
+ };
4557
+ async function renderDevErrorPage(vite, options, errorRoutes, projectInit, allPluginsWithInit, allPluginClientInits, res, info) {
4558
+ const plainText = () => {
4559
+ if (!res.headersSent)
4560
+ res.status(info.status).type("text/plain").send(info.message || info.title || "Error");
4561
+ };
4562
+ try {
4563
+ const token = resolveErrorPageToken(errorRoutes.keys(), info.status);
4564
+ if (!token) return plainText();
4565
+ const route = errorRoutes.get(token);
4566
+ const partsResult = await loadPageParts(
4567
+ vite,
4568
+ route,
4569
+ options.pagesRootFolder,
4570
+ options.projectRootFolder,
4571
+ options.jayRollupConfig
4572
+ );
4573
+ if (!partsResult.val) {
4574
+ getLogger().warn(
4575
+ `[DevServer] Error page "${token}" failed to load: ${partsResult.validations.join("; ")}`
4576
+ );
4577
+ return plainText();
4578
+ }
4579
+ const { parts, clientTrackByMap, usedPackages, scripts } = partsResult.val;
4580
+ const pluginsForPage = filterPluginsForPage(
4581
+ allPluginClientInits,
4582
+ allPluginsWithInit,
4583
+ usedPackages
4584
+ );
4585
+ const fullViewState = buildErrorViewState(info, {});
4586
+ await sendResponse(
4587
+ vite,
4588
+ res,
4589
+ info.url,
4590
+ route.jayHtmlPath,
4591
+ route.jayHtmlPath,
4592
+ parts,
4593
+ fullViewState,
4594
+ {},
4595
+ clientTrackByMap,
4596
+ projectInit,
4597
+ pluginsForPage,
4598
+ options,
4599
+ routeToExpressRoute(route),
4600
+ getRouteDir(route),
4601
+ {},
4602
+ void 0,
4603
+ void 0,
4604
+ void 0,
4605
+ scripts,
4606
+ info.status
4607
+ );
4608
+ } catch (err) {
4609
+ getLogger().error(
4610
+ `[DevServer] Error page render failed (${info.status} ${info.url}): ${err?.message}`
4611
+ );
4612
+ plainText();
4613
+ }
4614
+ }
4615
+ async function handleOtherResponseCodes(res, renderedResult, url) {
4616
+ if (renderedResult.kind === `Redirect`) {
4617
+ res.status(renderedResult.status).end("redirect to " + renderedResult.location);
4618
+ return;
4619
+ }
4620
+ await _renderDevErrorPage(res, {
4621
+ status: renderedResult.status,
4622
+ url,
4623
+ message: renderedResult.message,
4624
+ title: renderedResult.title,
4625
+ code: renderedResult.code,
4626
+ details: renderedResult.details
4627
+ });
4561
4628
  }
4562
4629
  function filterPluginsForPage(allPluginClientInits, allPluginsWithInit, usedPackages) {
4563
4630
  const pluginsByPackage = /* @__PURE__ */ new Map();
@@ -4685,7 +4752,12 @@ function mkRoute(route, vite, slowlyPhase, options, slowRenderCache, freezeStore
4685
4752
  } catch (e2) {
4686
4753
  vite?.ssrFixStacktrace(e2);
4687
4754
  getLogger().error(e2.stack);
4688
- res.status(500).end(e2.stack);
4755
+ const errUrl = (req.originalUrl || req.url || "/").split("?")[0];
4756
+ await _renderDevErrorPage(res, {
4757
+ status: 500,
4758
+ url: errUrl,
4759
+ message: e2.message
4760
+ });
4689
4761
  timing?.end();
4690
4762
  }
4691
4763
  };
@@ -4748,7 +4820,7 @@ async function handleCachedRequest(vite, route, options, cachedEntry, pageParams
4748
4820
  );
4749
4821
  timing?.recordFastRender(Date.now() - fastStart);
4750
4822
  if (renderedFast.kind !== "PhaseOutput") {
4751
- handleOtherResponseCodes(res, renderedFast);
4823
+ await handleOtherResponseCodes(res, renderedFast, url);
4752
4824
  timing?.end();
4753
4825
  return;
4754
4826
  }
@@ -4824,7 +4896,7 @@ async function handlePreRenderRequest(vite, route, options, slowlyPhase, slowRen
4824
4896
  );
4825
4897
  if (renderedSlowly.kind !== "PhaseOutput") {
4826
4898
  timing?.recordSlowRender(Date.now() - slowStart);
4827
- handleOtherResponseCodes(res, renderedSlowly);
4899
+ await handleOtherResponseCodes(res, renderedSlowly, url);
4828
4900
  timing?.end();
4829
4901
  return;
4830
4902
  }
@@ -4934,7 +5006,7 @@ async function handleClientOnlyRequest(vite, route, options, slowlyPhase, pagePa
4934
5006
  );
4935
5007
  if (renderedSlowly.kind !== "PhaseOutput") {
4936
5008
  timing?.recordSlowRender(Date.now() - slowStart);
4937
- handleOtherResponseCodes(res, renderedSlowly);
5009
+ await handleOtherResponseCodes(res, renderedSlowly, url);
4938
5010
  timing?.end();
4939
5011
  return;
4940
5012
  }
@@ -4955,7 +5027,7 @@ async function handleClientOnlyRequest(vite, route, options, slowlyPhase, pagePa
4955
5027
  );
4956
5028
  timing?.recordFastRender(Date.now() - fastStart);
4957
5029
  if (renderedFast.kind !== "PhaseOutput") {
4958
- handleOtherResponseCodes(res, renderedFast);
5030
+ await handleOtherResponseCodes(res, renderedFast, url);
4959
5031
  timing?.end();
4960
5032
  return;
4961
5033
  }
@@ -4999,7 +5071,7 @@ async function handleClientOnlyRequest(vite, route, options, slowlyPhase, pagePa
4999
5071
  res.status(200).set({ "Content-Type": "text/html" }).send(compiledPageHtml);
5000
5072
  timing?.end();
5001
5073
  }
5002
- async function sendResponse(vite, res, url, jayHtmlPath, sourceJayHtmlPath, pageParts, viewState, carryForward, clientTrackByMap, projectInit, pluginsForPage, options, routePattern, routeDir, slowViewState, timing, preLoadedContent, headTags, scripts) {
5074
+ async function sendResponse(vite, res, url, jayHtmlPath, sourceJayHtmlPath, pageParts, viewState, carryForward, clientTrackByMap, projectInit, pluginsForPage, options, routePattern, routeDir, slowViewState, timing, preLoadedContent, headTags, scripts, statusCode = 200) {
5003
5075
  let pageHtml;
5004
5076
  try {
5005
5077
  const jayHtmlDir = path__default.dirname(jayHtmlPath);
@@ -5062,7 +5134,7 @@ async function sendResponse(vite, res, url, jayHtmlPath, sourceJayHtmlPath, page
5062
5134
  const viteStart = Date.now();
5063
5135
  const compiledPageHtml = await vite.transformIndexHtml(!!url ? url : "/", pageHtml);
5064
5136
  timing?.recordViteClient(Date.now() - viteStart);
5065
- res.status(200).set({ "Content-Type": "text/html" }).send(compiledPageHtml);
5137
+ res.status(statusCode).set({ "Content-Type": "text/html" }).send(compiledPageHtml);
5066
5138
  timing?.end();
5067
5139
  }
5068
5140
  async function handleFrozenRequest(vite, route, options, freezeStore, slowRenderCache, freezeId, pageParams, format, res, url, timing) {
@@ -5244,6 +5316,7 @@ async function mkDevServer(rawOptions) {
5244
5316
  setupActionRouter(vite, buildFolder);
5245
5317
  const projectRoutes = await initRoutes(pagesRootFolder);
5246
5318
  const filteredProjectRoutes = buildFolder ? projectRoutes.filter((route) => !route.jayHtmlPath.startsWith(buildFolder)) : projectRoutes;
5319
+ const errorRoutes = await initErrorPages(pagesRootFolder);
5247
5320
  const pluginRoutes = await scanPluginRoutes(projectRootFolder, filteredProjectRoutes);
5248
5321
  const routes = [...filteredProjectRoutes, ...pluginRoutes];
5249
5322
  const slowlyPhase = new DevSlowlyChangingPhase();
@@ -5257,6 +5330,16 @@ async function mkDevServer(rawOptions) {
5257
5330
  const projectInit = lifecycleManager.getProjectInit() ?? void 0;
5258
5331
  const pluginsWithInit = lifecycleManager.getPluginsWithInit();
5259
5332
  const pluginClientInits = preparePluginClientInits(pluginsWithInit);
5333
+ _renderDevErrorPage = (res, info) => renderDevErrorPage(
5334
+ vite,
5335
+ options,
5336
+ errorRoutes,
5337
+ projectInit,
5338
+ pluginsWithInit,
5339
+ pluginClientInits,
5340
+ res,
5341
+ info
5342
+ );
5260
5343
  const freezeStore = buildFolder ? new FreezeStore(buildFolder) : void 0;
5261
5344
  if (freezeStore) {
5262
5345
  setupFreezeEndpoint(vite, freezeStore);
@@ -5328,7 +5411,11 @@ async function mkDevServer(rawOptions) {
5328
5411
  routes: devServerRoutes,
5329
5412
  lifecycleManager,
5330
5413
  freezeStore,
5331
- service
5414
+ service,
5415
+ errorPageHandler: (status) => async (req, res) => {
5416
+ const url = (req.originalUrl || req.url || "/").split("?")[0];
5417
+ await _renderDevErrorPage(res, { status, url });
5418
+ }
5332
5419
  };
5333
5420
  }
5334
5421
  function setupGracefulShutdown(lifecycleManager) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jay-framework/dev-server",
3
- "version": "0.25.0",
3
+ "version": "0.25.2",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "main": "dist/index.js",
@@ -23,24 +23,24 @@
23
23
  "test:watch": "vitest"
24
24
  },
25
25
  "dependencies": {
26
- "@jay-framework/compiler-jay-stack": "^0.25.0",
27
- "@jay-framework/compiler-shared": "^0.25.0",
28
- "@jay-framework/component": "^0.25.0",
29
- "@jay-framework/fullstack-component": "^0.25.0",
30
- "@jay-framework/logger": "^0.25.0",
31
- "@jay-framework/runtime": "^0.25.0",
32
- "@jay-framework/stack-client-runtime": "^0.25.0",
33
- "@jay-framework/stack-route-scanner": "^0.25.0",
34
- "@jay-framework/stack-server-build": "^0.25.0",
35
- "@jay-framework/stack-server-runtime": "^0.25.0",
36
- "@jay-framework/view-state-merge": "^0.25.0",
26
+ "@jay-framework/compiler-jay-stack": "^0.25.2",
27
+ "@jay-framework/compiler-shared": "^0.25.2",
28
+ "@jay-framework/component": "^0.25.2",
29
+ "@jay-framework/fullstack-component": "^0.25.2",
30
+ "@jay-framework/logger": "^0.25.2",
31
+ "@jay-framework/runtime": "^0.25.2",
32
+ "@jay-framework/stack-client-runtime": "^0.25.2",
33
+ "@jay-framework/stack-route-scanner": "^0.25.2",
34
+ "@jay-framework/stack-server-build": "^0.25.2",
35
+ "@jay-framework/stack-server-runtime": "^0.25.2",
36
+ "@jay-framework/view-state-merge": "^0.25.2",
37
37
  "busboy": "^1.6.0",
38
38
  "vite": "^5.0.11"
39
39
  },
40
40
  "devDependencies": {
41
- "@jay-framework/dev-environment": "^0.25.0",
42
- "@jay-framework/jay-cli": "^0.25.0",
43
- "@jay-framework/stack-client-runtime": "^0.25.0",
41
+ "@jay-framework/dev-environment": "^0.25.2",
42
+ "@jay-framework/jay-cli": "^0.25.2",
43
+ "@jay-framework/stack-client-runtime": "^0.25.2",
44
44
  "@playwright/test": "^1.58.2",
45
45
  "@types/busboy": "^1.5.4",
46
46
  "@types/express": "^5.0.2",