@lolyjs/core 0.2.0-alpha.30 → 0.2.0-alpha.31

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/cli.js CHANGED
@@ -10741,6 +10741,18 @@ function writeRoutesManifest({
10741
10741
  fs6.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
10742
10742
  }
10743
10743
 
10744
+ // modules/router/index.types.ts
10745
+ var RedirectResponse = class {
10746
+ constructor(destination, permanent = false) {
10747
+ this.destination = destination;
10748
+ this.permanent = permanent;
10749
+ }
10750
+ };
10751
+ var NotFoundResponse = class {
10752
+ constructor() {
10753
+ }
10754
+ };
10755
+
10744
10756
  // modules/router/loader-routes.ts
10745
10757
  import fs7 from "fs";
10746
10758
  import path7 from "path";
@@ -13117,16 +13129,6 @@ async function runRouteServerHook(route, ctx) {
13117
13129
  // modules/server/handlers/response.ts
13118
13130
  function handleDataResponse(res, loaderResult, theme, layoutProps, pageProps, error, message) {
13119
13131
  res.setHeader("Content-Type", "application/json; charset=utf-8");
13120
- if (loaderResult.redirect) {
13121
- res.statusCode = 200;
13122
- res.end(JSON.stringify({ redirect: loaderResult.redirect }));
13123
- return;
13124
- }
13125
- if (loaderResult.notFound) {
13126
- res.statusCode = 404;
13127
- res.end(JSON.stringify({ notFound: true }));
13128
- return;
13129
- }
13130
13132
  const response = {
13131
13133
  // Combined props for backward compatibility
13132
13134
  props: loaderResult.props ?? {},
@@ -13301,6 +13303,145 @@ function mergeMetadata(base, override) {
13301
13303
  function isDataRequest(req) {
13302
13304
  return req.query && req.query.__fw_data === "1" || req.headers["x-fw-data"] === "1";
13303
13305
  }
13306
+ async function renderNotFoundPage(notFoundPage, req, res, urlPath, finalUrlPath, routerData, assetManifest, theme, clientJsPath, clientCssPath, faviconInfo, projectRoot, skipLayoutHooks, errorPage, routeChunks, env, config) {
13307
+ const ctx = {
13308
+ req,
13309
+ res,
13310
+ params: {},
13311
+ pathname: urlPath,
13312
+ locals: {},
13313
+ Redirect: (destination, permanent = false) => new RedirectResponse(destination, permanent),
13314
+ NotFound: () => new NotFoundResponse()
13315
+ };
13316
+ const layoutProps = {};
13317
+ if (!skipLayoutHooks && notFoundPage.layoutServerHooks && notFoundPage.layoutServerHooks.length > 0) {
13318
+ for (let i = 0; i < notFoundPage.layoutServerHooks.length; i++) {
13319
+ const layoutServerHook = notFoundPage.layoutServerHooks[i];
13320
+ const layoutMiddlewares = notFoundPage.layoutMiddlewares?.[i] || [];
13321
+ if (layoutMiddlewares.length > 0) {
13322
+ for (const mw of layoutMiddlewares) {
13323
+ try {
13324
+ await Promise.resolve(
13325
+ mw(ctx, async () => {
13326
+ })
13327
+ );
13328
+ } catch (error) {
13329
+ const reqLogger = getRequestLogger(req);
13330
+ const layoutFile = notFoundPage.layoutFiles[i];
13331
+ const relativeLayoutPath = layoutFile ? path19.relative(projectRoot || process.cwd(), layoutFile) : "unknown";
13332
+ reqLogger.error("Layout middleware failed for not-found page", error instanceof Error ? error : new Error(String(error)), {
13333
+ layoutIndex: i,
13334
+ layoutFile: relativeLayoutPath
13335
+ });
13336
+ throw error;
13337
+ }
13338
+ if (ctx.res.headersSent) {
13339
+ return;
13340
+ }
13341
+ }
13342
+ }
13343
+ if (layoutServerHook) {
13344
+ try {
13345
+ const layoutResult = await layoutServerHook(ctx);
13346
+ if (layoutResult instanceof RedirectResponse) {
13347
+ handleRedirect(res, { destination: layoutResult.destination, permanent: layoutResult.permanent });
13348
+ return;
13349
+ }
13350
+ if (layoutResult instanceof NotFoundResponse) {
13351
+ handleNotFound(res, urlPath);
13352
+ return;
13353
+ }
13354
+ if (layoutResult.props) {
13355
+ Object.assign(layoutProps, layoutResult.props);
13356
+ }
13357
+ } catch (error) {
13358
+ const reqLogger = getRequestLogger(req);
13359
+ const layoutFile = notFoundPage.layoutFiles[i];
13360
+ const relativeLayoutPath = layoutFile ? path19.relative(projectRoot || process.cwd(), layoutFile) : "unknown";
13361
+ reqLogger.warn("Layout server hook failed for not-found page", {
13362
+ error: error instanceof Error ? error.message : String(error),
13363
+ stack: error instanceof Error ? error.stack : void 0,
13364
+ layoutFile: relativeLayoutPath,
13365
+ layoutIndex: i
13366
+ });
13367
+ }
13368
+ }
13369
+ }
13370
+ }
13371
+ let loaderResult = await runRouteServerHook(notFoundPage, ctx);
13372
+ if (loaderResult instanceof RedirectResponse) {
13373
+ handleRedirect(res, { destination: loaderResult.destination, permanent: loaderResult.permanent });
13374
+ return;
13375
+ }
13376
+ if (loaderResult instanceof NotFoundResponse) {
13377
+ handleNotFound(res, urlPath);
13378
+ return;
13379
+ }
13380
+ const notFoundLoaderResult = loaderResult;
13381
+ if (!notFoundLoaderResult.theme) {
13382
+ notFoundLoaderResult.theme = theme;
13383
+ }
13384
+ const combinedProps = {
13385
+ ...layoutProps,
13386
+ ...notFoundLoaderResult.props || {}
13387
+ };
13388
+ const combinedLoaderResult = {
13389
+ ...notFoundLoaderResult,
13390
+ props: combinedProps
13391
+ };
13392
+ const initialData = buildInitialData(finalUrlPath, {}, combinedLoaderResult);
13393
+ const appTree = buildAppTree(notFoundPage, {}, initialData.props);
13394
+ initialData.notFound = true;
13395
+ const nonce = res.locals.nonce || void 0;
13396
+ const entrypointFiles = [];
13397
+ if (assetManifest?.entrypoints?.client) {
13398
+ entrypointFiles.push(...assetManifest.entrypoints.client.map((file) => `${STATIC_PATH}/${file}`));
13399
+ }
13400
+ const documentTree = createDocumentTree({
13401
+ appTree,
13402
+ initialData,
13403
+ routerData,
13404
+ meta: combinedLoaderResult.metadata ?? null,
13405
+ titleFallback: "Not found",
13406
+ descriptionFallback: "Loly demo",
13407
+ chunkHref: null,
13408
+ entrypointFiles,
13409
+ theme,
13410
+ clientJsPath,
13411
+ clientCssPath,
13412
+ nonce,
13413
+ faviconPath: faviconInfo?.path || null,
13414
+ faviconType: faviconInfo?.type || null
13415
+ });
13416
+ let didError = false;
13417
+ const { pipe, abort } = renderToPipeableStream(documentTree, {
13418
+ onShellReady() {
13419
+ if (didError || res.headersSent) return;
13420
+ res.statusCode = 404;
13421
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
13422
+ pipe(res);
13423
+ },
13424
+ onShellError(err) {
13425
+ didError = true;
13426
+ const reqLogger = getRequestLogger(req);
13427
+ reqLogger.error("SSR shell error", err, { route: "not-found" });
13428
+ if (!res.headersSent && errorPage) {
13429
+ renderErrorPageWithStream(errorPage, req, res, err, routeChunks || {}, theme, projectRoot, env, config, notFoundPage);
13430
+ } else if (!res.headersSent) {
13431
+ res.statusCode = 500;
13432
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
13433
+ res.end("<!doctype html><h1>Internal Server Error</h1>");
13434
+ }
13435
+ abort();
13436
+ },
13437
+ onError(err) {
13438
+ didError = true;
13439
+ const reqLogger = getRequestLogger(req);
13440
+ reqLogger.error("SSR error", err, { route: "not-found" });
13441
+ }
13442
+ });
13443
+ req.on("close", () => abort());
13444
+ }
13304
13445
  async function handlePageRequest(options) {
13305
13446
  try {
13306
13447
  await handlePageRequestInternal(options);
@@ -13308,7 +13449,7 @@ async function handlePageRequest(options) {
13308
13449
  const { errorPage, req, res, routeChunks, theme, projectRoot } = options;
13309
13450
  const reqLogger = getRequestLogger(req);
13310
13451
  if (errorPage) {
13311
- await renderErrorPageWithStream(errorPage, req, res, error, routeChunks || {}, theme, projectRoot, options.env, options.config);
13452
+ await renderErrorPageWithStream(errorPage, req, res, error, routeChunks || {}, theme, projectRoot, options.env, options.config, options.notFoundPage);
13312
13453
  } else {
13313
13454
  reqLogger.error("Unhandled error in page request", error, {
13314
13455
  urlPath: options.urlPath,
@@ -13418,135 +13559,25 @@ async function handlePageRequestInternal(options) {
13418
13559
  return;
13419
13560
  }
13420
13561
  if (notFoundPage) {
13421
- const ctx2 = {
13562
+ await renderNotFoundPage(
13563
+ notFoundPage,
13422
13564
  req,
13423
13565
  res,
13424
- params: {},
13425
- pathname: urlPath,
13426
- locals: {}
13427
- };
13428
- const layoutProps2 = {};
13429
- if (!skipLayoutHooks && notFoundPage.layoutServerHooks && notFoundPage.layoutServerHooks.length > 0) {
13430
- for (let i = 0; i < notFoundPage.layoutServerHooks.length; i++) {
13431
- const layoutServerHook = notFoundPage.layoutServerHooks[i];
13432
- const layoutMiddlewares = notFoundPage.layoutMiddlewares?.[i] || [];
13433
- if (layoutMiddlewares.length > 0) {
13434
- for (const mw of layoutMiddlewares) {
13435
- try {
13436
- await Promise.resolve(
13437
- mw(ctx2, async () => {
13438
- })
13439
- );
13440
- } catch (error) {
13441
- const reqLogger2 = getRequestLogger(req);
13442
- const layoutFile = notFoundPage.layoutFiles[i];
13443
- const relativeLayoutPath = layoutFile ? path19.relative(projectRoot || process.cwd(), layoutFile) : "unknown";
13444
- reqLogger2.error("Layout middleware failed for not-found page", error instanceof Error ? error : new Error(String(error)), {
13445
- layoutIndex: i,
13446
- layoutFile: relativeLayoutPath
13447
- });
13448
- throw error;
13449
- }
13450
- if (ctx2.res.headersSent) {
13451
- return;
13452
- }
13453
- }
13454
- }
13455
- if (layoutServerHook) {
13456
- try {
13457
- const layoutResult = await layoutServerHook(ctx2);
13458
- if (layoutResult.props) {
13459
- Object.assign(layoutProps2, layoutResult.props);
13460
- }
13461
- } catch (error) {
13462
- const reqLogger2 = getRequestLogger(req);
13463
- const layoutFile = notFoundPage.layoutFiles[i];
13464
- const relativeLayoutPath = layoutFile ? path19.relative(projectRoot || process.cwd(), layoutFile) : "unknown";
13465
- reqLogger2.warn("Layout server hook failed for not-found page", {
13466
- error: error instanceof Error ? error.message : String(error),
13467
- stack: error instanceof Error ? error.stack : void 0,
13468
- layoutFile: relativeLayoutPath,
13469
- layoutIndex: i
13470
- });
13471
- }
13472
- }
13473
- }
13474
- }
13475
- let loaderResult2 = await runRouteServerHook(notFoundPage, ctx2);
13476
- if (!loaderResult2.theme) {
13477
- loaderResult2.theme = theme;
13478
- }
13479
- const combinedProps2 = {
13480
- ...layoutProps2,
13481
- ...loaderResult2.props || {}
13482
- };
13483
- const combinedLoaderResult2 = {
13484
- ...loaderResult2,
13485
- props: combinedProps2
13486
- };
13487
- if (isDataReq) {
13488
- const pagePropsOnly = loaderResult2.props || {};
13489
- handleDataResponse(
13490
- res,
13491
- combinedLoaderResult2,
13492
- theme,
13493
- skipLayoutHooks ? null : Object.keys(layoutProps2).length > 0 ? layoutProps2 : null,
13494
- pagePropsOnly
13495
- );
13496
- return;
13497
- }
13498
- const initialData2 = buildInitialData(finalUrlPath, {}, combinedLoaderResult2);
13499
- const appTree2 = buildAppTree(notFoundPage, {}, initialData2.props);
13500
- initialData2.notFound = true;
13501
- const nonce2 = res.locals.nonce || void 0;
13502
- const entrypointFiles2 = [];
13503
- if (assetManifest?.entrypoints?.client) {
13504
- entrypointFiles2.push(...assetManifest.entrypoints.client.map((file) => `${STATIC_PATH}/${file}`));
13505
- }
13506
- const documentTree2 = createDocumentTree({
13507
- appTree: appTree2,
13508
- initialData: initialData2,
13566
+ urlPath,
13567
+ finalUrlPath,
13509
13568
  routerData,
13510
- meta: combinedLoaderResult2.metadata ?? null,
13511
- titleFallback: "Not found",
13512
- descriptionFallback: "Loly demo",
13513
- chunkHref: null,
13514
- entrypointFiles: entrypointFiles2,
13569
+ assetManifest,
13515
13570
  theme,
13516
13571
  clientJsPath,
13517
13572
  clientCssPath,
13518
- nonce: nonce2,
13519
- faviconPath: faviconInfo?.path || null,
13520
- faviconType: faviconInfo?.type || null
13521
- });
13522
- let didError2 = false;
13523
- const { pipe: pipe2, abort: abort2 } = renderToPipeableStream(documentTree2, {
13524
- onShellReady() {
13525
- if (didError2 || res.headersSent) return;
13526
- res.statusCode = 404;
13527
- res.setHeader("Content-Type", "text/html; charset=utf-8");
13528
- pipe2(res);
13529
- },
13530
- onShellError(err) {
13531
- didError2 = true;
13532
- const reqLogger2 = getRequestLogger(req);
13533
- reqLogger2.error("SSR shell error", err, { route: "not-found" });
13534
- if (!res.headersSent && errorPage) {
13535
- renderErrorPageWithStream(errorPage, req, res, err, routeChunks, theme, projectRoot, env, config);
13536
- } else if (!res.headersSent) {
13537
- res.statusCode = 500;
13538
- res.setHeader("Content-Type", "text/html; charset=utf-8");
13539
- res.end("<!doctype html><h1>Internal Server Error</h1>");
13540
- }
13541
- abort2();
13542
- },
13543
- onError(err) {
13544
- didError2 = true;
13545
- const reqLogger2 = getRequestLogger(req);
13546
- reqLogger2.error("SSR error", err, { route: "not-found" });
13547
- }
13548
- });
13549
- req.on("close", () => abort2());
13573
+ faviconInfo,
13574
+ projectRoot,
13575
+ skipLayoutHooks,
13576
+ errorPage,
13577
+ routeChunks,
13578
+ env,
13579
+ config
13580
+ );
13550
13581
  return;
13551
13582
  }
13552
13583
  handleNotFound(res, urlPath);
@@ -13559,7 +13590,9 @@ async function handlePageRequestInternal(options) {
13559
13590
  res,
13560
13591
  params: sanitizedParams,
13561
13592
  pathname: urlPath,
13562
- locals: {}
13593
+ locals: {},
13594
+ Redirect: (destination, permanent = false) => new RedirectResponse(destination, permanent),
13595
+ NotFound: () => new NotFoundResponse()
13563
13596
  };
13564
13597
  await runRouteMiddlewares(route, ctx);
13565
13598
  if (res.headersSent) {
@@ -13597,6 +13630,48 @@ async function handlePageRequestInternal(options) {
13597
13630
  if (layoutServerHook) {
13598
13631
  try {
13599
13632
  const layoutResult = await layoutServerHook(ctx);
13633
+ if (layoutResult instanceof RedirectResponse) {
13634
+ if (isDataReq) {
13635
+ res.statusCode = 200;
13636
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
13637
+ res.end(JSON.stringify({ redirect: { destination: layoutResult.destination, permanent: layoutResult.permanent } }));
13638
+ } else {
13639
+ handleRedirect(res, { destination: layoutResult.destination, permanent: layoutResult.permanent });
13640
+ }
13641
+ return;
13642
+ }
13643
+ if (layoutResult instanceof NotFoundResponse) {
13644
+ if (isDataReq) {
13645
+ res.statusCode = 200;
13646
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
13647
+ res.end(JSON.stringify({ notFound: true }));
13648
+ } else {
13649
+ if (notFoundPage) {
13650
+ await renderNotFoundPage(
13651
+ notFoundPage,
13652
+ req,
13653
+ res,
13654
+ urlPath,
13655
+ finalUrlPath,
13656
+ routerData,
13657
+ assetManifest,
13658
+ theme,
13659
+ clientJsPath,
13660
+ clientCssPath,
13661
+ faviconInfo,
13662
+ projectRoot,
13663
+ skipLayoutHooks,
13664
+ errorPage,
13665
+ routeChunks,
13666
+ env,
13667
+ config
13668
+ );
13669
+ } else {
13670
+ handleNotFound(res, urlPath);
13671
+ }
13672
+ }
13673
+ return;
13674
+ }
13600
13675
  if (layoutResult.props) {
13601
13676
  Object.assign(layoutProps, layoutResult.props);
13602
13677
  }
@@ -13621,8 +13696,51 @@ async function handlePageRequestInternal(options) {
13621
13696
  let loaderResult;
13622
13697
  try {
13623
13698
  loaderResult = await runRouteServerHook(route, ctx);
13624
- if (!loaderResult.theme) {
13625
- loaderResult.theme = theme;
13699
+ if (loaderResult instanceof RedirectResponse) {
13700
+ if (isDataReq) {
13701
+ res.statusCode = 200;
13702
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
13703
+ res.end(JSON.stringify({ redirect: { destination: loaderResult.destination, permanent: loaderResult.permanent } }));
13704
+ } else {
13705
+ handleRedirect(res, { destination: loaderResult.destination, permanent: loaderResult.permanent });
13706
+ }
13707
+ return;
13708
+ }
13709
+ if (loaderResult instanceof NotFoundResponse) {
13710
+ if (isDataReq) {
13711
+ res.statusCode = 200;
13712
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
13713
+ res.end(JSON.stringify({ notFound: true }));
13714
+ } else {
13715
+ if (notFoundPage) {
13716
+ await renderNotFoundPage(
13717
+ notFoundPage,
13718
+ req,
13719
+ res,
13720
+ urlPath,
13721
+ finalUrlPath,
13722
+ routerData,
13723
+ assetManifest,
13724
+ theme,
13725
+ clientJsPath,
13726
+ clientCssPath,
13727
+ faviconInfo,
13728
+ projectRoot,
13729
+ skipLayoutHooks,
13730
+ errorPage,
13731
+ routeChunks,
13732
+ env,
13733
+ config
13734
+ );
13735
+ } else {
13736
+ handleNotFound(res, urlPath);
13737
+ }
13738
+ }
13739
+ return;
13740
+ }
13741
+ const pageLoaderResult2 = loaderResult;
13742
+ if (!pageLoaderResult2.theme) {
13743
+ pageLoaderResult2.theme = theme;
13626
13744
  }
13627
13745
  } catch (error) {
13628
13746
  const relativePagePath = route.pageFile ? path19.relative(projectRoot || process.cwd(), route.pageFile) : "unknown";
@@ -13647,17 +13765,18 @@ async function handlePageRequestInternal(options) {
13647
13765
  return;
13648
13766
  } else {
13649
13767
  if (errorPage) {
13650
- await renderErrorPageWithStream(errorPage, req, res, error, routeChunks, theme, projectRoot, env, config);
13768
+ await renderErrorPageWithStream(errorPage, req, res, error, routeChunks, theme, projectRoot, env, config, notFoundPage);
13651
13769
  return;
13652
13770
  } else {
13653
13771
  throw error;
13654
13772
  }
13655
13773
  }
13656
13774
  }
13775
+ const pageLoaderResult = loaderResult;
13657
13776
  const combinedProps = {
13658
13777
  ...layoutProps,
13659
13778
  // Props from layouts (stable)
13660
- ...loaderResult.props || {}
13779
+ ...pageLoaderResult.props || {}
13661
13780
  // Props from page (overrides layout)
13662
13781
  };
13663
13782
  let combinedMetadata = null;
@@ -13666,18 +13785,18 @@ async function handlePageRequestInternal(options) {
13666
13785
  combinedMetadata = mergeMetadata(combinedMetadata, layoutMeta);
13667
13786
  }
13668
13787
  }
13669
- if (loaderResult.metadata) {
13670
- combinedMetadata = mergeMetadata(combinedMetadata, loaderResult.metadata);
13788
+ if (pageLoaderResult.metadata) {
13789
+ combinedMetadata = mergeMetadata(combinedMetadata, pageLoaderResult.metadata);
13671
13790
  }
13672
13791
  const combinedLoaderResult = {
13673
- ...loaderResult,
13792
+ ...pageLoaderResult,
13674
13793
  props: combinedProps,
13675
13794
  metadata: combinedMetadata,
13676
13795
  pathname: finalUrlPath
13677
13796
  // Include rewritten pathname for client-side matching
13678
13797
  };
13679
13798
  if (isDataReq) {
13680
- const pagePropsOnly = loaderResult.props || {};
13799
+ const pagePropsOnly = pageLoaderResult.props || {};
13681
13800
  handleDataResponse(
13682
13801
  res,
13683
13802
  combinedLoaderResult,
@@ -13687,18 +13806,6 @@ async function handlePageRequestInternal(options) {
13687
13806
  );
13688
13807
  return;
13689
13808
  }
13690
- if (loaderResult.redirect) {
13691
- handleRedirect(res, loaderResult.redirect);
13692
- return;
13693
- }
13694
- if (loaderResult.notFound) {
13695
- if (isDataReq) {
13696
- res.status(200).json({ notFound: true });
13697
- } else {
13698
- handleNotFound(res, urlPath);
13699
- }
13700
- return;
13701
- }
13702
13809
  const initialData = buildInitialData(finalUrlPath, params, combinedLoaderResult);
13703
13810
  const appTree = buildAppTree(route, params, initialData.props);
13704
13811
  const chunkName = routeChunks[route.pattern];
@@ -13755,7 +13862,7 @@ async function handlePageRequestInternal(options) {
13755
13862
  }
13756
13863
  console.error("\u{1F4A1} This usually indicates a React rendering error\n");
13757
13864
  if (!res.headersSent && errorPage) {
13758
- renderErrorPageWithStream(errorPage, req, res, err, routeChunks, theme, projectRoot, env);
13865
+ renderErrorPageWithStream(errorPage, req, res, err, routeChunks, theme, projectRoot, env, config, notFoundPage);
13759
13866
  } else if (!res.headersSent) {
13760
13867
  res.statusCode = 500;
13761
13868
  res.setHeader("Content-Type", "text/html; charset=utf-8");
@@ -13777,7 +13884,7 @@ async function handlePageRequestInternal(options) {
13777
13884
  abort();
13778
13885
  });
13779
13886
  }
13780
- async function renderErrorPageWithStream(errorPage, req, res, error, routeChunks, theme, projectRoot, env = "dev", config) {
13887
+ async function renderErrorPageWithStream(errorPage, req, res, error, routeChunks, theme, projectRoot, env = "dev", config, notFoundPage) {
13781
13888
  try {
13782
13889
  const isDataReq = isDataRequest(req);
13783
13890
  const skipLayoutHooks = isDataReq && req.headers["x-skip-layout-hooks"] === "true";
@@ -13786,7 +13893,9 @@ async function renderErrorPageWithStream(errorPage, req, res, error, routeChunks
13786
13893
  res,
13787
13894
  params: { error: String(error) },
13788
13895
  pathname: req.path,
13789
- locals: { error }
13896
+ locals: { error },
13897
+ Redirect: (destination, permanent = false) => new RedirectResponse(destination, permanent),
13898
+ NotFound: () => new NotFoundResponse()
13790
13899
  };
13791
13900
  const faviconInfo = projectRoot && config ? getFaviconInfo(projectRoot, config.directories.static, env === "dev") : null;
13792
13901
  const layoutProps = {};
@@ -13819,6 +13928,26 @@ async function renderErrorPageWithStream(errorPage, req, res, error, routeChunks
13819
13928
  if (layoutServerHook) {
13820
13929
  try {
13821
13930
  const layoutResult = await layoutServerHook(ctx);
13931
+ if (layoutResult instanceof RedirectResponse) {
13932
+ if (isDataReq) {
13933
+ res.statusCode = 200;
13934
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
13935
+ res.end(JSON.stringify({ redirect: { destination: layoutResult.destination, permanent: layoutResult.permanent } }));
13936
+ } else {
13937
+ handleRedirect(res, { destination: layoutResult.destination, permanent: layoutResult.permanent });
13938
+ }
13939
+ return;
13940
+ }
13941
+ if (layoutResult instanceof NotFoundResponse) {
13942
+ if (isDataReq) {
13943
+ res.statusCode = 200;
13944
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
13945
+ res.end(JSON.stringify({ notFound: true }));
13946
+ } else {
13947
+ handleNotFound(res, req.path);
13948
+ }
13949
+ return;
13950
+ }
13822
13951
  if (layoutResult.props) {
13823
13952
  Object.assign(layoutProps, layoutResult.props);
13824
13953
  }
@@ -13836,22 +13965,69 @@ async function renderErrorPageWithStream(errorPage, req, res, error, routeChunks
13836
13965
  }
13837
13966
  }
13838
13967
  let loaderResult = await runRouteServerHook(errorPage, ctx);
13839
- if (!loaderResult.theme && theme) {
13840
- loaderResult.theme = theme;
13968
+ if (loaderResult instanceof RedirectResponse) {
13969
+ if (isDataReq) {
13970
+ res.statusCode = 200;
13971
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
13972
+ res.end(JSON.stringify({ redirect: { destination: loaderResult.destination, permanent: loaderResult.permanent } }));
13973
+ } else {
13974
+ handleRedirect(res, { destination: loaderResult.destination, permanent: loaderResult.permanent });
13975
+ }
13976
+ return;
13977
+ }
13978
+ if (loaderResult instanceof NotFoundResponse) {
13979
+ if (isDataReq) {
13980
+ res.statusCode = 200;
13981
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
13982
+ res.end(JSON.stringify({ notFound: true }));
13983
+ } else {
13984
+ if (notFoundPage) {
13985
+ const notFoundAssetManifest = env === "prod" && projectRoot ? loadAssetManifest(projectRoot) : null;
13986
+ const notFoundClientJsPath = env === "dev" ? "/static/client.js" : projectRoot ? getClientJsPath(projectRoot) : "/static/client.js";
13987
+ const notFoundClientCssPath = env === "dev" ? "/static/client.css" : projectRoot ? getClientCssPath(projectRoot) : "/static/client.css";
13988
+ const notFoundFaviconInfo = projectRoot && config ? getFaviconInfo(projectRoot, config.directories.static, env === "dev") : null;
13989
+ await renderNotFoundPage(
13990
+ notFoundPage,
13991
+ req,
13992
+ res,
13993
+ req.path,
13994
+ req.path,
13995
+ buildRouterData(req),
13996
+ notFoundAssetManifest,
13997
+ theme,
13998
+ notFoundClientJsPath,
13999
+ notFoundClientCssPath,
14000
+ notFoundFaviconInfo,
14001
+ projectRoot,
14002
+ false,
14003
+ errorPage,
14004
+ routeChunks,
14005
+ env,
14006
+ config
14007
+ );
14008
+ } else {
14009
+ handleNotFound(res, req.path);
14010
+ }
14011
+ }
14012
+ return;
14013
+ }
14014
+ const errorLoaderResult = loaderResult;
14015
+ if (!errorLoaderResult.theme && theme) {
14016
+ errorLoaderResult.theme = theme;
13841
14017
  }
13842
14018
  const combinedProps = {
13843
14019
  ...layoutProps,
13844
- ...loaderResult.props || {}
14020
+ ...errorLoaderResult.props || {}
13845
14021
  };
13846
14022
  const combinedLoaderResult = {
13847
- ...loaderResult,
14023
+ ...errorLoaderResult,
13848
14024
  props: combinedProps
13849
14025
  };
13850
14026
  const initialData = buildInitialData(req.path, { error: String(error) }, combinedLoaderResult);
13851
14027
  const routerData = buildRouterData(req);
13852
14028
  initialData.error = true;
13853
14029
  if (isDataReq) {
13854
- const pagePropsOnly = loaderResult.props || {};
14030
+ const pagePropsOnly = errorLoaderResult.props || {};
13855
14031
  handleDataResponse(
13856
14032
  res,
13857
14033
  combinedLoaderResult,
@@ -13980,7 +14156,9 @@ async function renderStaticRoute(projectRoot, ssgOutDir, route, urlPath, params,
13980
14156
  res,
13981
14157
  params,
13982
14158
  pathname: urlPath,
13983
- locals: {}
14159
+ locals: {},
14160
+ Redirect: (destination, permanent = false) => new RedirectResponse(destination, permanent),
14161
+ NotFound: () => new NotFoundResponse()
13984
14162
  };
13985
14163
  for (const mw of route.middlewares) {
13986
14164
  await Promise.resolve(
@@ -13996,6 +14174,12 @@ async function renderStaticRoute(projectRoot, ssgOutDir, route, urlPath, params,
13996
14174
  if (layoutServerHook) {
13997
14175
  try {
13998
14176
  const layoutResult = await layoutServerHook(ctx);
14177
+ if (layoutResult instanceof RedirectResponse || layoutResult instanceof NotFoundResponse) {
14178
+ console.warn(
14179
+ `\u26A0\uFE0F [framework][ssg] Layout server hook ${i} returned redirect/notFound for route ${route.pattern}. Redirect/NotFound is not supported in SSG (Static Site Generation). Skipping this route.`
14180
+ );
14181
+ return;
14182
+ }
13999
14183
  if (layoutResult.props) {
14000
14184
  Object.assign(layoutProps, layoutResult.props);
14001
14185
  }
@@ -14017,10 +14201,17 @@ async function renderStaticRoute(projectRoot, ssgOutDir, route, urlPath, params,
14017
14201
  let loaderResult = { props: {} };
14018
14202
  if (route.loader) {
14019
14203
  loaderResult = await route.loader(ctx);
14204
+ if (loaderResult instanceof RedirectResponse || loaderResult instanceof NotFoundResponse) {
14205
+ console.warn(
14206
+ `\u26A0\uFE0F [framework][ssg] Page server hook returned redirect/notFound for route ${route.pattern} (${urlPath}). Redirect/NotFound is not supported in SSG (Static Site Generation). Skipping this route.`
14207
+ );
14208
+ return;
14209
+ }
14020
14210
  }
14211
+ const pageLoaderResult = loaderResult;
14021
14212
  const combinedProps = {
14022
14213
  ...layoutProps,
14023
- ...loaderResult.props || {}
14214
+ ...pageLoaderResult.props || {}
14024
14215
  };
14025
14216
  let combinedMetadata = null;
14026
14217
  for (const layoutMeta of layoutMetadata) {
@@ -14028,17 +14219,14 @@ async function renderStaticRoute(projectRoot, ssgOutDir, route, urlPath, params,
14028
14219
  combinedMetadata = mergeMetadata(combinedMetadata, layoutMeta);
14029
14220
  }
14030
14221
  }
14031
- if (loaderResult.metadata) {
14032
- combinedMetadata = mergeMetadata(combinedMetadata, loaderResult.metadata);
14222
+ if (pageLoaderResult.metadata) {
14223
+ combinedMetadata = mergeMetadata(combinedMetadata, pageLoaderResult.metadata);
14033
14224
  }
14034
14225
  const combinedLoaderResult = {
14035
- ...loaderResult,
14226
+ ...pageLoaderResult,
14036
14227
  props: combinedProps,
14037
14228
  metadata: combinedMetadata
14038
14229
  };
14039
- if (combinedLoaderResult.redirect || combinedLoaderResult.notFound) {
14040
- return;
14041
- }
14042
14230
  const initialData = buildInitialData(urlPath, params, combinedLoaderResult);
14043
14231
  const routerData = buildRouterData(req);
14044
14232
  const appTree = buildAppTree(route, params, initialData.props);