~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


f0281e30 Peter John

2 years ago
basic csr working
packages/example/routes/app.jsx CHANGED
@@ -5,21 +5,21 @@ import { ErrorBoundary } from "parotta/error";
5
5
 
6
6
  const App = ({ routerProps, children }) => {
7
7
  return (
8
+ <SWRConfig value={{
9
+ fallback: {
10
+ 'https://jsonplaceholder.typicode.com/todos/1': { id: '123' },
11
+ },
12
+ // fallbackData: null,
13
+ fetcher: (resource, init) => fetch(resource, init).then(res => res.json()), suspense: true
14
+ }}>
15
+ <ErrorBoundary onError={(err) => console.log(err)} fallback={<p>Oops something went wrong</p>}>
16
+ <Suspense fallback={<p>Loading...</p>}>
8
- <RouterProvider value={routerProps}>
17
+ <RouterProvider value={routerProps}>
9
- <SWRConfig value={{
10
- fallback: {
11
- 'https://jsonplaceholder.typicode.com/todos/1': { id: '123' },
12
- },
13
- // fallbackData: null,
14
- fetcher: (resource, init) => fetch(resource, init).then(res => res.json()), suspense: true
15
- }}>
16
- <ErrorBoundary onError={(err) => console.log(err)} fallback={<p>Oops something went wrong</p>}>
17
- <Suspense fallback={<p>Loading...</p>}>
18
18
  {children}
19
+ </RouterProvider>
19
- </Suspense>
20
+ </Suspense>
20
- </ErrorBoundary>
21
+ </ErrorBoundary>
21
- </SWRConfig>
22
+ </SWRConfig>
22
- </RouterProvider>
23
23
  )
24
24
  }
25
25
 
packages/example/routes/page.jsx CHANGED
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import useSWR from "swr";
3
- import { useRouter } from "parotta/router";
3
+ import { Link, useRouter } from "parotta/router";
4
4
  import Counter from "@/components/Counter/Counter";
5
5
  import "./page.css";
6
6
 
@@ -18,7 +18,7 @@ const HomePage = () => {
18
18
  <Counter />
19
19
  </div>
20
20
  <footer>
21
- <a href="/about">About us</a>
21
+ <Link href="/about">About us</Link>
22
22
  </footer>
23
23
  </div>
24
24
  )
