react-server-frame 0.0.1
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/README.md +69 -0
- package/dist/client.d.mts +24 -0
- package/dist/client.mjs +52 -0
- package/dist/index.d.mts +57 -0
- package/dist/index.mjs +106 -0
- package/dist/vite/entry.client.tsx +123 -0
- package/dist/vite/entry.ssr.tsx +62 -0
- package/dist/vite/fetch-frame.d.mts +6 -0
- package/dist/vite/fetch-frame.mjs +18 -0
- package/dist/vite/frames.d.mts +10 -0
- package/dist/vite/frames.mjs +56 -0
- package/dist/vite/plugin.d.mts +13 -0
- package/dist/vite/plugin.mjs +21 -0
- package/package.json +81 -0
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# A new type of RSC routing based on `<Frame />`
|
|
2
|
+
|
|
3
|
+
New type of RSC routing inspired by iFrames and Remix 3.
|
|
4
|
+
|
|
5
|
+
## How it works
|
|
6
|
+
|
|
7
|
+
### Define your routes
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { route } from "remix/fetch-router/routes";
|
|
11
|
+
|
|
12
|
+
export const routes = route({
|
|
13
|
+
frames: {
|
|
14
|
+
home: "/",
|
|
15
|
+
about: "/about",
|
|
16
|
+
partials: {
|
|
17
|
+
sidebar: "/frame/sidebar",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Map your components and render a `<Frame />` for the current location
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
const router = createRouter();
|
|
27
|
+
|
|
28
|
+
router.route("ANY", "*", {
|
|
29
|
+
handler: ({ request }) => {
|
|
30
|
+
return render(
|
|
31
|
+
request,
|
|
32
|
+
<ProvideFrames
|
|
33
|
+
frames={routes.frames}
|
|
34
|
+
components={{
|
|
35
|
+
about: About,
|
|
36
|
+
home: Home,
|
|
37
|
+
partials: {
|
|
38
|
+
sidebar: Sidebar,
|
|
39
|
+
},
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
<Frame src={request.url} />
|
|
43
|
+
</ProvideFrames>,
|
|
44
|
+
);
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Nest frames
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
<Suspense fallback={<p>Loading sidebar...</p>}>
|
|
53
|
+
<Frame src={routes.frames.partials.sidebar.href()} />
|
|
54
|
+
</Suspense>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Revalidate frames
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
"use client";
|
|
61
|
+
|
|
62
|
+
import { useFrame } from "react-server-frame/client";
|
|
63
|
+
|
|
64
|
+
export function ReloadFrame() {
|
|
65
|
+
const { pending, reload } = useFrame();
|
|
66
|
+
|
|
67
|
+
return <button onClick={reload}>Reload{pending ? "ing..." : ""}</button>;
|
|
68
|
+
}
|
|
69
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as _$react_jsx_runtime0 from "react/jsx-runtime";
|
|
2
|
+
|
|
3
|
+
//#region src/frames.client.d.ts
|
|
4
|
+
type Frame = {
|
|
5
|
+
pending: boolean;
|
|
6
|
+
reload: () => void;
|
|
7
|
+
};
|
|
8
|
+
declare function FetchFrameProvider({
|
|
9
|
+
children,
|
|
10
|
+
fetchFrame
|
|
11
|
+
}: {
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
fetchFrame?: (url: URL, signal: AbortSignal) => Promise<React.ReactNode>;
|
|
14
|
+
}): _$react_jsx_runtime0.JSX.Element;
|
|
15
|
+
declare function useFrame(): Frame;
|
|
16
|
+
declare function ClientFrame({
|
|
17
|
+
children,
|
|
18
|
+
src
|
|
19
|
+
}: {
|
|
20
|
+
children?: React.ReactNode;
|
|
21
|
+
src: string;
|
|
22
|
+
}): _$react_jsx_runtime0.JSX.Element;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { ClientFrame, FetchFrameProvider, useFrame };
|
package/dist/client.mjs
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createContext, use, useCallback, useMemo, useRef, useState, useTransition } from "react";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
//#region src/frames.client.tsx
|
|
5
|
+
const FrameContext = createContext(void 0);
|
|
6
|
+
FrameContext.displayName = "FrameContext";
|
|
7
|
+
const FetchFrameContext = createContext(void 0);
|
|
8
|
+
function FetchFrameProvider({ children, fetchFrame }) {
|
|
9
|
+
return /* @__PURE__ */ jsx(FetchFrameContext.Provider, {
|
|
10
|
+
value: fetchFrame,
|
|
11
|
+
children
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
function useFrame() {
|
|
15
|
+
let frame = use(FrameContext);
|
|
16
|
+
if (!frame) throw new Error("useFrame must be used within a Frame / ClientFrame");
|
|
17
|
+
return frame;
|
|
18
|
+
}
|
|
19
|
+
function isPromise(value) {
|
|
20
|
+
return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
|
|
21
|
+
}
|
|
22
|
+
function ClientFrame({ children, src }) {
|
|
23
|
+
const [_content, setContent] = useState(children);
|
|
24
|
+
const content = isPromise(_content) ? use(_content) : _content;
|
|
25
|
+
const [pending, startTransition] = useTransition();
|
|
26
|
+
const [lastChildren, setLastChildren] = useState(children);
|
|
27
|
+
if (lastChildren !== children) {
|
|
28
|
+
setLastChildren(children);
|
|
29
|
+
setContent(children);
|
|
30
|
+
}
|
|
31
|
+
const controllerRef = useRef(void 0);
|
|
32
|
+
const fetchFrame = use(FetchFrameContext);
|
|
33
|
+
const reload = useCallback(() => {
|
|
34
|
+
if (!fetchFrame) throw new Error("FetchFrameContext is not provided");
|
|
35
|
+
const thisController = new AbortController();
|
|
36
|
+
startTransition(() => setContent(fetchFrame(new URL(src, window.location.href), thisController.signal)));
|
|
37
|
+
controllerRef.current?.abort();
|
|
38
|
+
controllerRef.current = thisController;
|
|
39
|
+
}, [fetchFrame, src]);
|
|
40
|
+
const frame = useMemo(() => {
|
|
41
|
+
return {
|
|
42
|
+
pending,
|
|
43
|
+
reload
|
|
44
|
+
};
|
|
45
|
+
}, [pending, reload]);
|
|
46
|
+
return /* @__PURE__ */ jsx(FrameContext.Provider, {
|
|
47
|
+
value: frame,
|
|
48
|
+
children: content
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
export { ClientFrame, FetchFrameProvider, useFrame };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as _$react_jsx_runtime0 from "react/jsx-runtime";
|
|
2
|
+
import { ReactFormState } from "react-dom/client";
|
|
3
|
+
import { Route } from "remix/fetch-router/routes";
|
|
4
|
+
|
|
5
|
+
//#region src/frames.d.ts
|
|
6
|
+
declare class UseServerState {
|
|
7
|
+
formState?: ReactFormState;
|
|
8
|
+
returnValue?: Promise<unknown>;
|
|
9
|
+
temporaryReferences?: unknown;
|
|
10
|
+
status?: number;
|
|
11
|
+
constructor(formState: ReactFormState | undefined, returnValue: Promise<unknown> | undefined, temporaryReferences: unknown, status: number | undefined);
|
|
12
|
+
}
|
|
13
|
+
declare class RedirectState {
|
|
14
|
+
location: string;
|
|
15
|
+
constructor(location: string);
|
|
16
|
+
}
|
|
17
|
+
declare function redirect(location: string): void;
|
|
18
|
+
declare function render({
|
|
19
|
+
createTemporaryReferenceSet,
|
|
20
|
+
prerender,
|
|
21
|
+
renderToReadableStream,
|
|
22
|
+
request,
|
|
23
|
+
root
|
|
24
|
+
}: {
|
|
25
|
+
createTemporaryReferenceSet: () => unknown;
|
|
26
|
+
prerender: (body: ReadableStream<Uint8Array>) => Promise<Response>;
|
|
27
|
+
renderToReadableStream: (payload: any, options: {
|
|
28
|
+
temporaryReferences: unknown;
|
|
29
|
+
onError: (error: unknown) => string | undefined;
|
|
30
|
+
}) => ReadableStream<Uint8Array>;
|
|
31
|
+
request: Request;
|
|
32
|
+
root: React.ReactNode;
|
|
33
|
+
}): Promise<Response>;
|
|
34
|
+
declare class NotFoundError extends Error {
|
|
35
|
+
constructor(message: string);
|
|
36
|
+
}
|
|
37
|
+
interface Routes extends Record<string, Route | Routes> {}
|
|
38
|
+
type Components<R extends Record<any, any>> = { [K in keyof R]: (R[K] extends Record<any, any> ? Components<R[K]> : never) | React.ComponentType };
|
|
39
|
+
type ProvideFramesProps<Frames extends Routes> = {
|
|
40
|
+
children?: React.ReactNode;
|
|
41
|
+
components: Components<Frames>;
|
|
42
|
+
fetchFrame: (url: URL, signal: AbortSignal) => Promise<React.ReactNode>;
|
|
43
|
+
frames: Frames;
|
|
44
|
+
};
|
|
45
|
+
declare function ProvideFrames<Frames extends Routes>({
|
|
46
|
+
children,
|
|
47
|
+
components,
|
|
48
|
+
fetchFrame,
|
|
49
|
+
frames
|
|
50
|
+
}: ProvideFramesProps<Frames>): _$react_jsx_runtime0.JSX.Element;
|
|
51
|
+
declare function Frame({
|
|
52
|
+
src
|
|
53
|
+
}: {
|
|
54
|
+
src: string;
|
|
55
|
+
}): _$react_jsx_runtime0.JSX.Element;
|
|
56
|
+
//#endregion
|
|
57
|
+
export { Frame, NotFoundError, ProvideFrames, ProvideFramesProps, RedirectState, UseServerState, redirect, render };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { ClientFrame, FetchFrameProvider } from "./client.mjs";
|
|
2
|
+
import { cache } from "react";
|
|
3
|
+
import { getContext } from "remix/async-context-middleware";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
//#region src/frames.tsx
|
|
6
|
+
var UseServerState = class {
|
|
7
|
+
formState;
|
|
8
|
+
returnValue;
|
|
9
|
+
temporaryReferences;
|
|
10
|
+
status;
|
|
11
|
+
constructor(formState, returnValue, temporaryReferences, status) {
|
|
12
|
+
this.formState = formState;
|
|
13
|
+
this.returnValue = returnValue;
|
|
14
|
+
this.temporaryReferences = temporaryReferences;
|
|
15
|
+
this.status = status;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
var RedirectState = class {
|
|
19
|
+
location;
|
|
20
|
+
constructor(location) {
|
|
21
|
+
this.location = location;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
function redirect(location) {
|
|
25
|
+
getContext().set(RedirectState, new RedirectState(location));
|
|
26
|
+
}
|
|
27
|
+
async function render({ createTemporaryReferenceSet, prerender, renderToReadableStream, request, root }) {
|
|
28
|
+
const ctx = getContext();
|
|
29
|
+
let redirect;
|
|
30
|
+
try {
|
|
31
|
+
redirect = ctx.get(RedirectState);
|
|
32
|
+
} catch {}
|
|
33
|
+
let state;
|
|
34
|
+
try {
|
|
35
|
+
state = ctx.get(UseServerState);
|
|
36
|
+
} catch {}
|
|
37
|
+
try {
|
|
38
|
+
const body = renderToReadableStream(redirect ? {
|
|
39
|
+
type: "redirect",
|
|
40
|
+
redirect: redirect.location,
|
|
41
|
+
returnValue: state?.returnValue
|
|
42
|
+
} : {
|
|
43
|
+
type: "render",
|
|
44
|
+
root,
|
|
45
|
+
returnValue: state?.returnValue,
|
|
46
|
+
formState: state?.formState
|
|
47
|
+
}, {
|
|
48
|
+
temporaryReferences: state?.temporaryReferences ?? createTemporaryReferenceSet(),
|
|
49
|
+
onError(error) {
|
|
50
|
+
if (error instanceof NotFoundError) return "404";
|
|
51
|
+
if (request.signal.aborted) return;
|
|
52
|
+
console.error(error);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
if (new URL(request.url).pathname.endsWith(".rsc")) return new Response(body, { headers: { "Content-Type": "text/x-component; charset=utf-8" } });
|
|
56
|
+
return await prerender(body);
|
|
57
|
+
} catch (reason) {
|
|
58
|
+
if (reason instanceof Error && reason.name === "NotFoundError") return new Response("Not Found", { status: 404 });
|
|
59
|
+
if (!request.signal.aborted) console.error(reason);
|
|
60
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const frameCache = cache(() => ({}));
|
|
64
|
+
var NotFoundError = class extends Error {
|
|
65
|
+
constructor(message) {
|
|
66
|
+
super(message);
|
|
67
|
+
this.name = "NotFoundError";
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
function ProvideFrames({ children, components, fetchFrame, frames }) {
|
|
71
|
+
const cache = frameCache();
|
|
72
|
+
cache.components = {
|
|
73
|
+
...cache.components,
|
|
74
|
+
...components
|
|
75
|
+
};
|
|
76
|
+
cache.frames = {
|
|
77
|
+
...cache.frames,
|
|
78
|
+
...frames
|
|
79
|
+
};
|
|
80
|
+
return /* @__PURE__ */ jsx(FetchFrameProvider, {
|
|
81
|
+
fetchFrame,
|
|
82
|
+
children
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function Frame({ src }) {
|
|
86
|
+
const cache = frameCache();
|
|
87
|
+
if (!cache.components || !cache.frames) throw new Error("No frames provided");
|
|
88
|
+
const url = new URL(src, "http://react-server-frame/");
|
|
89
|
+
if (url.pathname.endsWith(".rsc")) url.pathname = url.pathname.slice(0, -4);
|
|
90
|
+
const Component = match(cache.frames, cache.components, url.href);
|
|
91
|
+
if (!Component) throw new NotFoundError("No matching frame found");
|
|
92
|
+
return /* @__PURE__ */ jsx(ClientFrame, {
|
|
93
|
+
src: url.pathname + url.search,
|
|
94
|
+
children: /* @__PURE__ */ jsx(Component, {})
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function match(frames, components, href) {
|
|
98
|
+
for (const [id, route] of Object.entries(frames)) if (typeof route.pattern?.test === "function") {
|
|
99
|
+
if (route.pattern.test(href)) return components?.[id];
|
|
100
|
+
} else {
|
|
101
|
+
let matched = match(route, components?.[id], href);
|
|
102
|
+
if (matched) return matched;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
export { Frame, NotFoundError, ProvideFrames, RedirectState, UseServerState, redirect, render };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createFromFetch,
|
|
3
|
+
createFromReadableStream,
|
|
4
|
+
createTemporaryReferenceSet,
|
|
5
|
+
encodeReply,
|
|
6
|
+
setServerCallback,
|
|
7
|
+
} from "@vitejs/plugin-rsc/browser";
|
|
8
|
+
import { startTransition, StrictMode, use, useState } from "react";
|
|
9
|
+
import { hydrateRoot } from "react-dom/client";
|
|
10
|
+
import { rscStream } from "rsc-html-stream/client";
|
|
11
|
+
|
|
12
|
+
import type { Payload } from "../generic-payload.ts";
|
|
13
|
+
|
|
14
|
+
setServerCallback(async (id, args) => {
|
|
15
|
+
const url = new URL(window.location.href);
|
|
16
|
+
url.pathname += ".rsc";
|
|
17
|
+
const temporaryReferences = createTemporaryReferenceSet();
|
|
18
|
+
const payload = await createFromFetch<Payload>(
|
|
19
|
+
fetch(url, {
|
|
20
|
+
method: "POST",
|
|
21
|
+
body: await encodeReply(args, { temporaryReferences }),
|
|
22
|
+
headers: {
|
|
23
|
+
"x-rsc-action": id,
|
|
24
|
+
},
|
|
25
|
+
}),
|
|
26
|
+
{ temporaryReferences },
|
|
27
|
+
);
|
|
28
|
+
if (payload.type === "render") {
|
|
29
|
+
startTransition(() => {
|
|
30
|
+
setPayload(Promise.resolve(payload));
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (payload.type === "redirect") {
|
|
34
|
+
if (window.navigation) {
|
|
35
|
+
navigate(payload.redirect);
|
|
36
|
+
} else {
|
|
37
|
+
window.location.href = payload.redirect;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return payload.returnValue;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
createFromReadableStream<Payload>(rscStream).then(
|
|
44
|
+
(payload) =>
|
|
45
|
+
startTransition(() => {
|
|
46
|
+
hydrateRoot(
|
|
47
|
+
document,
|
|
48
|
+
<StrictMode>
|
|
49
|
+
<Content initialPayload={Promise.resolve(payload)} />
|
|
50
|
+
</StrictMode>,
|
|
51
|
+
{
|
|
52
|
+
formState: payload.formState,
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
}),
|
|
56
|
+
(reason) => console.error("Failed to hydrate root", reason),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
let setPayload: (payload: Promise<Payload>) => void;
|
|
60
|
+
|
|
61
|
+
let seenPayloads = new WeakSet<Payload>();
|
|
62
|
+
|
|
63
|
+
function Content({ initialPayload }: { initialPayload: Promise<Payload> }) {
|
|
64
|
+
const [promise, _setPayload] = useState(initialPayload);
|
|
65
|
+
setPayload = _setPayload;
|
|
66
|
+
|
|
67
|
+
const payload = use(promise);
|
|
68
|
+
|
|
69
|
+
if (payload.type === "redirect") {
|
|
70
|
+
if (window.navigation) {
|
|
71
|
+
if (!seenPayloads) {
|
|
72
|
+
navigate(payload.redirect);
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
} else {
|
|
76
|
+
window.location.href = payload.redirect;
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return payload.root;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let navigationController = new AbortController();
|
|
85
|
+
function navigate(to: string) {
|
|
86
|
+
const url = new URL(to, window.location.href);
|
|
87
|
+
url.pathname += ".rsc";
|
|
88
|
+
|
|
89
|
+
if (window.location.host !== url.host) {
|
|
90
|
+
window.location.href = url.href;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let thisController = new AbortController();
|
|
95
|
+
startTransition(() =>
|
|
96
|
+
setPayload(createFromFetch<Payload>(fetch(url, { signal: thisController.signal }))),
|
|
97
|
+
);
|
|
98
|
+
navigationController.abort();
|
|
99
|
+
navigationController = thisController;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
window.navigation?.addEventListener("navigate", (event) => {
|
|
103
|
+
if (!event.canIntercept) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (event.hashChange || event.downloadRequest !== null) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
event.intercept({
|
|
112
|
+
async handler() {
|
|
113
|
+
navigate(event.destination.url);
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (import.meta.hot) {
|
|
119
|
+
import.meta.hot.on("rsc:update", (e) => {
|
|
120
|
+
console.log("[vite-rsc:update]", e.file);
|
|
121
|
+
navigate(location.href);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createFromReadableStream } from "@vitejs/plugin-rsc/ssr";
|
|
2
|
+
import { use } from "react";
|
|
3
|
+
import { renderToReadableStream } from "react-dom/server";
|
|
4
|
+
import { injectRSCPayload } from "rsc-html-stream/server";
|
|
5
|
+
|
|
6
|
+
import type { Payload } from "../generic-payload.ts";
|
|
7
|
+
|
|
8
|
+
export async function prerender(body: ReadableStream<Uint8Array>) {
|
|
9
|
+
const [decodeBody, inlineBody] = body.tee();
|
|
10
|
+
|
|
11
|
+
let decode: Promise<Payload> | undefined;
|
|
12
|
+
function Content() {
|
|
13
|
+
decode ??= createFromReadableStream<Payload>(decodeBody);
|
|
14
|
+
const payload = use(decode);
|
|
15
|
+
|
|
16
|
+
if (payload.type === "redirect") {
|
|
17
|
+
return (
|
|
18
|
+
<html>
|
|
19
|
+
<head></head>
|
|
20
|
+
<body>
|
|
21
|
+
<meta http-equiv="refresh" content={`0; url=${payload.redirect}`} />
|
|
22
|
+
</body>
|
|
23
|
+
</html>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return <>{payload.root}</>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let notFoundError = false;
|
|
31
|
+
try {
|
|
32
|
+
const bootstrapScriptContent = await import.meta.viteRsc.loadBootstrapScriptContent("index");
|
|
33
|
+
|
|
34
|
+
const html = await renderToReadableStream(<Content />, {
|
|
35
|
+
bootstrapScriptContent,
|
|
36
|
+
onError(reason: unknown) {
|
|
37
|
+
if (
|
|
38
|
+
typeof reason === "object" &&
|
|
39
|
+
reason !== null &&
|
|
40
|
+
"digest" in reason &&
|
|
41
|
+
reason.digest === "404"
|
|
42
|
+
) {
|
|
43
|
+
notFoundError = true;
|
|
44
|
+
return reason.digest;
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const payload = await decode;
|
|
50
|
+
return new Response(html.pipeThrough(injectRSCPayload(inlineBody)), {
|
|
51
|
+
status: payload!.type === "redirect" ? 302 : notFoundError ? 404 : 200,
|
|
52
|
+
headers: {
|
|
53
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
} catch (reason) {
|
|
57
|
+
if (notFoundError) return new Response("Not Found", { status: 404 });
|
|
58
|
+
|
|
59
|
+
console.error("Failed to prerender", reason);
|
|
60
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import * as _$react from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/vite/fetch-frame.d.ts
|
|
4
|
+
declare function fetchFrame(url: URL, signal: AbortSignal): Promise<string | number | bigint | boolean | Iterable<_$react.ReactNode> | _$react.ReactElement<unknown, string | _$react.JSXElementConstructor<any>> | _$react.ReactPortal | null | undefined>;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { fetchFrame };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
//#region src/vite/fetch-frame.ts
|
|
3
|
+
if (typeof document !== "undefined") import("@vitejs/plugin-rsc/browser");
|
|
4
|
+
async function fetchFrame(url, signal) {
|
|
5
|
+
const { createFromFetch } = await import("@vitejs/plugin-rsc/browser");
|
|
6
|
+
url.pathname += ".rsc";
|
|
7
|
+
const payload = await createFromFetch(fetch(url, { signal }));
|
|
8
|
+
if (payload.type === "redirect") if (window.navigation) return Promise.resolve(window.navigation.navigate(payload.redirect, { history: "replace" }).finished).then(() => {
|
|
9
|
+
return null;
|
|
10
|
+
});
|
|
11
|
+
else {
|
|
12
|
+
window.location.href = payload.redirect;
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
return payload.root;
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { fetchFrame };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ProvideFrames as ProvideFrames$1 } from "../index.mjs";
|
|
2
|
+
import * as _$react_jsx_runtime0 from "react/jsx-runtime";
|
|
3
|
+
import { Middleware } from "remix/fetch-router";
|
|
4
|
+
|
|
5
|
+
//#region src/vite/frames.d.ts
|
|
6
|
+
declare function useServerMiddleware(): Middleware;
|
|
7
|
+
declare function render(request: Request, root: React.ReactNode): Promise<Response>;
|
|
8
|
+
declare function ProvideFrames(props: Omit<React.ComponentProps<typeof ProvideFrames$1>, "fetchFrame">): _$react_jsx_runtime0.JSX.Element;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { ProvideFrames, render, useServerMiddleware };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ProvideFrames as ProvideFrames$1, UseServerState, render as render$1 } from "../index.mjs";
|
|
2
|
+
import { fetchFrame } from "./fetch-frame.mjs";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
import { createTemporaryReferenceSet, decodeAction, decodeFormState, decodeReply, loadServerAction, renderToReadableStream } from "@vitejs/plugin-rsc/rsc";
|
|
5
|
+
//#region src/vite/frames.tsx
|
|
6
|
+
function useServerMiddleware() {
|
|
7
|
+
return async ({ request, set }, next) => {
|
|
8
|
+
let formState;
|
|
9
|
+
let returnValue;
|
|
10
|
+
let temporaryReferences;
|
|
11
|
+
let actionStatus;
|
|
12
|
+
if (request.method === "POST") {
|
|
13
|
+
const actionId = request.headers.get("x-rsc-action");
|
|
14
|
+
if (actionId) {
|
|
15
|
+
const body = request.headers.get("content-type")?.startsWith("multipart/form-data") ? await request.formData() : await request.text();
|
|
16
|
+
temporaryReferences = createTemporaryReferenceSet();
|
|
17
|
+
const args = await decodeReply(body, { temporaryReferences });
|
|
18
|
+
const action = await loadServerAction(actionId);
|
|
19
|
+
try {
|
|
20
|
+
returnValue = action.apply(null, args);
|
|
21
|
+
await returnValue;
|
|
22
|
+
} catch (e) {
|
|
23
|
+
actionStatus = 500;
|
|
24
|
+
returnValue = Promise.reject(e);
|
|
25
|
+
}
|
|
26
|
+
} else {
|
|
27
|
+
const formData = await request.formData();
|
|
28
|
+
const decodedAction = await decodeAction(formData);
|
|
29
|
+
try {
|
|
30
|
+
formState = await decodeFormState(await decodedAction(), formData);
|
|
31
|
+
} catch (e) {
|
|
32
|
+
console.error(e);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
set(UseServerState, new UseServerState(formState, returnValue, temporaryReferences, actionStatus));
|
|
37
|
+
return next();
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async function render(request, root) {
|
|
41
|
+
return render$1({
|
|
42
|
+
createTemporaryReferenceSet,
|
|
43
|
+
prerender: (await import.meta.viteRsc.import("./entry.ssr.tsx", { environment: "ssr" })).prerender,
|
|
44
|
+
renderToReadableStream,
|
|
45
|
+
request,
|
|
46
|
+
root
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function ProvideFrames(props) {
|
|
50
|
+
return /* @__PURE__ */ jsx(ProvideFrames$1, {
|
|
51
|
+
...props,
|
|
52
|
+
fetchFrame
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
export { ProvideFrames, render, useServerMiddleware };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as Vite from "vite";
|
|
2
|
+
|
|
3
|
+
//#region src/vite/plugin.d.ts
|
|
4
|
+
declare function framework({
|
|
5
|
+
entry
|
|
6
|
+
}?: {
|
|
7
|
+
entry?: string;
|
|
8
|
+
}): {
|
|
9
|
+
name: string;
|
|
10
|
+
config(this: Vite.ConfigPluginContext, userConfig: Vite.UserConfig): Record<string, any>;
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { framework };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import * as Vite from "vite";
|
|
4
|
+
//#region src/vite/plugin.ts
|
|
5
|
+
function getEntry(file) {
|
|
6
|
+
return Vite.normalizePath(path.join(path.dirname(fileURLToPath(import.meta.url)), file));
|
|
7
|
+
}
|
|
8
|
+
function framework({ entry = "/src/entry.server" } = {}) {
|
|
9
|
+
return {
|
|
10
|
+
name: "framework",
|
|
11
|
+
config(userConfig) {
|
|
12
|
+
return Vite.mergeConfig({ environments: {
|
|
13
|
+
client: { build: { rolldownOptions: { input: { index: getEntry("entry.client.tsx") } } } },
|
|
14
|
+
rsc: { build: { rolldownOptions: { input: { index: entry } } } },
|
|
15
|
+
ssr: { build: { rolldownOptions: { input: { index: getEntry("entry.ssr.tsx") } } } }
|
|
16
|
+
} }, userConfig, true);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { framework };
|
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-server-frame",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A new type of RSC routing.",
|
|
5
|
+
"homepage": "https://github.com/jacob-ebey/react-server-frame#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/jacob-ebey/react-server-frame/issues"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"author": "Jacob Ebey <jacob.ebey@live.com>",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/jacob-ebey/react-server-frame.git"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"type": "module",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": "./dist/index.mjs",
|
|
22
|
+
"./client": "./dist/client.mjs",
|
|
23
|
+
"./vite/fetch-frame": "./dist/vite/fetch-frame.mjs",
|
|
24
|
+
"./vite/frames": "./dist/vite/frames.mjs",
|
|
25
|
+
"./vite/plugin": "./dist/vite/plugin.mjs",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "vp pack",
|
|
33
|
+
"dev": "vp pack --watch",
|
|
34
|
+
"test": "vp test",
|
|
35
|
+
"check": "vp check",
|
|
36
|
+
"prepublishOnly": "vp run build"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"rsc-html-stream": "0.0.7"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/dom-navigation": "^1.0.7",
|
|
43
|
+
"@types/node": "catalog:",
|
|
44
|
+
"@types/react": "catalog:",
|
|
45
|
+
"@types/react-dom": "catalog:",
|
|
46
|
+
"@typescript/native-preview": "catalog:",
|
|
47
|
+
"@vitejs/plugin-react": "^6.0.1",
|
|
48
|
+
"@vitejs/plugin-rsc": "^0.5.21",
|
|
49
|
+
"bumpp": "^11.0.1",
|
|
50
|
+
"react": "catalog:",
|
|
51
|
+
"react-dom": "catalog:",
|
|
52
|
+
"react-server-dom-webpack": "catalog:",
|
|
53
|
+
"remix": "catalog:",
|
|
54
|
+
"typescript": "catalog:",
|
|
55
|
+
"vite": "catalog:",
|
|
56
|
+
"vite-plus": "catalog:"
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"@vitejs/plugin-react": "catalog:",
|
|
60
|
+
"@vitejs/plugin-rsc": "catalog:",
|
|
61
|
+
"react": "catalog:",
|
|
62
|
+
"react-dom": "catalog:",
|
|
63
|
+
"react-server-dom-webpack": "catalog:",
|
|
64
|
+
"remix": "*",
|
|
65
|
+
"vite": "*"
|
|
66
|
+
},
|
|
67
|
+
"peerDependenciesMeta": {
|
|
68
|
+
"@vitejs/plugin-react": {
|
|
69
|
+
"optional": true
|
|
70
|
+
},
|
|
71
|
+
"@vitejs/plugin-rsc": {
|
|
72
|
+
"optional": true
|
|
73
|
+
},
|
|
74
|
+
"react-server-dom-webpack": {
|
|
75
|
+
"optional": true
|
|
76
|
+
},
|
|
77
|
+
"vite": {
|
|
78
|
+
"optional": true
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|