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

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.js CHANGED
@@ -10782,6 +10782,18 @@ function writeRoutesManifest({
10782
10782
  fs7.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
10783
10783
  }
10784
10784
 
10785
+ // modules/router/index.types.ts
10786
+ var RedirectResponse = class {
10787
+ constructor(destination, permanent = false) {
10788
+ this.destination = destination;
10789
+ this.permanent = permanent;
10790
+ }
10791
+ };
10792
+ var NotFoundResponse = class {
10793
+ constructor() {
10794
+ }
10795
+ };
10796
+
10785
10797
  // modules/router/loader-routes.ts
10786
10798
  import fs8 from "fs";
10787
10799
  import path8 from "path";
@@ -15605,16 +15617,6 @@ async function runRouteServerHook(route, ctx) {
15605
15617
  // modules/server/handlers/response.ts
15606
15618
  function handleDataResponse(res, loaderResult, theme, layoutProps, pageProps, error, message) {
15607
15619
  res.setHeader("Content-Type", "application/json; charset=utf-8");
15608
- if (loaderResult.redirect) {
15609
- res.statusCode = 200;
15610
- res.end(JSON.stringify({ redirect: loaderResult.redirect }));
15611
- return;
15612
- }
15613
- if (loaderResult.notFound) {
15614
- res.statusCode = 404;
15615
- res.end(JSON.stringify({ notFound: true }));
15616
- return;
15617
- }
15618
15620
  const response = {
15619
15621
  // Combined props for backward compatibility
15620
15622
  props: loaderResult.props ?? {},
@@ -15735,6 +15737,145 @@ function mergeMetadata(base, override) {
15735
15737
  function isDataRequest(req) {
15736
15738
  return req.query && req.query.__fw_data === "1" || req.headers["x-fw-data"] === "1";
15737
15739
  }
15740
+ async function renderNotFoundPage(notFoundPage, req, res, urlPath, finalUrlPath, routerData, assetManifest, theme, clientJsPath, clientCssPath, faviconInfo, projectRoot, skipLayoutHooks, errorPage, routeChunks, env, config) {
15741
+ const ctx = {
15742
+ req,
15743
+ res,
15744
+ params: {},
15745
+ pathname: urlPath,
15746
+ locals: {},
15747
+ Redirect: (destination, permanent = false) => new RedirectResponse(destination, permanent),
15748
+ NotFound: () => new NotFoundResponse()
15749
+ };
15750
+ const layoutProps = {};
15751
+ if (!skipLayoutHooks && notFoundPage.layoutServerHooks && notFoundPage.layoutServerHooks.length > 0) {
15752
+ for (let i = 0; i < notFoundPage.layoutServerHooks.length; i++) {
15753
+ const layoutServerHook = notFoundPage.layoutServerHooks[i];
15754
+ const layoutMiddlewares = notFoundPage.layoutMiddlewares?.[i] || [];
15755
+ if (layoutMiddlewares.length > 0) {
15756
+ for (const mw of layoutMiddlewares) {
15757
+ try {
15758
+ await Promise.resolve(
15759
+ mw(ctx, async () => {
15760
+ })
15761
+ );
15762
+ } catch (error) {
15763
+ const reqLogger = getRequestLogger(req);
15764
+ const layoutFile = notFoundPage.layoutFiles[i];
15765
+ const relativeLayoutPath = layoutFile ? path23.relative(projectRoot || process.cwd(), layoutFile) : "unknown";
15766
+ reqLogger.error("Layout middleware failed for not-found page", error instanceof Error ? error : new Error(String(error)), {
15767
+ layoutIndex: i,
15768
+ layoutFile: relativeLayoutPath
15769
+ });
15770
+ throw error;
15771
+ }
15772
+ if (ctx.res.headersSent) {
15773
+ return;
15774
+ }
15775
+ }
15776
+ }
15777
+ if (layoutServerHook) {
15778
+ try {
15779
+ const layoutResult = await layoutServerHook(ctx);
15780
+ if (layoutResult instanceof RedirectResponse) {
15781
+ handleRedirect(res, { destination: layoutResult.destination, permanent: layoutResult.permanent });
15782
+ return;
15783
+ }
15784
+ if (layoutResult instanceof NotFoundResponse) {
15785
+ handleNotFound(res, urlPath);
15786
+ return;
15787
+ }
15788
+ if (layoutResult.props) {
15789
+ Object.assign(layoutProps, layoutResult.props);
15790
+ }
15791
+ } catch (error) {
15792
+ const reqLogger = getRequestLogger(req);
15793
+ const layoutFile = notFoundPage.layoutFiles[i];
15794
+ const relativeLayoutPath = layoutFile ? path23.relative(projectRoot || process.cwd(), layoutFile) : "unknown";
15795
+ reqLogger.warn("Layout server hook failed for not-found page", {
15796
+ error: error instanceof Error ? error.message : String(error),
15797
+ stack: error instanceof Error ? error.stack : void 0,
15798
+ layoutFile: relativeLayoutPath,
15799
+ layoutIndex: i
15800
+ });
15801
+ }
15802
+ }
15803
+ }
15804
+ }
15805
+ let loaderResult = await runRouteServerHook(notFoundPage, ctx);
15806
+ if (loaderResult instanceof RedirectResponse) {
15807
+ handleRedirect(res, { destination: loaderResult.destination, permanent: loaderResult.permanent });
15808
+ return;
15809
+ }
15810
+ if (loaderResult instanceof NotFoundResponse) {
15811
+ handleNotFound(res, urlPath);
15812
+ return;
15813
+ }
15814
+ const notFoundLoaderResult = loaderResult;
15815
+ if (!notFoundLoaderResult.theme) {
15816
+ notFoundLoaderResult.theme = theme;
15817
+ }
15818
+ const combinedProps = {
15819
+ ...layoutProps,
15820
+ ...notFoundLoaderResult.props || {}
15821
+ };
15822
+ const combinedLoaderResult = {
15823
+ ...notFoundLoaderResult,
15824
+ props: combinedProps
15825
+ };
15826
+ const initialData = buildInitialData(finalUrlPath, {}, combinedLoaderResult);
15827
+ const appTree = buildAppTree(notFoundPage, {}, initialData.props);
15828
+ initialData.notFound = true;
15829
+ const nonce = res.locals.nonce || void 0;
15830
+ const entrypointFiles = [];
15831
+ if (assetManifest?.entrypoints?.client) {
15832
+ entrypointFiles.push(...assetManifest.entrypoints.client.map((file) => `${STATIC_PATH}/${file}`));
15833
+ }
15834
+ const documentTree = createDocumentTree({
15835
+ appTree,
15836
+ initialData,
15837
+ routerData,
15838
+ meta: combinedLoaderResult.metadata ?? null,
15839
+ titleFallback: "Not found",
15840
+ descriptionFallback: "Loly demo",
15841
+ chunkHref: null,
15842
+ entrypointFiles,
15843
+ theme,
15844
+ clientJsPath,
15845
+ clientCssPath,
15846
+ nonce,
15847
+ faviconPath: faviconInfo?.path || null,
15848
+ faviconType: faviconInfo?.type || null
15849
+ });
15850
+ let didError = false;
15851
+ const { pipe, abort } = renderToPipeableStream(documentTree, {
15852
+ onShellReady() {
15853
+ if (didError || res.headersSent) return;
15854
+ res.statusCode = 404;
15855
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
15856
+ pipe(res);
15857
+ },
15858
+ onShellError(err) {
15859
+ didError = true;
15860
+ const reqLogger = getRequestLogger(req);
15861
+ reqLogger.error("SSR shell error", err, { route: "not-found" });
15862
+ if (!res.headersSent && errorPage) {
15863
+ renderErrorPageWithStream(errorPage, req, res, err, routeChunks || {}, theme, projectRoot, env, config, notFoundPage);
15864
+ } else if (!res.headersSent) {
15865
+ res.statusCode = 500;
15866
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
15867
+ res.end("<!doctype html><h1>Internal Server Error</h1>");
15868
+ }
15869
+ abort();
15870
+ },
15871
+ onError(err) {
15872
+ didError = true;
15873
+ const reqLogger = getRequestLogger(req);
15874
+ reqLogger.error("SSR error", err, { route: "not-found" });
15875
+ }
15876
+ });
15877
+ req.on("close", () => abort());
15878
+ }
15738
15879
  async function handlePageRequest(options) {
15739
15880
  try {
15740
15881
  await handlePageRequestInternal(options);
@@ -15742,7 +15883,7 @@ async function handlePageRequest(options) {
15742
15883
  const { errorPage, req, res, routeChunks, theme, projectRoot } = options;
15743
15884
  const reqLogger = getRequestLogger(req);
15744
15885
  if (errorPage) {
15745
- await renderErrorPageWithStream(errorPage, req, res, error, routeChunks || {}, theme, projectRoot, options.env, options.config);
15886
+ await renderErrorPageWithStream(errorPage, req, res, error, routeChunks || {}, theme, projectRoot, options.env, options.config, options.notFoundPage);
15746
15887
  } else {
15747
15888
  reqLogger.error("Unhandled error in page request", error, {
15748
15889
  urlPath: options.urlPath,
@@ -15852,135 +15993,25 @@ async function handlePageRequestInternal(options) {
15852
15993
  return;
15853
15994
  }
15854
15995
  if (notFoundPage) {
15855
- const ctx2 = {
15996
+ await renderNotFoundPage(
15997
+ notFoundPage,
15856
15998
  req,
15857
15999
  res,
15858
- params: {},
15859
- pathname: urlPath,
15860
- locals: {}
15861
- };
15862
- const layoutProps2 = {};
15863
- if (!skipLayoutHooks && notFoundPage.layoutServerHooks && notFoundPage.layoutServerHooks.length > 0) {
15864
- for (let i = 0; i < notFoundPage.layoutServerHooks.length; i++) {
15865
- const layoutServerHook = notFoundPage.layoutServerHooks[i];
15866
- const layoutMiddlewares = notFoundPage.layoutMiddlewares?.[i] || [];
15867
- if (layoutMiddlewares.length > 0) {
15868
- for (const mw of layoutMiddlewares) {
15869
- try {
15870
- await Promise.resolve(
15871
- mw(ctx2, async () => {
15872
- })
15873
- );
15874
- } catch (error) {
15875
- const reqLogger2 = getRequestLogger(req);
15876
- const layoutFile = notFoundPage.layoutFiles[i];
15877
- const relativeLayoutPath = layoutFile ? path23.relative(projectRoot || process.cwd(), layoutFile) : "unknown";
15878
- reqLogger2.error("Layout middleware failed for not-found page", error instanceof Error ? error : new Error(String(error)), {
15879
- layoutIndex: i,
15880
- layoutFile: relativeLayoutPath
15881
- });
15882
- throw error;
15883
- }
15884
- if (ctx2.res.headersSent) {
15885
- return;
15886
- }
15887
- }
15888
- }
15889
- if (layoutServerHook) {
15890
- try {
15891
- const layoutResult = await layoutServerHook(ctx2);
15892
- if (layoutResult.props) {
15893
- Object.assign(layoutProps2, layoutResult.props);
15894
- }
15895
- } catch (error) {
15896
- const reqLogger2 = getRequestLogger(req);
15897
- const layoutFile = notFoundPage.layoutFiles[i];
15898
- const relativeLayoutPath = layoutFile ? path23.relative(projectRoot || process.cwd(), layoutFile) : "unknown";
15899
- reqLogger2.warn("Layout server hook failed for not-found page", {
15900
- error: error instanceof Error ? error.message : String(error),
15901
- stack: error instanceof Error ? error.stack : void 0,
15902
- layoutFile: relativeLayoutPath,
15903
- layoutIndex: i
15904
- });
15905
- }
15906
- }
15907
- }
15908
- }
15909
- let loaderResult2 = await runRouteServerHook(notFoundPage, ctx2);
15910
- if (!loaderResult2.theme) {
15911
- loaderResult2.theme = theme;
15912
- }
15913
- const combinedProps2 = {
15914
- ...layoutProps2,
15915
- ...loaderResult2.props || {}
15916
- };
15917
- const combinedLoaderResult2 = {
15918
- ...loaderResult2,
15919
- props: combinedProps2
15920
- };
15921
- if (isDataReq) {
15922
- const pagePropsOnly = loaderResult2.props || {};
15923
- handleDataResponse(
15924
- res,
15925
- combinedLoaderResult2,
15926
- theme,
15927
- skipLayoutHooks ? null : Object.keys(layoutProps2).length > 0 ? layoutProps2 : null,
15928
- pagePropsOnly
15929
- );
15930
- return;
15931
- }
15932
- const initialData2 = buildInitialData(finalUrlPath, {}, combinedLoaderResult2);
15933
- const appTree2 = buildAppTree(notFoundPage, {}, initialData2.props);
15934
- initialData2.notFound = true;
15935
- const nonce2 = res.locals.nonce || void 0;
15936
- const entrypointFiles2 = [];
15937
- if (assetManifest?.entrypoints?.client) {
15938
- entrypointFiles2.push(...assetManifest.entrypoints.client.map((file) => `${STATIC_PATH}/${file}`));
15939
- }
15940
- const documentTree2 = createDocumentTree({
15941
- appTree: appTree2,
15942
- initialData: initialData2,
16000
+ urlPath,
16001
+ finalUrlPath,
15943
16002
  routerData,
15944
- meta: combinedLoaderResult2.metadata ?? null,
15945
- titleFallback: "Not found",
15946
- descriptionFallback: "Loly demo",
15947
- chunkHref: null,
15948
- entrypointFiles: entrypointFiles2,
16003
+ assetManifest,
15949
16004
  theme,
15950
16005
  clientJsPath,
15951
16006
  clientCssPath,
15952
- nonce: nonce2,
15953
- faviconPath: faviconInfo?.path || null,
15954
- faviconType: faviconInfo?.type || null
15955
- });
15956
- let didError2 = false;
15957
- const { pipe: pipe2, abort: abort2 } = renderToPipeableStream(documentTree2, {
15958
- onShellReady() {
15959
- if (didError2 || res.headersSent) return;
15960
- res.statusCode = 404;
15961
- res.setHeader("Content-Type", "text/html; charset=utf-8");
15962
- pipe2(res);
15963
- },
15964
- onShellError(err) {
15965
- didError2 = true;
15966
- const reqLogger2 = getRequestLogger(req);
15967
- reqLogger2.error("SSR shell error", err, { route: "not-found" });
15968
- if (!res.headersSent && errorPage) {
15969
- renderErrorPageWithStream(errorPage, req, res, err, routeChunks, theme, projectRoot, env, config);
15970
- } else if (!res.headersSent) {
15971
- res.statusCode = 500;
15972
- res.setHeader("Content-Type", "text/html; charset=utf-8");
15973
- res.end("<!doctype html><h1>Internal Server Error</h1>");
15974
- }
15975
- abort2();
15976
- },
15977
- onError(err) {
15978
- didError2 = true;
15979
- const reqLogger2 = getRequestLogger(req);
15980
- reqLogger2.error("SSR error", err, { route: "not-found" });
15981
- }
15982
- });
15983
- req.on("close", () => abort2());
16007
+ faviconInfo,
16008
+ projectRoot,
16009
+ skipLayoutHooks,
16010
+ errorPage,
16011
+ routeChunks,
16012
+ env,
16013
+ config
16014
+ );
15984
16015
  return;
15985
16016
  }
15986
16017
  handleNotFound(res, urlPath);
@@ -15993,7 +16024,9 @@ async function handlePageRequestInternal(options) {
15993
16024
  res,
15994
16025
  params: sanitizedParams,
15995
16026
  pathname: urlPath,
15996
- locals: {}
16027
+ locals: {},
16028
+ Redirect: (destination, permanent = false) => new RedirectResponse(destination, permanent),
16029
+ NotFound: () => new NotFoundResponse()
15997
16030
  };
15998
16031
  await runRouteMiddlewares(route, ctx);
15999
16032
  if (res.headersSent) {
@@ -16031,6 +16064,48 @@ async function handlePageRequestInternal(options) {
16031
16064
  if (layoutServerHook) {
16032
16065
  try {
16033
16066
  const layoutResult = await layoutServerHook(ctx);
16067
+ if (layoutResult instanceof RedirectResponse) {
16068
+ if (isDataReq) {
16069
+ res.statusCode = 200;
16070
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
16071
+ res.end(JSON.stringify({ redirect: { destination: layoutResult.destination, permanent: layoutResult.permanent } }));
16072
+ } else {
16073
+ handleRedirect(res, { destination: layoutResult.destination, permanent: layoutResult.permanent });
16074
+ }
16075
+ return;
16076
+ }
16077
+ if (layoutResult instanceof NotFoundResponse) {
16078
+ if (isDataReq) {
16079
+ res.statusCode = 200;
16080
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
16081
+ res.end(JSON.stringify({ notFound: true }));
16082
+ } else {
16083
+ if (notFoundPage) {
16084
+ await renderNotFoundPage(
16085
+ notFoundPage,
16086
+ req,
16087
+ res,
16088
+ urlPath,
16089
+ finalUrlPath,
16090
+ routerData,
16091
+ assetManifest,
16092
+ theme,
16093
+ clientJsPath,
16094
+ clientCssPath,
16095
+ faviconInfo,
16096
+ projectRoot,
16097
+ skipLayoutHooks,
16098
+ errorPage,
16099
+ routeChunks,
16100
+ env,
16101
+ config
16102
+ );
16103
+ } else {
16104
+ handleNotFound(res, urlPath);
16105
+ }
16106
+ }
16107
+ return;
16108
+ }
16034
16109
  if (layoutResult.props) {
16035
16110
  Object.assign(layoutProps, layoutResult.props);
16036
16111
  }
@@ -16055,8 +16130,51 @@ async function handlePageRequestInternal(options) {
16055
16130
  let loaderResult;
16056
16131
  try {
16057
16132
  loaderResult = await runRouteServerHook(route, ctx);
16058
- if (!loaderResult.theme) {
16059
- loaderResult.theme = theme;
16133
+ if (loaderResult instanceof RedirectResponse) {
16134
+ if (isDataReq) {
16135
+ res.statusCode = 200;
16136
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
16137
+ res.end(JSON.stringify({ redirect: { destination: loaderResult.destination, permanent: loaderResult.permanent } }));
16138
+ } else {
16139
+ handleRedirect(res, { destination: loaderResult.destination, permanent: loaderResult.permanent });
16140
+ }
16141
+ return;
16142
+ }
16143
+ if (loaderResult instanceof NotFoundResponse) {
16144
+ if (isDataReq) {
16145
+ res.statusCode = 200;
16146
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
16147
+ res.end(JSON.stringify({ notFound: true }));
16148
+ } else {
16149
+ if (notFoundPage) {
16150
+ await renderNotFoundPage(
16151
+ notFoundPage,
16152
+ req,
16153
+ res,
16154
+ urlPath,
16155
+ finalUrlPath,
16156
+ routerData,
16157
+ assetManifest,
16158
+ theme,
16159
+ clientJsPath,
16160
+ clientCssPath,
16161
+ faviconInfo,
16162
+ projectRoot,
16163
+ skipLayoutHooks,
16164
+ errorPage,
16165
+ routeChunks,
16166
+ env,
16167
+ config
16168
+ );
16169
+ } else {
16170
+ handleNotFound(res, urlPath);
16171
+ }
16172
+ }
16173
+ return;
16174
+ }
16175
+ const pageLoaderResult2 = loaderResult;
16176
+ if (!pageLoaderResult2.theme) {
16177
+ pageLoaderResult2.theme = theme;
16060
16178
  }
16061
16179
  } catch (error) {
16062
16180
  const relativePagePath = route.pageFile ? path23.relative(projectRoot || process.cwd(), route.pageFile) : "unknown";
@@ -16081,17 +16199,18 @@ async function handlePageRequestInternal(options) {
16081
16199
  return;
16082
16200
  } else {
16083
16201
  if (errorPage) {
16084
- await renderErrorPageWithStream(errorPage, req, res, error, routeChunks, theme, projectRoot, env, config);
16202
+ await renderErrorPageWithStream(errorPage, req, res, error, routeChunks, theme, projectRoot, env, config, notFoundPage);
16085
16203
  return;
16086
16204
  } else {
16087
16205
  throw error;
16088
16206
  }
16089
16207
  }
16090
16208
  }
16209
+ const pageLoaderResult = loaderResult;
16091
16210
  const combinedProps = {
16092
16211
  ...layoutProps,
16093
16212
  // Props from layouts (stable)
16094
- ...loaderResult.props || {}
16213
+ ...pageLoaderResult.props || {}
16095
16214
  // Props from page (overrides layout)
16096
16215
  };
16097
16216
  let combinedMetadata = null;
@@ -16100,18 +16219,18 @@ async function handlePageRequestInternal(options) {
16100
16219
  combinedMetadata = mergeMetadata(combinedMetadata, layoutMeta);
16101
16220
  }
16102
16221
  }
16103
- if (loaderResult.metadata) {
16104
- combinedMetadata = mergeMetadata(combinedMetadata, loaderResult.metadata);
16222
+ if (pageLoaderResult.metadata) {
16223
+ combinedMetadata = mergeMetadata(combinedMetadata, pageLoaderResult.metadata);
16105
16224
  }
16106
16225
  const combinedLoaderResult = {
16107
- ...loaderResult,
16226
+ ...pageLoaderResult,
16108
16227
  props: combinedProps,
16109
16228
  metadata: combinedMetadata,
16110
16229
  pathname: finalUrlPath
16111
16230
  // Include rewritten pathname for client-side matching
16112
16231
  };
16113
16232
  if (isDataReq) {
16114
- const pagePropsOnly = loaderResult.props || {};
16233
+ const pagePropsOnly = pageLoaderResult.props || {};
16115
16234
  handleDataResponse(
16116
16235
  res,
16117
16236
  combinedLoaderResult,
@@ -16121,18 +16240,6 @@ async function handlePageRequestInternal(options) {
16121
16240
  );
16122
16241
  return;
16123
16242
  }
16124
- if (loaderResult.redirect) {
16125
- handleRedirect(res, loaderResult.redirect);
16126
- return;
16127
- }
16128
- if (loaderResult.notFound) {
16129
- if (isDataReq) {
16130
- res.status(200).json({ notFound: true });
16131
- } else {
16132
- handleNotFound(res, urlPath);
16133
- }
16134
- return;
16135
- }
16136
16243
  const initialData = buildInitialData(finalUrlPath, params, combinedLoaderResult);
16137
16244
  const appTree = buildAppTree(route, params, initialData.props);
16138
16245
  const chunkName = routeChunks[route.pattern];
@@ -16189,7 +16296,7 @@ async function handlePageRequestInternal(options) {
16189
16296
  }
16190
16297
  console.error("\u{1F4A1} This usually indicates a React rendering error\n");
16191
16298
  if (!res.headersSent && errorPage) {
16192
- renderErrorPageWithStream(errorPage, req, res, err, routeChunks, theme, projectRoot, env);
16299
+ renderErrorPageWithStream(errorPage, req, res, err, routeChunks, theme, projectRoot, env, config, notFoundPage);
16193
16300
  } else if (!res.headersSent) {
16194
16301
  res.statusCode = 500;
16195
16302
  res.setHeader("Content-Type", "text/html; charset=utf-8");
@@ -16211,7 +16318,7 @@ async function handlePageRequestInternal(options) {
16211
16318
  abort();
16212
16319
  });
16213
16320
  }
16214
- async function renderErrorPageWithStream(errorPage, req, res, error, routeChunks, theme, projectRoot, env = "dev", config) {
16321
+ async function renderErrorPageWithStream(errorPage, req, res, error, routeChunks, theme, projectRoot, env = "dev", config, notFoundPage) {
16215
16322
  try {
16216
16323
  const isDataReq = isDataRequest(req);
16217
16324
  const skipLayoutHooks = isDataReq && req.headers["x-skip-layout-hooks"] === "true";
@@ -16220,7 +16327,9 @@ async function renderErrorPageWithStream(errorPage, req, res, error, routeChunks
16220
16327
  res,
16221
16328
  params: { error: String(error) },
16222
16329
  pathname: req.path,
16223
- locals: { error }
16330
+ locals: { error },
16331
+ Redirect: (destination, permanent = false) => new RedirectResponse(destination, permanent),
16332
+ NotFound: () => new NotFoundResponse()
16224
16333
  };
16225
16334
  const faviconInfo = projectRoot && config ? getFaviconInfo(projectRoot, config.directories.static, env === "dev") : null;
16226
16335
  const layoutProps = {};
@@ -16253,6 +16362,26 @@ async function renderErrorPageWithStream(errorPage, req, res, error, routeChunks
16253
16362
  if (layoutServerHook) {
16254
16363
  try {
16255
16364
  const layoutResult = await layoutServerHook(ctx);
16365
+ if (layoutResult instanceof RedirectResponse) {
16366
+ if (isDataReq) {
16367
+ res.statusCode = 200;
16368
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
16369
+ res.end(JSON.stringify({ redirect: { destination: layoutResult.destination, permanent: layoutResult.permanent } }));
16370
+ } else {
16371
+ handleRedirect(res, { destination: layoutResult.destination, permanent: layoutResult.permanent });
16372
+ }
16373
+ return;
16374
+ }
16375
+ if (layoutResult instanceof NotFoundResponse) {
16376
+ if (isDataReq) {
16377
+ res.statusCode = 200;
16378
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
16379
+ res.end(JSON.stringify({ notFound: true }));
16380
+ } else {
16381
+ handleNotFound(res, req.path);
16382
+ }
16383
+ return;
16384
+ }
16256
16385
  if (layoutResult.props) {
16257
16386
  Object.assign(layoutProps, layoutResult.props);
16258
16387
  }
@@ -16270,22 +16399,69 @@ async function renderErrorPageWithStream(errorPage, req, res, error, routeChunks
16270
16399
  }
16271
16400
  }
16272
16401
  let loaderResult = await runRouteServerHook(errorPage, ctx);
16273
- if (!loaderResult.theme && theme) {
16274
- loaderResult.theme = theme;
16402
+ if (loaderResult instanceof RedirectResponse) {
16403
+ if (isDataReq) {
16404
+ res.statusCode = 200;
16405
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
16406
+ res.end(JSON.stringify({ redirect: { destination: loaderResult.destination, permanent: loaderResult.permanent } }));
16407
+ } else {
16408
+ handleRedirect(res, { destination: loaderResult.destination, permanent: loaderResult.permanent });
16409
+ }
16410
+ return;
16411
+ }
16412
+ if (loaderResult instanceof NotFoundResponse) {
16413
+ if (isDataReq) {
16414
+ res.statusCode = 200;
16415
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
16416
+ res.end(JSON.stringify({ notFound: true }));
16417
+ } else {
16418
+ if (notFoundPage) {
16419
+ const notFoundAssetManifest = env === "prod" && projectRoot ? loadAssetManifest(projectRoot) : null;
16420
+ const notFoundClientJsPath = env === "dev" ? "/static/client.js" : projectRoot ? getClientJsPath(projectRoot) : "/static/client.js";
16421
+ const notFoundClientCssPath = env === "dev" ? "/static/client.css" : projectRoot ? getClientCssPath(projectRoot) : "/static/client.css";
16422
+ const notFoundFaviconInfo = projectRoot && config ? getFaviconInfo(projectRoot, config.directories.static, env === "dev") : null;
16423
+ await renderNotFoundPage(
16424
+ notFoundPage,
16425
+ req,
16426
+ res,
16427
+ req.path,
16428
+ req.path,
16429
+ buildRouterData(req),
16430
+ notFoundAssetManifest,
16431
+ theme,
16432
+ notFoundClientJsPath,
16433
+ notFoundClientCssPath,
16434
+ notFoundFaviconInfo,
16435
+ projectRoot,
16436
+ false,
16437
+ errorPage,
16438
+ routeChunks,
16439
+ env,
16440
+ config
16441
+ );
16442
+ } else {
16443
+ handleNotFound(res, req.path);
16444
+ }
16445
+ }
16446
+ return;
16447
+ }
16448
+ const errorLoaderResult = loaderResult;
16449
+ if (!errorLoaderResult.theme && theme) {
16450
+ errorLoaderResult.theme = theme;
16275
16451
  }
16276
16452
  const combinedProps = {
16277
16453
  ...layoutProps,
16278
- ...loaderResult.props || {}
16454
+ ...errorLoaderResult.props || {}
16279
16455
  };
16280
16456
  const combinedLoaderResult = {
16281
- ...loaderResult,
16457
+ ...errorLoaderResult,
16282
16458
  props: combinedProps
16283
16459
  };
16284
16460
  const initialData = buildInitialData(req.path, { error: String(error) }, combinedLoaderResult);
16285
16461
  const routerData = buildRouterData(req);
16286
16462
  initialData.error = true;
16287
16463
  if (isDataReq) {
16288
- const pagePropsOnly = loaderResult.props || {};
16464
+ const pagePropsOnly = errorLoaderResult.props || {};
16289
16465
  handleDataResponse(
16290
16466
  res,
16291
16467
  combinedLoaderResult,
@@ -17966,7 +18142,9 @@ async function renderStaticRoute(projectRoot, ssgOutDir, route, urlPath, params,
17966
18142
  res,
17967
18143
  params,
17968
18144
  pathname: urlPath,
17969
- locals: {}
18145
+ locals: {},
18146
+ Redirect: (destination, permanent = false) => new RedirectResponse(destination, permanent),
18147
+ NotFound: () => new NotFoundResponse()
17970
18148
  };
17971
18149
  for (const mw of route.middlewares) {
17972
18150
  await Promise.resolve(
@@ -17982,6 +18160,12 @@ async function renderStaticRoute(projectRoot, ssgOutDir, route, urlPath, params,
17982
18160
  if (layoutServerHook) {
17983
18161
  try {
17984
18162
  const layoutResult = await layoutServerHook(ctx);
18163
+ if (layoutResult instanceof RedirectResponse || layoutResult instanceof NotFoundResponse) {
18164
+ console.warn(
18165
+ `\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.`
18166
+ );
18167
+ return;
18168
+ }
17985
18169
  if (layoutResult.props) {
17986
18170
  Object.assign(layoutProps, layoutResult.props);
17987
18171
  }
@@ -18003,10 +18187,17 @@ async function renderStaticRoute(projectRoot, ssgOutDir, route, urlPath, params,
18003
18187
  let loaderResult = { props: {} };
18004
18188
  if (route.loader) {
18005
18189
  loaderResult = await route.loader(ctx);
18190
+ if (loaderResult instanceof RedirectResponse || loaderResult instanceof NotFoundResponse) {
18191
+ console.warn(
18192
+ `\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.`
18193
+ );
18194
+ return;
18195
+ }
18006
18196
  }
18197
+ const pageLoaderResult = loaderResult;
18007
18198
  const combinedProps = {
18008
18199
  ...layoutProps,
18009
- ...loaderResult.props || {}
18200
+ ...pageLoaderResult.props || {}
18010
18201
  };
18011
18202
  let combinedMetadata = null;
18012
18203
  for (const layoutMeta of layoutMetadata) {
@@ -18014,17 +18205,14 @@ async function renderStaticRoute(projectRoot, ssgOutDir, route, urlPath, params,
18014
18205
  combinedMetadata = mergeMetadata(combinedMetadata, layoutMeta);
18015
18206
  }
18016
18207
  }
18017
- if (loaderResult.metadata) {
18018
- combinedMetadata = mergeMetadata(combinedMetadata, loaderResult.metadata);
18208
+ if (pageLoaderResult.metadata) {
18209
+ combinedMetadata = mergeMetadata(combinedMetadata, pageLoaderResult.metadata);
18019
18210
  }
18020
18211
  const combinedLoaderResult = {
18021
- ...loaderResult,
18212
+ ...pageLoaderResult,
18022
18213
  props: combinedProps,
18023
18214
  metadata: combinedMetadata
18024
18215
  };
18025
- if (combinedLoaderResult.redirect || combinedLoaderResult.notFound) {
18026
- return;
18027
- }
18028
18216
  const initialData = buildInitialData(urlPath, params, combinedLoaderResult);
18029
18217
  const routerData = buildRouterData(req);
18030
18218
  const appTree = buildAppTree(route, params, initialData.props);
@@ -19625,6 +19813,8 @@ var commonSchemas = {
19625
19813
  export {
19626
19814
  DEFAULT_CONFIG,
19627
19815
  Logger,
19816
+ NotFoundResponse,
19817
+ RedirectResponse,
19628
19818
  ValidationError,
19629
19819
  bootstrapClient,
19630
19820
  buildApp,