~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


091ac2e7 Peter John

2 years ago
improve routing
packages/example/bun.lockb CHANGED
Binary file
packages/example/routes/about/page.jsx CHANGED
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { useRouter } from "parotta/router";
2
+ import { Link, useRouter } from "parotta/router";
3
3
  import "./page.css";
4
4
 
5
5
  const AboutPage = () => {
@@ -12,6 +12,9 @@ const AboutPage = () => {
12
12
  Path: {router.pathname}
13
13
  </p>
14
14
  </div>
15
+ <footer>
16
+ <Link href="/">Back</Link>
17
+ </footer>
15
18
  </div>
16
19
  )
17
20
  }
packages/example/routes/app.jsx CHANGED
@@ -1,9 +1,9 @@
1
1
  import React, { Suspense } from "react";
2
2
  import { SWRConfig } from "swr";
3
- import { RouterProvider } from "parotta/router";
4
3
  import { ErrorBoundary } from "parotta/error";
5
4
 
6
- const App = ({ routerProps, children }) => {
5
+ const App = ({ children }) => {
6
+ console.log('app');
7
7
  return (
8
8
  <SWRConfig value={{
9
9
  fallback: {
@@ -14,9 +14,7 @@ const App = ({ routerProps, children }) => {
14
14
  }}>
15
15
  <ErrorBoundary onError={(err) => console.log(err)} fallback={<p>Oops something went wrong</p>}>
16
16
  <Suspense fallback={<p>Loading...</p>}>
17
- <RouterProvider value={routerProps}>
18
- {children}
17
+ {children}
19
- </RouterProvider>
20
18
  </Suspense>
21
19
  </ErrorBoundary>
22
20
  </SWRConfig>
packages/example/routes/page.jsx CHANGED
@@ -5,6 +5,7 @@ import Counter from "@/components/Counter/Counter";
5
5
  import "./page.css";
6
6
 
7
7
  const HomePage = () => {
8
+ console.log('page');
8
9
  // const todo = useAsync('123', () => getData());
9
10
  const { data } = useSWR(`https://jsonplaceholder.typicode.com/todos/1`);
10
11
  const router = useRouter();
packages/parotta/bun.lockb CHANGED
Binary file
packages/parotta/cli.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env bun --hot
2
2
 
3
+ import React from 'react';
3
4
  import path from 'path';
4
5
  import walkdir from 'walkdir';
5
6
  import postcss from "postcss"
@@ -7,8 +8,10 @@ import autoprefixer from "autoprefixer";
7
8
  import postcssCustomMedia from "postcss-custom-media";
8
9
  // import postcssNormalize from 'postcss-normalize';
9
10
  import postcssNesting from "postcss-nesting";
11
+ import { createMemoryHistory } from "history";
10
12
  import { createRouter } from 'radix3';
11
13
  import mimeTypes from "mime-types";
14
+ import { Router } from "./router";
12
15
  import { renderToReadableStream } from 'react-dom/server';
13
16
  // import { renderToStream } from './render';
14
17
 
@@ -20,7 +23,7 @@ console.log("running in folder:", path.basename(process.cwd()), "env:", process.
20
23
 
21
24
  const isProd = process.env.NODE_ENV === "production";
22
25
 
23
- const mapFiles = ({ includeStatic, includeApi }) => {
26
+ const mapFiles = () => {
24
27
  const routes = {};
25
28
  const dirs = walkdir.sync(path.join(process.cwd(), "routes"))
26
29
  .map((s) => s.replace(process.cwd(), "")
@@ -34,23 +37,19 @@ const mapFiles = ({ includeStatic, includeApi }) => {
34
37
  const key = page.route || "/";
35
38
  routes[key] = { key: key, page: page.path };
36
39
  });
37
- if (includeApi) {
38
- dirs.filter((p) => p.includes('api.js'))
40
+ dirs.filter((p) => p.includes('api.js'))
39
- .map((s) => s.replace(process.cwd(), ""))
41
+ .map((s) => s.replace(process.cwd(), ""))
40
- .map((s) => ({ path: s, route: s.replace("/api.js", "") }))
42
+ .map((s) => ({ path: s, route: s.replace("/api.js", "") }))
41
- .forEach((api) => {
43
+ .forEach((api) => {
42
- const key = api.route || "/";
44
+ const key = api.route || "/";
43
- routes[key] = routes[key] || { key };
45
+ routes[key] = routes[key] || { key };
44
- routes[key].api = api.path;
46
+ routes[key].api = api.path;
45
- });
47
+ });
46
- }
47
- if (includeStatic) {
48
- walkdir.sync(path.join(process.cwd(), "static"))
48
+ walkdir.sync(path.join(process.cwd(), "static"))
49
- .map((s) => s.replace(process.cwd(), "").replace("/static", ""))
49
+ .map((s) => s.replace(process.cwd(), "").replace("/static", ""))
50
- .forEach((route) => {
50
+ .forEach((route) => {
51
- routes[route] = { key: route, file: route }
51
+ routes[route] = { key: route, file: route }
52
- });
52
+ });
53
- }
54
53
  return routes;
55
54
  }
56
55
 
@@ -64,32 +63,20 @@ const mapDeps = (dir) => {
64
63
  }, {});
65
64
  }
66
65
 
66
+ const mapPages = () => walkdir.sync(path.join(process.cwd(), "routes"))
67
+ .filter((p) => p.includes('page.jsx'))
67
- const removeCwd = (s) => s.replace(process.cwd(), "")
68
+ .map((s) => s.replace(process.cwd(), ""))
69
+ .map((s) => s.replace("/routes", ""))
70
+ .map((s) => s.replace("/page.jsx", ""));
71
+
68
- const serverSideRoutes = mapFiles({ includeApi: true, includeStatic: true });
72
+ const serverSideRoutes = mapFiles();
69
- const clientSideRoutes = mapFiles({ includeApi: false, includeStatic: false });
73
+ const clientSideRoutes = mapPages();
70
74
 
71
75
  const radixRouter = createRouter({
72
76
  strictTrailingSlash: true,
73
77
  routes: serverSideRoutes,
74
78
  });
75
79
 
76
- const hydrateScript = (appPath, pagePath, routerProps) => {
77
- return `
78
- import React from "react";
79
- import { hydrateRoot } from "react-dom/client";
80
- import App from "${removeCwd(appPath)}";
81
- import Page from "${removeCwd(pagePath)}";
82
-
83
- const routerProps = ${JSON.stringify(routerProps)};
84
-
85
- hydrateRoot(document.getElementById("root"), React.createElement(App, {
86
- routerProps: routerProps,
87
- children: React.createElement(Page, {}),
88
- }));
89
- `;
90
- }
91
-
92
-
93
80
  const renderApi = async (filePath, req) => {
94
81
  const routeImport = await import(path.join(process.cwd(), filePath));
95
82
  switch (req.method) {
@@ -116,33 +103,19 @@ const renderApi = async (filePath, req) => {
116
103
  }
117
104
 
118
105
  const renderPage = async (filePath, url, params) => {
119
- const query = {};
120
- for (const key of url.searchParams.keys()) {
121
- query[key] = url.searchParams.get(key);
122
- }
123
- const routerProps = {
124
- routes: clientSideRoutes,
125
- state: {
126
- query: query,
127
- params: params,
128
- pathname: url.pathname,
129
- },
130
- }
131
- const appPath = path.join(process.cwd(), "routes", "app.jsx");
132
- const pagePath = path.join(process.cwd(), filePath);
133
106
  const packageJson = await import(path.join(process.cwd(), "package.json"));
134
107
  const devTag = !isProd ? "?dev" : "";
135
108
  const nodeDeps = Object.keys(packageJson.default.dependencies).reduce((acc, dep) => {
136
109
  acc[dep] = `https://esm.sh/${dep}@${packageJson.default.dependencies[dep]}`;
137
110
  return acc;
138
111
  }, {})
139
- const App = (await import(appPath)).default;
140
112
  const components = mapDeps("components");
141
113
  const containers = mapDeps("containers");
142
114
  const cssFile = `${filePath.replace("jsx", "css")}`;
143
115
  const stream = await renderToReadableStream(
144
116
  <html lang="en">
145
117
  <head>
118
+ <title>Parotta</title>
146
119
  <link rel="preload" href={cssFile} as="style" />
147
120
  <link rel="stylesheet" href={cssFile} />
148
121
  <script type="importmap" dangerouslySetInnerHTML={{
@@ -150,11 +123,11 @@ const renderPage = async (filePath, url, params) => {
150
123
  {
151
124
  "imports": {
152
125
  "radix3": `https://esm.sh/radix3`,
126
+ "history": "https://esm.sh/history@5.3.0",
153
- "react": `https://esm.sh/react@18.2.0`,
127
+ "react": `https://esm.sh/react@18.2.0${devTag}`,
154
- // TODO: need to remove this
128
+ // TODO: need to remove this in prod
155
- "react/jsx-dev-runtime": `https://esm.sh/react@18.2.0/jsx-dev-runtime`,
129
+ "react/jsx-dev-runtime": `https://esm.sh/react@18.2.0${devTag}/jsx-dev-runtime`,
156
- "react-dom/client": `https://esm.sh/react-dom@18.2.0/client`,
130
+ "react-dom/client": `https://esm.sh/react-dom@18.2.0${devTag}/client`,
157
- "react-streaming": "https://esm.sh/react-streaming",
158
131
  // "parotta/router": `https://esm.sh/parotta@${version}/router.js`,
159
132
  // "parotta/error": `https://esm.sh/parotta@${version}/error.js`,
160
133
  "parotta/router": `/parotta/router.js`,
@@ -168,16 +141,41 @@ const renderPage = async (filePath, url, params) => {
168
141
  }}>
169
142
  </script>
170
143
  <script type="module" defer={true} dangerouslySetInnerHTML={{
144
+ __html: `
145
+ import React from "react";
146
+ import { hydrateRoot } from "react-dom/client";
147
+ import { createBrowserHistory } from "history";
148
+ import { createRouter } from "radix3";
171
- __html: hydrateScript(appPath, pagePath, routerProps)
149
+ import { Router } from "parotta/router";
150
+
151
+ hydrateRoot(document.getElementById("root"), React.createElement(Router, {
152
+ App: React.lazy(() => import("/routes/app.jsx")),
153
+ history: createBrowserHistory(),
154
+ radixRouter: createRouter({
155
+ strictTrailingSlash: true,
156
+ routes: {
157
+ ${clientSideRoutes.map((r) => `"${r === "" ? "/" : r}": React.lazy(() => import("/routes${r === "" ? "" : r}/page.jsx"))`).join(',\n ')}
158
+ },
159
+ }),
160
+ }));
161
+ `
172
162
  }}></script>
173
- <title>
174
- Parotta
175
- </title>
176
163
  </head>
177
164
  <body>
178
165
  <div id="root">
166
+ <Router
167
+ App={React.lazy(() => import(`${process.cwd()}/routes/app.jsx`))}
168
+ history={createMemoryHistory({
169
+ initialEntries: [url.pathname + url.search],
170
+ })}
179
- <App routerProps={routerProps}>
171
+ radixRouter={createRouter({
172
+ strictTrailingSlash: true,
173
+ routes: clientSideRoutes.reduce((acc, r) => {
174
+ acc[r === "" ? "/" : r] = React.lazy(() => import(`${process.cwd()}/routes${r === "" ? "" : r}/page.jsx`));
175
+ return acc
176
+ }, {})
177
+ })}
180
- </App>
178
+ />
181
179
  </div>
182
180
  </body>
183
181
  </html >
packages/parotta/package.json CHANGED
@@ -4,6 +4,7 @@
4
4
  "type": "module",
5
5
  "dependencies": {
6
6
  "autoprefixer": "^10.4.14",
7
+ "history": "^5.3.0",
7
8
  "mime-types": "2.1.35",
8
9
  "postcss": "^8.4.21",
9
10
  "postcss-custom-media": "^9.1.2",
packages/parotta/router.js CHANGED
@@ -1,80 +1,55 @@
1
- import React, { createContext, useContext, useState, useMemo, useEffect } from "react";
1
+ import React, { createContext, useContext, useState, useEffect } from "react";
2
- import { createRouter } from 'radix3';
3
2
 
4
- export const RouterContext = createContext({
3
+ export const RouterContext = createContext(undefined);
5
- stack: [],
6
- state: {
7
- pathname: "",
8
- query: {},
9
- params: {},
10
- }
11
- });
12
-
13
- const getBasePath = () => typeof window !== "undefined" ? "" : process.cwd();
14
4
 
15
- export const RouterProvider = ({ value, children }) => {
5
+ export const Router = ({ App, history, radixRouter }) => {
16
- const [path, setPath] = useState(value.state.pathname);
6
+ const [Page, setPage] = useState(radixRouter.lookup(history.location.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
7
  useEffect(() => {
24
- window.addEventListener('popstate', function (event) {
8
+ return history.listen(({ action, location }) => {
25
- setPath(location.pathname);
26
- }, false);
27
- }, [])
28
- useEffect(() => {
29
- if (path !== value.state.pathname) {
30
- let match = radixRouter.lookup(path)
9
+ const matchedPage = radixRouter.lookup(location.pathname);
31
- if (!match) {
10
+ if (!matchedPage) {
32
- match = { page: '/404.jsx' }
11
+ matchedPage = '/404.jsx';
33
12
  console.log('route not matched');
13
+ setPage(() => <h1> Not found</h1>);
34
14
  } else {
35
- console.log('route match', match, `${getBasePath()}/routes${match.page}`);
15
+ console.log('route match', matchedPage);
16
+ setPage(matchedPage);
36
17
  }
37
- import(`${getBasePath()}/routes${match.page}`)
38
- .then((comp) => {
39
- setState({ pathname: path, params: match.params, query: {} });
40
- setPage(comp.default);
41
- })
18
+ });
42
- } else {
43
- setState(value.state);
44
- setPage(null);
45
- }
46
- }, [path])
19
+ }, [])
20
+ console.log('Router');
47
21
  return React.createElement(RouterContext.Provider, {
48
- value: {
22
+ value: history,
49
- stack: [],
50
- state,
51
- setPath,
23
+ children: React.createElement(App, {
24
+ children: React.createElement(Page, {})
52
- },
25
+ }),
53
- children: Page || children,
54
26
  });
55
27
  }
56
28
 
57
29
  export const useRouter = () => {
58
- const ctx = useContext(RouterContext);
30
+ const history = useContext(RouterContext);
59
31
  return {
32
+ pathname: history.location.pathname,
33
+ query: {},
60
- ...ctx.state,
34
+ params: {},
61
35
  push: (path) => {
36
+ history.push(path)
62
37
  },
63
38
  replace: (path) => {
39
+ history.replace(path)
64
40
  },
65
- prefetch: () => {
41
+ forward: () => {
66
- },
67
- beforePopState: () => {
42
+ history.forward();
68
43
  },
69
44
  back: () => {
45
+ history.back();
70
46
  },
71
47
  reload: () => window.location.reload(),
72
48
  };
73
49
  }
74
50
 
75
51
  export const Link = (props) => {
76
- const ctx = useContext(RouterContext);
52
+ const router = useRouter();
77
-
78
53
  return React.createElement("a", {
79
54
  ...props,
80
55
  onClick: (e) => {
@@ -82,8 +57,7 @@ export const Link = (props) => {
82
57
  if (props && props.onClick) {
83
58
  props.onClick(e);
84
59
  }
85
- history.pushState({}, "", props.href);
86
- ctx.setPath(props.href)
60
+ router.push(props.href);
87
61
  },
88
62
  })
89
63
  }
readme.md CHANGED
@@ -0,0 +1,5 @@
1
+ # parotta
2
+
3
+ parotta is a next level meta-framework for react that runs only on the bun js runtime.
4
+ It uses File system routing with SSR with streaming + CSR as the method to render pages. Basically a MPA + SPA Transitional App.
5
+ It is very opionated and has set of idiomatic ways of doing things.