Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support 404 for rsc #76

Merged
merged 4 commits into from
Jun 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## [Unreleased]
### Changed
- fix: rename getBuilder to getBuildConfig #75
- feat: support 404 for rsc #76

## [0.11.3] - 2023-06-10
### Changed
Expand Down
28 changes: 28 additions & 0 deletions examples/07_router/src/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Component } from "react";
import type { ReactNode, FunctionComponent } from "react";

interface Props {
fallback: (error: unknown) => ReactNode;
children: ReactNode;
}

class ErrorBoundaryClass extends Component<Props, { error?: unknown }> {
constructor(props: Props) {
super(props);
this.state = {};
}

static getDerivedStateFromError(error: unknown) {
return { error };
}

render() {
if ("error" in this.state) {
return this.props.fallback(this.state.error);
}
return this.props.children;
}
}

export const ErrorBoundary =
ErrorBoundaryClass as unknown as FunctionComponent<Props>;
6 changes: 5 additions & 1 deletion examples/07_router/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { Router } from "waku/router/client";

import { ErrorBoundary } from "./ErrorBoundary.js";

const root = createRoot(document.getElementById("root")!);

root.render(
<StrictMode>
<Router />
<ErrorBoundary fallback={(error) => <h1>{String(error)}</h1>}>
<Router />
</ErrorBoundary>
</StrictMode>
);
21 changes: 16 additions & 5 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ import { cache, use, useEffect, useState } from "react";
import type { ReactElement } from "react";
import { createFromFetch, encodeReply } from "react-server-dom-webpack/client";

const checkStatus = async (
responsePromise: Promise<Response>
): Promise<Response> => {
const response = await responsePromise;
if (!response.ok) {
const err = new Error(response.statusText);
(err as any).statusCode = response.status;
throw err;
}
return response;
};

export function serve<Props>(rscId: string, basePath = "/RSC/") {
type SetRerender = (
rerender: (next: [ReactElement, string]) => void
Expand Down Expand Up @@ -35,7 +47,7 @@ export function serve<Props>(rscId: string, basePath = "/RSC/") {
method: "POST",
body: await encodeReply(args),
});
const data = createFromFetch(response, options);
const data = createFromFetch(checkStatus(response), options);
if (isMutating) {
rerender?.([data, serializedProps]);
}
Expand All @@ -45,10 +57,9 @@ export function serve<Props>(rscId: string, basePath = "/RSC/") {
const prefetched = (globalThis as any).__WAKU_PREFETCHED__?.[rscId]?.[
serializedProps
];
const data = createFromFetch(
prefetched || fetch(basePath + rscId + "/" + searchParams),
options
);
const response =
prefetched || fetch(basePath + rscId + "/" + searchParams);
const data = createFromFetch(checkStatus(response), options);
return [data, setRerender];
}
);
Expand Down
9 changes: 7 additions & 2 deletions src/lib/middleware/rsc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import RSDWServer from "react-server-dom-webpack/server.node.unbundled";
import busboy from "busboy";

import { resolveConfig } from "../config.js";
import { hasStatusCode } from "./rsc/utils.js";
import { renderRSC } from "./rsc/worker-api.js";

