~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


485b9513 Peter John

2 years ago
add Header
packages/example/package.json CHANGED
@@ -10,5 +10,8 @@
10
10
  "dependencies": {
11
11
  "react": "18.2.0",
12
12
  "react-dom": "^18.2.0"
13
+ },
14
+ "parotta": {
15
+ "hydrate": true
13
16
  }
14
17
  }
packages/example/routes/about/page.jsx CHANGED
@@ -4,9 +4,7 @@ import "./page.css";
4
4
 
5
5
  export function Head() {
6
6
  return (
7
- <>
8
- <title>About us</title>
7
+ <title>About us</title>
9
- </>
10
8
  )
11
9
  }
12
10
 
packages/example/routes/app.jsx CHANGED
@@ -2,7 +2,6 @@ import React, { Suspense } from "react";
2
2
  import { ErrorBoundary } from "parotta/error";
3
3
 
4
4
  export default function App({ children }) {
5
- console.log('app');
6
5
  return (
7
6
  <ErrorBoundary onError={(err) => console.log(err)} fallback={<p>Oops something went wrong</p>}>
8
7
  <Suspense fallback={<p>Loading...</p>}>
packages/example/routes/page.jsx CHANGED
@@ -6,9 +6,7 @@ import "./page.css";
6
6
  export function Head() {
7
7
  const { data } = useFetch("/todos");
8
8
  return (
9
- <>
10
- <title>Parotta</title>
9
+ <title>Parotta</title>
11
- </>
12
10
  )
13
11
  }
14
12
 
@@ -16,12 +14,11 @@ export default function Page() {
16
14
  const { data, cache } = useFetch("/todos");
17
15
  console.log('page');
18
16
  console.log('data', data);
19
- useEffect(() => {
17
+ // useEffect(() => {
20
- setTimeout(() => {
18
+ // setTimeout(() => {
21
- cache.invalidate(/todos/);
19
+ // cache.invalidate(/todos/);
22
- }, 3000)
20
+ // }, 3000)
23
- }, [])
21
+ // }, [])
24
- // const todo = useAsync('123', () => getData());
25
22
  const router = useRouter();
26
23
  return (
27
24
  <div className="home-page">
packages/parotta/cli.js CHANGED
@@ -11,7 +11,7 @@ import postcssNesting from "postcss-nesting";
11
11
  import { createMemoryHistory } from "history";
12
12
  import { createRouter } from 'radix3';
13
13
  import mimeTypes from "mime-types";
14
- import { Router } from "./router";
14
+ import { Header, Router } from "./router";
15
15
  import { renderToReadableStream } from 'react-dom/server';
16
16
  // import { renderToStream } from './render';
17
17
 
@@ -72,11 +72,19 @@ const mapPages = () => walkdir.sync(path.join(process.cwd(), "routes"))
72
72
  const serverSideRoutes = mapFiles();
73
73
  const clientSideRoutes = mapPages();
74
74
 
75
- const radixRouter = createRouter({
75
+ const serverRouter = createRouter({
76
76
  strictTrailingSlash: true,
77
77
  routes: serverSideRoutes,
78
78
  });
79
79
 
80
+ const clientRouter = createRouter({
81
+ strictTrailingSlash: true,
82
+ routes: clientSideRoutes.reduce((acc, r) => {
83
+ acc[r === "" ? "/" : r] = React.lazy(() => import(`${process.cwd()}/routes${r === "" ? "" : r}/page.jsx`));
84
+ return acc
85
+ }, {})
86
+ });
87
+
80
88
  const renderApi = async (filePath, req) => {
81
89
  const routeImport = await import(path.join(process.cwd(), filePath));
82
90
  switch (req.method) {
@@ -104,6 +112,7 @@ const renderApi = async (filePath, req) => {
104
112
 
105
113
  const renderPage = async (filePath, url, params) => {
106
114
  const packageJson = await import(path.join(process.cwd(), "package.json"));
115
+ const config = packageJson.default.parotta || { hydrate: true };
107
116
  const devTag = !isProd ? "?dev" : "";
108
117
  const nodeDeps = Object.keys(packageJson.default.dependencies).reduce((acc, dep) => {
109
118
  acc[dep] = `https://esm.sh/${dep}@${packageJson.default.dependencies[dep]}`;
@@ -111,73 +120,78 @@ const renderPage = async (filePath, url, params) => {
111
120
  }, {})
112
121
  const components = mapDeps("components");
113
122
  const containers = mapDeps("containers");
114
- const cssFile = `${filePath.replace("jsx", "css")}`;
123
+ const history = createMemoryHistory({
115
- const mainPage = await import(path.join(process.cwd(), filePath));
124
+ initialEntries: [url.pathname + url.search],
125
+ });
116
126
  const stream = await renderToReadableStream(
117
127
  <html lang="en">
118
128
  <head>
119
- <link rel="preload" href={cssFile} as="style" />
120
- <link rel="stylesheet" href={cssFile} />
121
- <script type="importmap" dangerouslySetInnerHTML={{
122
- __html: JSON.stringify(
123
- {
124
- "imports": {
125
- "radix3": `https://esm.sh/radix3`,
126
- "history": "https://esm.sh/history@5.3.0",
127
- "react": `https://esm.sh/react@18.2.0${devTag}`,
128
- // TODO: need to remove this in prod
129
- "react/jsx-dev-runtime": `https://esm.sh/react@18.2.0${devTag}/jsx-dev-runtime`,
130
- "react-dom/client": `https://esm.sh/react-dom@18.2.0${devTag}/client`,
131
- // "parotta/router": `https://esm.sh/parotta@${version}/router.js`,
132
- // "parotta/error": `https://esm.sh/parotta@${version}/error.js`,
133
- "parotta/router": `/parotta/router.js`,
134
- "parotta/error": `/parotta/error.js`,
135
- ...nodeDeps,
136
- ...components,
137
- ...containers,
129
+ <Header
138
- }
139
- }
140
- )
141
- }}>
142
- </script>
143
- <mainPage.Head />
144
- <script type="module" defer={true} dangerouslySetInnerHTML={{
145
- __html: `
146
- import React from "react";
147
- import { hydrateRoot } from "react-dom/client";
148
- import { createBrowserHistory } from "history";
149
- import { createRouter } from "radix3";
150
- import { Router } from "parotta/router";
151
-
152
- hydrateRoot(document.getElementById("root"), React.createElement(Router, {
153
- App: React.lazy(() => import("/routes/app.jsx")),
154
- history: createBrowserHistory(),
130
+ history={history}
155
- radixRouter: createRouter({
131
+ radixRouter={clientRouter}
156
- strictTrailingSlash: true,
157
- routes: {
158
- ${clientSideRoutes.map((r) => `"${r === "" ? "/" : r}": React.lazy(() => import("/routes${r === "" ? "" : r}/page.jsx"))`).join(',\n ')}
159
- },
160
- }),
161
- }));
162
- `
163
- }}></script>
132
+ />
164
133
  </head>
165
134
  <body>
166
135
  <div id="root">
167
136
  <Router
168
137
  App={React.lazy(() => import(`${process.cwd()}/routes/app.jsx`))}
169
- history={createMemoryHistory({
138
+ history={history}
170
- initialEntries: [url.pathname + url.search],
171
- })}
172
- radixRouter={createRouter({
139
+ radixRouter={clientRouter}
173
- strictTrailingSlash: true,
174
- routes: clientSideRoutes.reduce((acc, r) => {
175
- acc[r === "" ? "/" : r] = React.lazy(() => import(`${process.cwd()}/routes${r === "" ? "" : r}/page.jsx`));
176
- return acc
177
- }, {})
178
- })}
179
140
  />
180
141
  </div>
142
+ {config.hydrate &&
143
+ <>
144
+ <script type="importmap" dangerouslySetInnerHTML={{
145
+ __html: JSON.stringify(
146
+ {
147
+ "imports": {
148
+ "radix3": `https://esm.sh/radix3`,
149
+ "history": "https://esm.sh/history@5.3.0",
150
+ "react": `https://esm.sh/react@18.2.0${devTag}`,
151
+ // TODO: need to remove this in prod
152
+ "react/jsx-dev-runtime": `https://esm.sh/react@18.2.0${devTag}/jsx-dev-runtime`,
153
+ "react-dom/client": `https://esm.sh/react-dom@18.2.0${devTag}/client`,
154
+ // "parotta/router": `https://esm.sh/parotta@${version}/router.js`,
155
+ // "parotta/error": `https://esm.sh/parotta@${version}/error.js`,
156
+ "parotta/router": `/parotta/router.js`,
157
+ "parotta/error": `/parotta/error.js`,
158
+ ...nodeDeps,
159
+ ...components,
160
+ ...containers,
161
+ }
162
+ }
163
+ )
164
+ }}>
165
+ </script>
166
+ <script type="module" defer={true} dangerouslySetInnerHTML={{
167
+ __html: `
168
+ import React from "react";
169
+ import { hydrateRoot } from "react-dom/client";
170
+ import { createBrowserHistory } from "history";
171
+ import { createRouter } from "radix3";
172
+ import { Header, Router } from "parotta/router";
173
+
174
+ const history = createBrowserHistory();
175
+ const radixRouter = createRouter({
176
+ strictTrailingSlash: true,
177
+ routes: {
178
+ ${clientSideRoutes.map((r) => `"${r === "" ? "/" : r}": React.lazy(() => import("/routes${r === "" ? "" : r}/page.jsx"))`).join(',\n ')}
179
+ },
180
+ });
181
+
182
+ hydrateRoot(document.head, React.createElement(Header, {
183
+ history,
184
+ radixRouter,
185
+ }))
186
+
187
+ hydrateRoot(document.getElementById("root"), React.createElement(Router, {
188
+ App: React.lazy(() => import("/routes/app.jsx")),
189
+ history,
190
+ radixRouter,
191
+ }));`}}>
192
+ </script>
193
+ </>
194
+ }
181
195
  </body>
182
196
  </html >
183
197
  );
@@ -226,7 +240,10 @@ const renderJs = async (src) => {
226
240
  // TODO
227
241
  //.replaceAll("$jsx", "React.createElement");
228
242
  return new Response(js, {
243
+ headers: {
229
- headers: { 'Content-Type': 'application/javascript' },
244
+ 'Content-Type': 'application/javascript',
245
+ // 'Link': `</routes/about/page.css>; rel=prefetch`,
246
+ },
230
247
  status: 200,
231
248
  });
232
249
  } catch (err) {
@@ -268,7 +285,7 @@ export default {
268
285
  if (url.pathname.endsWith(".js") || url.pathname.endsWith(".jsx")) {
269
286
  return renderJs(path.join(process.cwd(), url.pathname));
270
287
  }
271
- const match = radixRouter.lookup(url.pathname);
288
+ const match = serverRouter.lookup(url.pathname);
272
289
  if (match) {
273
290
  if (match.file) {
274
291
  return sendFile(path.join(process.cwd(), `/static${match.file}`));
packages/parotta/router.js CHANGED
@@ -1,7 +1,8 @@
1
- import React, { createContext, useContext, useState, useEffect, useMemo } from "react";
1
+ import React, { createContext, useContext, useState, useEffect, useMemo, useSyncExternalStore } from "react";
2
2
 
3
3
  export const isClient = () => typeof window !== 'undefined';
4
4
  export const domain = () => isClient() ? window.origin : "http://0.0.0.0:3000";
5
+ export const basePath = () => isClient() ? "" : process.cwd()
5
6
  export const globalCache = new Map();
6
7
  export const useFetchCache = () => {
7
8
  const [_, rerender] = useState(false);
@@ -65,24 +66,30 @@ const getMatch = (radixRouter, pathname) => {
65
66
  return matchedPage;
66
67
  }
67
68
 
68
- const loadCss = (pathname) => {
69
- const href = `/routes${pathname === "/" ? "" : pathname}/page.css`;
69
+ const getCssUrl = (pathname) => `/routes${pathname === "/" ? "/page.css" : pathname + "/page.css"}`;
70
+
71
+ export const Header = ({ history, radixRouter }) => {
72
+ useEffect(() => {
73
+ return history.listen(({ location }) => {
70
- const isLoaded = Array.from(document.getElementsByTagName("link"))
74
+ // document.getElementById("pageCss").href = getCssUrl(location.pathname)
75
+ });
76
+ }, []);
71
- .map((link) => link.href.replace(window.origin, "")).includes(href);
77
+ return React.createElement(React.Suspense, {
72
- if (!isLoaded) {
78
+ children: [
73
- const fileref = document.createElement("link");
79
+ React.createElement("link", {
80
+ id: "pageCss",
74
- fileref.rel = "stylesheet";
81
+ rel: "stylesheet",
75
- fileref.type = "text/css";
82
+ href: getCssUrl(history.location.pathname)
76
- fileref.href = href;
77
- document.getElementsByTagName("head")[0].appendChild(fileref);
78
- }
83
+ }),
84
+ React.createElement(React.lazy(() => import(`${basePath()}/routes/page.jsx`).then((js) => ({ default: js.Head }))), {}),
85
+ ]
86
+ });
79
87
  }
80
88
 
81
89
  export const Router = ({ App, history, radixRouter }) => {
82
90
  const [MatchedPage, setMatchedPage] = useState(() => getMatch(radixRouter, history.location.pathname));
83
91
  useEffect(() => {
84
92
  return history.listen(({ location }) => {
85
- loadCss(location.pathname);
86
93
  setMatchedPage(getMatch(radixRouter, location.pathname));
87
94
  });
88
95
  }, [])
@@ -93,7 +100,7 @@ export const Router = ({ App, history, radixRouter }) => {
93
100
  params: MatchedPage.params || {},
94
101
  },
95
102
  children: React.createElement(App, {
96
- children: React.createElement(MatchedPage, {})
103
+ children: React.createElement(MatchedPage, {}),
97
104
  }),
98
105
  });
99
106
  }
@@ -116,6 +123,10 @@ export const Link = (props) => {
116
123
  const router = useRouter();
117
124
  return React.createElement("a", {
118
125
  ...props,
126
+ // onMouseOver: (e) => {
127
+ // fetch(`/routes${pathname === "/" ? "" : pathname}/page.css`);
128
+ // fetch(`/routes${pathname === "/" ? "" : pathname}/page.jsx`);
129
+ // },
119
130
  onClick: (e) => {
120
131
  e.preventDefault();
121
132
  if (props && props.onClick) {