~repos /edge-city

#react#js#ssr

git clone https://pyrossh.dev/repos/edge-city.git

edge-city is a next level meta-framework for react that runs only on edge runtimes


011acde7 pyrossh

2 years ago
get streaming ssr hydration working
Files changed (4) hide show
  1. example/src/pages/todos/page.jsx +7 -1
  2. index.js +1 -1
  3. readme.md +7 -8
  4. renderPage.js +105 -49
example/src/pages/todos/page.jsx CHANGED
@@ -1,4 +1,4 @@
1
- import React, { Suspense } from 'react';
1
+ import React, { Suspense, useEffect } from 'react';
2
2
  import { Helmet } from 'react-helmet-async';
3
3
  import { useQuery, useMutation } from "edge-city";
4
4
  import { useForm } from 'react-hook-form';
@@ -29,6 +29,7 @@ export default Page;
29
29
 
30
30
  const TodoList = () => {
31
31
  const { data, refetch } = useQuery("todos", () => getTodos());
32
+ console.log("data", data);
32
33
  const { mutate, isMutating, err } = useMutation(async ({ text }) => {
33
34
  await createTodo({
34
35
  text,
@@ -38,6 +39,11 @@ const TodoList = () => {
38
39
  });
39
40
  const { register, handleSubmit, formState: { errors } } = useForm();
40
41
  console.log('err', err, errors);
42
+ useEffect(() => {
43
+ setTimeout(() => {
44
+ refetch();
45
+ }, 3000)
46
+ }, [])
41
47
  return (
42
48
  <div>
43
49
  <ul>
index.js CHANGED
@@ -205,7 +205,7 @@ export const hydrateApp = async () => {
205
205
  module.default.hydrateRoot(root, _jsx(RouterProvider, {
206
206
  history,
207
207
  router,
208
- rpcContext: {},
208
+ rpcContext: window.__EC_RPC_DATA__ || {},
209
209
  helmetContext: {},
210
210
  }));
211
211
  }
readme.md CHANGED
@@ -40,14 +40,13 @@ which are compatible with these API's. Here is a list of some of them,
40
40
  2. `pnpm >= v8.5.1`
41
41
 
42
42
  ### Todo[General]
43
- 1. Hydrate rpc cache
44
- 2. Build a docs website
43
+ 1. Build a docs website
45
- 4. Add tests for bot
44
+ 2. Add tests for bot
46
- 5. Add tests for runtime
45
+ 3. Add tests for runtime
47
- 6. Add Env variables `PUBLIC_` for client
48
- 7. Add E2E tests for example
46
+ 4. Add E2E tests for example
49
- 8. Maybe move to vite for HMR goodness
47
+ 5. Maybe move to vite for HMR goodness
48
+ 6. Fix todos service not loading
50
49
 
51
50
  ### Todo[Cloudflare]
52
51
  1. Fix 404/500 pages not routing on server
53
- 2. Fix todos service not loading
52
+ 2. Fix neondb serverless driver
renderPage.js CHANGED
@@ -7,6 +7,100 @@ import isbot from "isbot";
7
7
  import routemap from '/routemap.json' assert {type: 'json'};
8
8
  import { RouterProvider } from "./index";
9
9
 
10
+ const stringToStream = (str) => {
11
+ return new ReadableStream({
12
+ start(controller) {
13
+ controller.enqueue(new TextEncoder().encode(str))
14
+ controller.close()
15
+ },
16
+ })
17
+ }
18
+
19
+ const createTagHtmlInjectTransformer = (
20
+ token,
21
+ oneTime,
22
+ inject,
23
+ ) => {
24
+ let injected = false
25
+
26
+ return new TransformStream({
27
+ transform(chunk, controller) {
28
+ if (!oneTime || !injected) {
29
+ const content = new TextDecoder().decode(chunk)
30
+ let index
31
+ if ((index = content.indexOf(token)) !== -1) {
32
+ const newContent =
33
+ content.slice(0, index) +
34
+ inject() +
35
+ content.slice(index, content.length)
36
+ injected = true
37
+ controller.enqueue(new TextEncoder().encode(newContent))
38
+ return
39
+ }
40
+ }
41
+ controller.enqueue(chunk)
42
+ },
43
+ })
44
+ }
45
+
46
+ const createEndHtmlInjectTransformer = (inject) => {
47
+ return new TransformStream({
48
+ flush(controller) {
49
+ controller.enqueue(new TextEncoder().encode(inject()))
50
+ },
51
+ transform(chunk, controller) {
52
+ controller.enqueue(chunk)
53
+ },
54
+ })
55
+ }
56
+
57
+ const render = async (children, {
58
+ injectBeforeBodyClose,
59
+ injectBeforeHeadClose,
60
+ injectBeforeEveryScript,
61
+ injectOnEnd,
62
+ isSeo,
63
+ }) => {
64
+ function transfromStream(stream) {
65
+ let out = stream
66
+ if (injectBeforeBodyClose) {
67
+ out = out.pipeThrough(
68
+ createTagHtmlInjectTransformer('</body>', true, injectBeforeBodyClose)
69
+ )
70
+ }
71
+ if (injectBeforeHeadClose) {
72
+ out = out.pipeThrough(
73
+ createTagHtmlInjectTransformer('</head>', true, injectBeforeHeadClose)
74
+ )
75
+ }
76
+ if (injectBeforeEveryScript) {
77
+ out = out.pipeThrough(
78
+ createTagHtmlInjectTransformer(
79
+ '<script>',
80
+ false,
81
+ injectBeforeEveryScript
82
+ )
83
+ )
84
+ }
85
+ if (injectOnEnd) {
86
+ out = out.pipeThrough(
87
+ createEndHtmlInjectTransformer(injectOnEnd)
88
+ )
89
+ }
90
+ return out;
91
+ }
92
+
93
+ try {
94
+ const reactStream = await renderToReadableStream(children)
95
+ if (isSeo) {
96
+ await reactStream.allReady;
97
+ }
98
+ return transfromStream(reactStream);
99
+ } catch (error) {
100
+ throw error;
101
+ }
102
+ }
103
+
10
104
  const renderPage = async (Page, req) => {
11
105
  const url = new URL(req.url);
12
106
  const history = createMemoryHistory({
@@ -22,47 +116,7 @@ const renderPage = async (Page, req) => {
22
116
  const jsScript = url.pathname === "/" ? "/index" : url.pathname;
23
117
  const helmetContext = {};
24
118
  const rpcContext = {};
25
- if (isbot(req.headers.get('User-Agent')) || url.search.includes("ec_is_bot=true")) {
26
- const stream = await renderToReadableStream(_jsx("body", {
27
- children: _jsx("div", {
28
- id: "root",
29
- children: _jsx(RouterProvider, {
30
- history,
31
- router,
32
- rpcCache: {},
33
- helmetContext,
34
- }),
35
- })
36
- }))
37
- await stream.allReady;
38
- let isFirstChunk = true;
39
- // TODO: add rpcContext
40
- const transformStream = new TransformStream({
41
- transform(chunk, controller) {
42
- if (isFirstChunk) {
43
- isFirstChunk = false;
44
- const encoder = new TextEncoder();
45
- controller.enqueue(encoder.encode(`<!DOCTYPE html><html lang="en"><head>`));
46
- for (const key of Object.keys(helmetContext.helmet)) {
47
- controller.enqueue(encoder.encode(helmetContext.helmet[key].toString()));
48
- }
49
- controller.enqueue(encoder.encode(`<link rel="stylesheet" href="/css/app.css">`));
50
- controller.enqueue(encoder.encode(`<script type="module" src="/js${jsScript}.js?hydrate=true" defer></script>`));
51
- controller.enqueue(encoder.encode(`</head>`));
52
- }
53
- controller.enqueue(chunk);
54
- },
55
- flush(controller) {
56
- controller.enqueue(new TextEncoder().encode(`</html>`));
57
- controller.terminate();
58
- },
59
- });
60
- return new Response(stream.pipeThrough(transformStream), {
61
- headers: { 'Content-Type': 'text/html' },
62
- status: 200,
63
- });
64
- }
65
- const stream = await renderToReadableStream(
119
+ const stream = await render(
66
120
  _jsxs("html", {
67
121
  lang: "en",
68
122
  children: [
@@ -84,17 +138,19 @@ const renderPage = async (Page, req) => {
84
138
  rpcContext,
85
139
  helmetContext,
86
140
  }),
87
- _jsx(_Fragment, {
88
- children: _jsx("script", {
89
- type: "module",
90
- defer: true,
91
- src: `/js${jsScript}.js?hydrate=true`,
92
- })
93
- }),
94
141
  ]
95
142
  })
96
143
  })]
144
+ }), {
145
+ isSeo: isbot(req.headers.get('User-Agent')) || url.search.includes("ec_is_bot=true"),
146
+ injectBeforeHeadClose: () =>
147
+ Object.keys(helmetContext.helmet)
148
+ .map((k) => helmetContext.helmet[k].toString())
149
+ .join(''),
150
+ injectBeforeBodyClose: () => ''
151
+ + `<script>window.__EC_RPC_DATA__ = ${JSON.stringify(rpcContext)};</script>`
152
+ + `<script type="module" src="/js${jsScript}.js?hydrate=true"></script>`
97
- }));
153
+ });
98
154
  return new Response(stream, {
99
155
  headers: { 'Content-Type': 'text/html' },
100
156
  status: 200,