type Middleware = (
Expand Down Expand Up @@ -62,8 +63,12 @@ export function rsc(options: {
: { rscId: rscId as string, props }
);
readable.on("error", (err) => {
console.info("Cannot render RSC", err);
res.statusCode = 500;
if (hasStatusCode(err)) {
res.statusCode = err.statusCode;
} else {
console.info("Cannot render RSC", err);
res.statusCode = 500;
}
if (options.mode === "development") {
res.end(String(err));
} else {
Expand Down
3 changes: 3 additions & 0 deletions src/lib/middleware/rsc/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Buffer } from "node:buffer";
import { Transform } from "node:stream";

export const hasStatusCode = (x: unknown): x is { statusCode: number } =>
typeof (x as any)?.statusCode === "number";

export const codeToInject = `
globalThis.__waku_module_cache__ = new Map();
globalThis.__webpack_chunk_load__ = (id) => import(id).then((m) => globalThis.__waku_module_cache__.set(id, m));
Expand Down
11 changes: 7 additions & 4 deletions src/lib/middleware/rsc/worker-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export type MessageRes =
| { id: number; type: "buf"; buf: ArrayBuffer; offset: number; len: number }
| { id: number; type: "moduleId"; moduleId: string }
| { id: number; type: "end" }
| { id: number; type: "err"; err: unknown }
| { id: number; type: "err"; err: unknown; statusCode?: number }
| {
id: number;
type: "builder";
Expand Down Expand Up @@ -78,9 +78,12 @@ export function renderRSC(
passthrough.end();
messageCallbacks.delete(id);
} else if (mesg.type === "err") {
passthrough.destroy(
mesg.err instanceof Error ? mesg.err : new Error(String(mesg.err))
);
const err =
mesg.err instanceof Error ? mesg.err : new Error(String(mesg.err));
if (mesg.statusCode) {
(err as any).statusCode = mesg.statusCode;
}
passthrough.destroy(err);
messageCallbacks.delete(id);
}
});
Expand Down
9 changes: 7 additions & 2 deletions src/lib/middleware/rsc/worker-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { createElement } from "react";
import RSDWServer from "react-server-dom-webpack/server";

import { configFileConfig, resolveConfig } from "../../config.js";
import { transformRsfId } from "./utils.js";
import { hasStatusCode, transformRsfId } from "./utils.js";
import type { MessageReq, MessageRes } from "./worker-api.js";
import { defineEntries } from "../../../server.js";
import type { RenderInput } from "../../../server.js";
Expand Down Expand Up @@ -54,6 +54,9 @@ const handleRender = async (mesg: MessageReq & { type: "render" }) => {
pipeable.pipe(writable);
} catch (err) {
const mesg: MessageRes = { id, type: "err", err };
if (hasStatusCode(err)) {
mesg.statusCode = err.statusCode;
}
parentPort!.postMessage(mesg);
}
};
Expand Down Expand Up @@ -143,7 +146,9 @@ const getFunctionComponent = async (rscId: string) => {
if (typeof mod?.default === "function") {
return mod?.default;
}
throw new Error("No function component found");
const err = new Error("No function component found");
(err as any).statusCode = 404; // HACK our convention for NotFound
throw err;
};

const resolveClientEntry = (filePath: string) => {
Expand Down
5 changes: 4 additions & 1 deletion src/router/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ const RouterContext = createContext<{
export function useChangeLocation() {
const value = useContext(RouterContext);
if (!value) {
throw new Error("Missing Router");
const dummyFn: ChangeLocation = () => {
throw new Error("Missing Router");
};
return dummyFn;
}
return value.changeLocation;
}
Expand Down
15 changes: 13 additions & 2 deletions src/router/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,18 @@ export function defineRouter(
getAllPaths: (root: string) => Promise<string[]>
): { getEntry: GetEntry; getBuildConfig: GetBuildConfig } {
const getEntry = async (id: string) => {
const mod = await getComponent(id);
let mod: Awaited<ReturnType<typeof getComponent>>;
try {
mod = await getComponent(id);
} catch (e) {
if (
e instanceof Error &&
e.message.startsWith("Unknown variable dynamic import")
) {
return null;
}
throw e;
}
const component =
typeof mod === "function" ? mod : mod?.default || Fragment;
const RouteComponent: FunctionComponent<any> = (props: RouteProps) => {
Expand Down Expand Up @@ -91,7 +102,7 @@ export function defineRouter(
globalThis.__WAKU_ROUTER_PREFETCH__ = (pathname, search) => {
const path = search ? pathname + "?" + search : pathname;
const path2ids = ${JSON.stringify(path2moduleIds)};
for (const id of path2ids[path]) {
for (const id of path2ids[path] || []) {
import(id);
}
};`;
Expand Down