packages/parotta/cli.js CHANGED
@@ -20,7 +20,7 @@ console.log("running in folder:", path.basename(process.cwd()), "env:", process.
20
20
 
21
21
  const isProd = process.env.NODE_ENV === "production";
22
22
 
23
- const mapFiles = () => {
23
+ const mapFiles = ({ includeStatic, includeApi }) => {
24
24
  const routes = {};
25
25
  const dirs = walkdir.sync(path.join(process.cwd(), "routes"))
26
26
  .map((s) => s.replace(process.cwd(), "")
@@ -34,19 +34,23 @@ const mapFiles = () => {
34
34
  const key = page.route || "/";
35
35
  routes[key] = { key: key, page: page.path };
36
36
  });
37
+ if (includeApi) {
37
- dirs.filter((p) => p.includes('api.js'))
38
+ dirs.filter((p) => p.includes('api.js'))
38
- .map((s) => s.replace(process.cwd(), ""))
39
+ .map((s) => s.replace(process.cwd(), ""))
39
- .map((s) => ({ path: s, route: s.replace("/api.js", "") }))
40
+ .map((s) => ({ path: s, route: s.replace("/api.js", "") }))
40
- .forEach((api) => {
41
+ .forEach((api) => {
41
- const key = api.route || "/";
42
+ const key = api.route || "/";
42
- routes[key] = routes[key] || { key };
43
+ routes[key] = routes[key] || { key };
43
- routes[key].api = api.path;
44
+ routes[key].api = api.path;
44
- });
45
+ });
46
+ }
47
+ if (includeStatic) {
45
- walkdir.sync(path.join(process.cwd(), "static"))
48
+ walkdir.sync(path.join(process.cwd(), "static"))
46
- .map((s) => s.replace(process.cwd(), "").replace("/static", ""))
49
+ .map((s) => s.replace(process.cwd(), "").replace("/static", ""))
47
- .forEach((route) => {
50
+ .forEach((route) => {
48
- routes[route] = { key: route, file: route }
51
+ routes[route] = { key: route, file: route }
49
- });
52
+ });
53
+ }
50
54
  return routes;
51
55
  }
52
56
 
@@ -61,6 +65,13 @@ const mapDeps = (dir) => {
61
65
  }
62
66
 
63
67
  const removeCwd = (s) => s.replace(process.cwd(), "")
68
+ const serverSideRoutes = mapFiles({ includeApi: true, includeStatic: true });
69
+ const clientSideRoutes = mapFiles({ includeApi: false, includeStatic: false });
70
+
71
+ const radixRouter = createRouter({
72
+ strictTrailingSlash: true,
73
+ routes: serverSideRoutes,
74
+ });
64
75
 
65
76
  const hydrateScript = (appPath, pagePath, routerProps) => {
66
77
  return `
@@ -78,10 +89,6 @@ hydrateRoot(document.getElementById("root"), React.createElement(App, {
78
89
  `;
79
90
  }
80
91
 
81
- const radixRouter = createRouter({
82
- strictTrailingSlash: true,
83
- routes: mapFiles(),
84
- });
85
92
 
86
93
  const renderApi = async (filePath, req) => {
87
94
  const routeImport = await import(path.join(process.cwd(), filePath));
@@ -114,9 +121,12 @@ const renderPage = async (filePath, url, params) => {
114
121
  query[key] = url.searchParams.get(key);
115
122
  }
116
123
  const routerProps = {
124
+ routes: clientSideRoutes,
125
+ state: {
117
- query: query,
126
+ query: query,
118
- params: params,
127
+ params: params,
119
- pathname: url.pathname,
128
+ pathname: url.pathname,
129
+ },
120
130
  }
121
131
  const appPath = path.join(process.cwd(), "routes", "app.jsx");
122
132
  const pagePath = path.join(process.cwd(), filePath);
@@ -127,7 +137,6 @@ const renderPage = async (filePath, url, params) => {
127
137
  return acc;
128
138
  }, {})
129
139
  const App = (await import(appPath)).default;
130
- const Page = (await import(pagePath)).default;
131
140
  const components = mapDeps("components");
132
141
  const containers = mapDeps("containers");
133
142
  const cssFile = `${filePath.replace("jsx", "css")}`;
@@ -142,11 +151,14 @@ const renderPage = async (filePath, url, params) => {
142
151
  "imports": {
143
152
  "radix3": `https://esm.sh/radix3`,
144
153
  "react": `https://esm.sh/react@18.2.0`,
154
+ // TODO: need to remove this
145
155
  "react/jsx-dev-runtime": `https://esm.sh/react@18.2.0/jsx-dev-runtime`,
146
156
  "react-dom/client": `https://esm.sh/react-dom@18.2.0/client`,
147
157
  "react-streaming": "https://esm.sh/react-streaming",
148
- "parotta/router": `https://esm.sh/parotta@${version}/router.js`,
158
+ // "parotta/router": `https://esm.sh/parotta@${version}/router.js`,
149
- "parotta/error": `https://esm.sh/parotta@${version}/error.js`,
159
+ // "parotta/error": `https://esm.sh/parotta@${version}/error.js`,
160
+ "parotta/router": `/parotta/router.js`,
161
+ "parotta/error": `/parotta/error.js`,
150
162
  ...nodeDeps,
151
163
  ...components,
152
164
  ...containers,
@@ -165,7 +177,6 @@ const renderPage = async (filePath, url, params) => {
165
177
  <body>
166
178
  <div id="root">
167
179
  <App routerProps={routerProps}>
168
- <Page />
169
180
  </App>
170
181
  </div>
171
182
  </body>
@@ -178,15 +189,15 @@ const renderPage = async (filePath, url, params) => {
178
189
  });
179
190
  }
180
191
 
181
- const renderCss = async (url) => {
192
+ const renderCss = async (src) => {
182
193
  try {
183
- const cssText = await Bun.file(path.join(process.cwd(), url.pathname)).text();
194
+ const cssText = await Bun.file(src).text();
184
195
  const result = await postcss([
185
196
  autoprefixer(),
186
197
  postcssCustomMedia(),
187
198
  // postcssNormalize({ browsers: 'last 2 versions' }),
188
199
  postcssNesting,
189
- ]).process(cssText, { from: url.pathname, to: url.pathname });
200
+ ]).process(cssText, { from: src, to: src });
190
201
  return new Response(result.css, {
191
202
  headers: { 'Content-Type': 'text/css' },
192
203
  status: 200,
@@ -228,10 +239,10 @@ const renderJs = async (src) => {
228
239
  }
229
240
  }
230
241
 
231
- const sendFile = async (file) => {
242
+ const sendFile = async (src) => {
232
243
  try {
233
- const contentType = mimeTypes.lookup(file) || "application/octet-stream";
244
+ const contentType = mimeTypes.lookup(src) || "application/octet-stream";
234
- const stream = await Bun.file(path.join(process.cwd(), file)).stream();
245
+ const stream = await Bun.file(src).stream();
235
246
  return new Response(stream, {
236
247
  headers: { 'Content-Type': contentType },
237
248
  status: 200,
@@ -248,20 +259,21 @@ export default {
248
259
  port: 3000,
249
260
  async fetch(req) {
250
261
  const url = new URL(req.url);
262
+ console.log("GET", url.pathname);
263
+ // maybe this is needed
264
+ if (url.pathname.startsWith("/parotta/")) {
265
+ return renderJs(path.join(import.meta.dir, url.pathname.replace("/parotta/", "")));
266
+ }
251
267
  if (url.pathname.endsWith(".css")) {
252
- return renderCss(url);
268
+ return renderCss(path.join(process.cwd(), url.pathname));
253
269
  }
254
270
  if (url.pathname.endsWith(".js") || url.pathname.endsWith(".jsx")) {
255
271
  return renderJs(path.join(process.cwd(), url.pathname));
256
272
  }
257
- // maybe this is needed
258
- // if (url.pathname.startsWith("/parotta/")) {
259
- // return renderJs(path.join(import.meta.dir, url.pathname.replace("/parotta/", "")) + ".js");
260
- // }
261
273
  const match = radixRouter.lookup(url.pathname);
262
274
  if (match) {
263
275
  if (match.file) {
264
- return sendFile(`/static${match.file}`);
276
+ return sendFile(path.join(process.cwd(), `/static${match.file}`));
265
277
  }
266
278
  if (match.page && req.headers.get("Accept")?.includes('text/html')) {
267
279
  return renderPage(`/routes${match.page}`, url, match.params);
packages/parotta/router.js CHANGED
@@ -1,4 +1,5 @@
1
- import React, { createContext, useContext } from "react";
1
+ import React, { createContext, useContext, useState, useMemo, useEffect } from "react";
2
+ import { createRouter } from 'radix3';
2
3
 
3
4
  export const RouterContext = createContext({
4
5
  stack: [],
@@ -9,13 +10,47 @@ export const RouterContext = createContext({
9
10
  }
10
11
  });
11
12
 
13
+ const getBasePath = () => typeof window !== "undefined" ? "" : process.cwd();
14
+
12
15
  export const RouterProvider = ({ value, children }) => {
16
+ const [path, setPath] = useState(value.state.pathname);
17
+ const [state, setState] = useState(value.state);
18
+ const [Page, setPage] = useState();
19
+ const radixRouter = useMemo(() => createRouter({
20
+ strictTrailingSlash: true,
21
+ routes: value.routes,
22
+ }), [])
23
+ useEffect(() => {
24
+ window.addEventListener('popstate', function (event) {
25
+ setPath(location.pathname);
26
+ }, false);
27
+ }, [])
28
+ useEffect(() => {
29
+ if (path !== value.state.pathname) {
30
+ let match = radixRouter.lookup(path)
31
+ if (!match) {
32
+ match = { page: '/404.jsx' }
33
+ console.log('route not matched');
34
+ } else {
35
+ console.log('route match', match, `${getBasePath()}/routes${match.page}`);
36
+ }
37
+ import(`${getBasePath()}/routes${match.page}`)
38
+ .then((comp) => {
39
+ setState({ pathname: path, params: match.params, query: {} });
40
+ setPage(comp.default);
41
+ })
42
+ } else {
43
+ setState(value.state);
44
+ setPage(null);
45
+ }
46
+ }, [path])
13
47
  return React.createElement(RouterContext.Provider, {
14
48
  value: {
15
49
  stack: [],
16
- state: value,
50
+ state,
51
+ setPath,
17
52
  },
18
- children,
53
+ children: Page || children,
19
54
  });
20
55
  }
21
56
 
@@ -35,4 +70,20 @@ export const useRouter = () => {
35
70
  },
36
71
  reload: () => window.location.reload(),
37
72
  };
73
+ }
74
+
75
+ export const Link = (props) => {
76
+ const ctx = useContext(RouterContext);
77
+
78
+ return React.createElement("a", {
79
+ ...props,
80
+ onClick: (e) => {
81
+ e.preventDefault();
82
+ if (props && props.onClick) {
83
+ props.onClick(e);
84
+ }
85
+ history.pushState({}, "", props.href);
86
+ ctx.setPath(props.href)
87
+ },
88
+ })
38
89
  }