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