Resolve page errors gracefully

This commit is contained in:
Benjamin Toby 2026-03-17 17:00:08 +01:00
parent c6166f5fd2
commit 12de7ff891
8 changed files with 177 additions and 123 deletions

View File

@ -38,6 +38,8 @@ export default function watcher() {
console.log(`Page ${action}: ${filename}. Rebuilding ...`);
global.ROUTER.reload();
await global.BUNDLER_CTX?.dispose();
global.BUNDLER_CTX = undefined;

View File

@ -1,15 +1,16 @@
import type { FC } from "react";
import grabDirNames from "../../../utils/grab-dir-names";
import grabRouteParams from "../../../utils/grab-route-params";
import grabRouter from "../../../utils/grab-router";
import type {
BundlerCTXMap,
BunextPageModule,
BunextPageModuleServerReturn,
BunxRouteParams,
GrabPageComponentRes,
} from "../../../types";
import path from "path";
import AppNames from "../../../utils/grab-app-names";
import { existsSync } from "fs";
import grabPageErrorComponent from "./grab-page-error-component";
class NotFoundError extends Error {}
@ -23,20 +24,22 @@ export default async function grabPageComponent({
file_path: passed_file_path,
}: Params): Promise<GrabPageComponentRes> {
const url = req?.url ? new URL(req.url) : undefined;
const router = grabRouter();
const router = global.ROUTER;
const {
BUNX_ROOT_500_PRESET_COMPONENT,
BUNX_ROOT_404_PRESET_COMPONENT,
PAGES_DIR,
} = grabDirNames();
const { PAGES_DIR } = grabDirNames();
const routeParams = req ? await grabRouteParams({ req }) : undefined;
let routeParams: BunxRouteParams | undefined = undefined;
try {
const match = url ? router.match(url.pathname) : undefined;
routeParams = req ? await grabRouteParams({ req }) : undefined;
console.log("match", match);
let url_path = url ? url.pathname : undefined;
if (url_path && url?.search) {
url_path += url.search;
}
const match = url_path ? router.match(url_path) : undefined;
if (!match?.filePath && url?.pathname) {
throw new NotFoundError(`Page ${url.pathname} not found`);
@ -91,14 +94,22 @@ export default async function grabPageComponent({
const module: BunextPageModule = await import(`${file_path}?t=${now}`);
const serverRes = await (async () => {
const serverRes: BunextPageModuleServerReturn = await (async () => {
try {
if (routeParams) {
return await module["server"]?.(routeParams);
const serverData = await module["server"]?.(routeParams);
return {
...serverData,
query: match?.query,
};
}
return {};
return {
query: match?.query,
};
} catch (error) {
return {};
return {
query: match?.query,
};
}
})();
@ -132,55 +143,10 @@ export default async function grabPageComponent({
meta,
};
} catch (error: any) {
const is404 = error instanceof NotFoundError;
const errorRoute = is404 ? "/404" : "/500";
const presetComponent = is404
? BUNX_ROOT_404_PRESET_COMPONENT
: BUNX_ROOT_500_PRESET_COMPONENT;
try {
const match = router.match(errorRoute);
const filePath = match?.filePath || presetComponent;
const bundledMap = match?.filePath
? (global.BUNDLER_CTX_MAP?.find(
(m) => m.local_path === match.filePath,
) ?? ({} as BundlerCTXMap))
: ({} as BundlerCTXMap);
const module: BunextPageModule = await import(filePath);
const Component = module.default as FC<any>;
const component = <Component />;
return {
component,
routeParams,
module,
bundledMap,
};
} catch {
const DefaultNotFound: FC = () => (
<div
style={{
width: "100vw",
height: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<span>
{is404 ? "404 Not Found" : "500 Internal Server Error"}
</span>
</div>
);
return {
component: <DefaultNotFound />,
routeParams,
module: { default: DefaultNotFound },
bundledMap: {} as BundlerCTXMap,
};
}
return await grabPageErrorComponent({
error,
routeParams,
is404: error instanceof NotFoundError,
});
}
}

View File

@ -0,0 +1,75 @@
import type { FC } from "react";
import grabDirNames from "../../../utils/grab-dir-names";
import type {
BundlerCTXMap,
BunextPageModule,
BunxRouteParams,
GrabPageComponentRes,
} from "../../../types";
type Params = {
error?: any;
routeParams?: BunxRouteParams;
is404?: boolean;
};
export default async function grabPageErrorComponent({
error,
routeParams,
is404,
}: Params): Promise<GrabPageComponentRes> {
const router = global.ROUTER;
const { BUNX_ROOT_500_PRESET_COMPONENT, BUNX_ROOT_404_PRESET_COMPONENT } =
grabDirNames();
const errorRoute = is404 ? "/404" : "/500";
const presetComponent = is404
? BUNX_ROOT_404_PRESET_COMPONENT
: BUNX_ROOT_500_PRESET_COMPONENT;
try {
const match = router.match(errorRoute);
const filePath = match?.filePath || presetComponent;
const bundledMap = match?.filePath
? (global.BUNDLER_CTX_MAP?.find(
(m) => m.local_path === match.filePath,
) ?? ({} as BundlerCTXMap))
: ({} as BundlerCTXMap);
const module: BunextPageModule = await import(filePath);
const Component = module.default as FC<any>;
const component = <Component>{<span>{error.message}</span>}</Component>;
return {
component,
routeParams,
module,
bundledMap,
};
} catch {
const DefaultNotFound: FC = () => (
<div
style={{
width: "100vw",
height: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexDirection: "column",
}}
>
<h1>{is404 ? "404 Not Found" : "500 Internal Server Error"}</h1>
<span>{error.message}</span>
</div>
);
return {
component: <DefaultNotFound />,
routeParams,
module: { default: DefaultNotFound },
bundledMap: {} as BundlerCTXMap,
};
}
}

View File

@ -1,55 +1,59 @@
import type { GrabPageComponentRes } from "../../../types";
import isDevelopment from "../../../utils/is-development";
import genWebHTML from "./generate-web-html";
import grabPageComponent from "./grab-page-component";
import grabPageErrorComponent from "./grab-page-error-component";
type Params = {
req: Request;
};
export default async function ({ req }: Params): Promise<Response> {
export default async function handleWebPages({
req,
}: Params): Promise<Response> {
try {
const {
component,
bundledMap,
module,
serverRes,
meta,
head,
routeParams,
} = await grabPageComponent({ req });
const html = await genWebHTML({
component,
pageProps: serverRes,
bundledMap,
module,
meta,
head,
});
const res_opts: ResponseInit = {
headers: {
"Content-Type": "text/html",
},
};
if (isDevelopment()) {
res_opts.headers = {
...res_opts.headers,
"Cache-Control": "no-cache, no-store, must-revalidate",
Pragma: "no-cache",
Expires: "0",
};
}
const res = new Response(html, res_opts);
return res;
const componentRes = await grabPageComponent({ req });
return await generateRes(componentRes);
} catch (error: any) {
console.log(`Handle web pages Error =>`, error.message);
return new Response(error.message || `Page Not Found`, {
status: 404,
});
const componentRes = await grabPageErrorComponent({ error });
return await generateRes(componentRes);
}
}
async function generateRes({
component,
module,
bundledMap,
head,
meta,
routeParams,
serverRes,
}: GrabPageComponentRes) {
const html = await genWebHTML({
component,
pageProps: serverRes,
bundledMap,
module,
meta,
head,
});
const res_opts: ResponseInit = {
headers: {
"Content-Type": "text/html",
},
};
if (isDevelopment()) {
res_opts.headers = {
...res_opts.headers,
"Cache-Control": "no-cache, no-store, must-revalidate",
Pragma: "no-cache",
Expires: "0",
};
}
const res = new Response(html, res_opts);
return res;
}

View File

@ -1,4 +1,6 @@
export default function DefaultNotFoundPage() {
import type { PropsWithChildren } from "react";
export default function DefaultNotFoundPage({ children }: PropsWithChildren) {
return (
<div
style={{
@ -8,9 +10,12 @@ export default function DefaultNotFoundPage() {
display: "flex",
alignItems: "center",
justifyContent: "center",
flexDirection: "column",
gap: "20px",
}}
>
<span>404 Not Found</span>
<h1>404 Not Found</h1>
<span>{children}</span>
</div>
);
}

View File

@ -1,4 +1,8 @@
export default function DefaultServerErrorPage() {
import type { PropsWithChildren } from "react";
export default function DefaultServerErrorPage({
children,
}: PropsWithChildren) {
return (
<div
style={{
@ -8,9 +12,12 @@ export default function DefaultServerErrorPage() {
display: "flex",
alignItems: "center",
justifyContent: "center",
flexDirection: "column",
gap: "20px",
}}
>
<span>500 Internal Server Error</span>
<h1>500 Internal Server Error</h1>
<span>{children}</span>
</div>
);
}

View File

@ -176,8 +176,10 @@ export type BunextPageServerFn<
export type BunextPageModuleServerReturn<
T extends { [k: string]: any } = { [k: string]: any },
Q extends { [k: string]: any } = { [k: string]: any },
> = {
props?: T;
query?: Q;
};
export type BunextPageModuleMetadata = {

View File

@ -1,14 +1,7 @@
import grabDirNames from "./grab-dir-names";
export default function grabRouter() {
const { PAGES_DIR } = grabDirNames();
// if (process.env.NODE_ENV !== "production") {
// global.ROUTER.reload();
// }
if (process.env.NODE_ENV == "production") {
return global.ROUTER;
}
return new Bun.FileSystemRouter({
style: "nextjs",
dir: PAGES_DIR,
});
return global.ROUTER;
}