edge-city

#react#js#ssr

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

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


5a97fa6 — Peter John 2023-03-21T22:48:30+05:30
improve stuff

Generated files not shown: package-lock.json, packages/example/bun.lockb

bun.toml DELETED
@@ -1,3 +0,0 @@
1
- [loaders]
2
- ".jsx" = "js"
3
- ".css" = "css"
packages/example/components/Counter/Counter.jsx CHANGED
@@ -1,4 +1,4 @@
1
- import { useState } from "react";
1
+ import React, { useState } from "react";
2
2
 
3
3
  const Counter = () => {
4
4
  const [count, setCount] = useState(5);
packages/example/components/Todo/Todo.jsx CHANGED
@@ -1,3 +1,5 @@
1
+ import React from 'react';
2
+
1
3
  const Todo = ({ id, text }) => {
2
4
  return (
3
5
  <div id={id}>
packages/example/containers/TodoList/TodoList.jsx CHANGED
@@ -1,3 +1,4 @@
1
+ import React from 'react';
1
2
  import Todo from "@/components/Todo/Todo";
2
3
 
3
4
  const todos = [
packages/example/jsconfig.json CHANGED
@@ -4,6 +4,10 @@
4
4
  "@/*": [
5
5
  "./*"
6
6
  ]
7
- }
7
+ },
8
+ // TODO these options are not supported
9
+ "jsx": "react",
10
+ "jsxFactory": "React.createElement",
11
+ "jsxImportSource": "react"
8
12
  }
9
13
  }
packages/example/package.json CHANGED
@@ -8,8 +8,8 @@
8
8
  "run": "docker run -p 3000:3000 example"
9
9
  },
10
10
  "dependencies": {
11
- "parotta": "0.1.0",
12
11
  "react": "18.2.0",
13
- "react-dom": "^18.2.0"
12
+ "react-dom": "^18.2.0",
13
+ "swr": "2.1.0"
14
14
  }
15
15
  }
packages/example/routes/about/page.css CHANGED
@@ -0,0 +1,3 @@
1
+ .about-page {
2
+ padding: 100px;
3
+ }
packages/example/routes/about/page.jsx CHANGED
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ import { useRouter } from "parotta/router";
3
+ import "./page.css";
4
+
5
+ const AboutPage = () => {
6
+ const router = useRouter();
7
+ return (
8
+ <div className="about-page">
9
+ <div>
10
+ <h1>About Page</h1>
11
+ <p>
12
+ Path: {router.pathname}
13
+ </p>
14
+ </div>
15
+ </div>
16
+ )
17
+ }
18
+
19
+ export default AboutPage;
packages/example/routes/app.jsx ADDED
@@ -0,0 +1,26 @@
1
+ import React, { Suspense } from "react";
2
+ import { SWRConfig } from "swr";
3
+ import { RouterProvider } from "parotta/router";
4
+ import { ErrorBoundary } from "parotta/error";
5
+
6
+ const App = ({ routerProps, children }) => {
7
+ return (
8
+ <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
+ {children}
19
+ </Suspense>
20
+ </ErrorBoundary>
21
+ </SWRConfig>
22
+ </RouterProvider>
23
+ )
24
+ }
25
+
26
+ export default App;
packages/example/routes/page.css CHANGED
@@ -8,4 +8,8 @@
8
8
  font-size: 30px;
9
9
  font-weight: 600;
10
10
  }
11
+
12
+ & footer {
13
+ margin-top: 100px;
14
+ }
11
15
  }
packages/example/routes/page.jsx CHANGED
@@ -1,18 +1,25 @@
1
+ import React from 'react';
2
+ import useSWR from "swr";
3
+ import { useRouter } from "parotta/router";
1
4
  import Counter from "@/components/Counter/Counter";
2
- // import { useRouter } from "muffinjs/router.js";
3
5
  import "./page.css";
4
6
 
5
7
  const HomePage = () => {
8
+ // const todo = useAsync('123', () => getData());
9
+ const { data } = useSWR(`https://jsonplaceholder.typicode.com/todos/1`);
6
- // const router = useRouter();
10
+ const router = useRouter();
7
11
  return (
8
12
  <div className="home-page">
9
13
  <div>
10
14
  <h1>Home Page</h1>
11
- {/* <p>
15
+ <p>
12
16
  Path: {router.pathname}
13
- </p> */}
17
+ </p>
14
18
  <Counter />
15
19
  </div>
20
+ <footer>
21
+ <a href="/about">About us</a>
22
+ </footer>
16
23
  </div>
17
24
  )
18
25
  }
packages/parotta/cli.js CHANGED
@@ -7,24 +7,19 @@ import autoprefixer from "autoprefixer";
7
7
  import postcssCustomMedia from "postcss-custom-media";
8
8
  // import postcssNormalize from 'postcss-normalize';
9
9
  import postcssNesting from "postcss-nesting";
10
- import { renderToReadableStream } from 'react-dom/server';
11
10
  import { createRouter } from 'radix3';
12
11
  import mimeTypes from "mime-types";
12
+ import { renderToReadableStream } from 'react-dom/server';
13
- import { routerSignal } from './router.js';
13
+ // import { renderToStream } from './render';
14
14
 
15
+ const version = (await import(path.join(import.meta.dir, "package.json"))).default.version;
16
+ console.log(`parotta v${version}`)
15
17
  console.log("running in folder:", path.basename(process.cwd()), "env:", process.env.NODE_ENV);
16
18
  // console.log("deleting cache");
17
19
  // rmSync(path.join(process.cwd(), ".cache"), { force: true, recursive: true })
18
20
 
19
21
  const isProd = process.env.NODE_ENV === "production";
20
22
 
21
- const transpiler = new Bun.Transpiler({
22
- loader: "jsx",
23
- autoImportJSX: true,
24
- jsxOptimizationInline: true,
25
- });
26
-
27
-
28
23
  const mapFiles = () => {
29
24
  const routes = {};
30
25
  const dirs = walkdir.sync(path.join(process.cwd(), "routes"))
@@ -65,16 +60,21 @@ const mapDeps = (dir) => {
65
60
  }, {});
66
61
  }
67
62
 
63
+ const removeCwd = (s) => s.replace(process.cwd(), "")
64
+
68
- const hydrateScript = (filePath, initialRouteValue) => {
65
+ const hydrateScript = (appPath, pagePath, routerProps) => {
69
66
  return `
70
- import React from 'react';
67
+ import React from "react";
71
- import { hydrateRoot } from 'react-dom/client';
68
+ import { hydrateRoot } from "react-dom/client";
72
- import { routerSignal } from "parotta/router";
69
+ import App from "${removeCwd(appPath)}";
73
- import Page from "${filePath}";
70
+ import Page from "${removeCwd(pagePath)}";
74
71
 
75
- routerSignal.value = ${JSON.stringify(initialRouteValue)};
72
+ const routerProps = ${JSON.stringify(routerProps)};
76
73
 
77
- hydrateRoot(document.getElementById("root"), React.createElement(Page, {}, undefined, false, undefined, this));
74
+ hydrateRoot(document.getElementById("root"), React.createElement(App, {
75
+ routerProps: routerProps,
76
+ children: React.createElement(Page, {}),
77
+ }));
78
78
  `;
79
79
  }
80
80
 
@@ -113,23 +113,23 @@ const renderPage = async (filePath, url, params) => {
113
113
  for (const key of url.searchParams.keys()) {
114
114
  query[key] = url.searchParams.get(key);
115
115
  }
116
- const initialRouteValue = {
116
+ const routerProps = {
117
117
  query: query,
118
118
  params: params,
119
119
  pathname: url.pathname,
120
120
  }
121
- routerSignal.value = initialRouteValue;
121
+ const appPath = path.join(process.cwd(), "routes", "app.jsx");
122
- const routeImport = await import(path.join(process.cwd(), filePath));
122
+ const pagePath = path.join(process.cwd(), filePath);
123
123
  const packageJson = await import(path.join(process.cwd(), "package.json"));
124
124
  const devTag = !isProd ? "?dev" : "";
125
125
  const nodeDeps = Object.keys(packageJson.default.dependencies).reduce((acc, dep) => {
126
- acc[dep] = `https://esm.sh/${dep}@${packageJson.default.dependencies[dep]}${devTag}`;
126
+ acc[dep] = `https://esm.sh/${dep}@${packageJson.default.dependencies[dep]}`;
127
127
  return acc;
128
128
  }, {})
129
+ const App = (await import(appPath)).default;
129
- const Page = routeImport.default;
130
+ const Page = (await import(pagePath)).default;
130
131
  const components = mapDeps("components");
131
132
  const containers = mapDeps("containers");
132
- const parottaVersion = packageJson.default.dependencies["parotta"];
133
133
  const cssFile = `${filePath.replace("jsx", "css")}`;
134
134
  const stream = await renderToReadableStream(
135
135
  <html lang="en">
@@ -141,9 +141,12 @@ const renderPage = async (filePath, url, params) => {
141
141
  {
142
142
  "imports": {
143
143
  "radix3": `https://esm.sh/radix3`,
144
- "react-dom/client": `https://esm.sh/react-dom@18.2.0/client${devTag}`,
144
+ "react": `https://esm.sh/[email protected]`,
145
- "react/jsx-dev-runtime": `https://esm.sh/[email protected]/jsx-dev-runtime${devTag}`,
145
+ "react/jsx-dev-runtime": `https://esm.sh/[email protected]/jsx-dev-runtime`,
146
+ "react-dom/client": `https://esm.sh/[email protected]/client`,
147
+ "react-streaming": "https://esm.sh/react-streaming",
146
- "parotta/router": `https://esm.sh/parotta@${parottaVersion}`,
148
+ "parotta/router": `https://esm.sh/parotta@${version}/router.js`,
149
+ "parotta/error": `https://esm.sh/parotta@${version}/error.js`,
147
150
  ...nodeDeps,
148
151
  ...components,
149
152
  ...containers,
@@ -152,8 +155,8 @@ const renderPage = async (filePath, url, params) => {
152
155
  )
153
156
  }}>
154
157
  </script>
155
- <script type="module" defer dangerouslySetInnerHTML={{
158
+ <script type="module" defer={true} dangerouslySetInnerHTML={{
156
- __html: hydrateScript(filePath, initialRouteValue)
159
+ __html: hydrateScript(appPath, pagePath, routerProps)
157
160
  }}></script>
158
161
  <title>
159
162
  Parotta
@@ -161,11 +164,14 @@ const renderPage = async (filePath, url, params) => {
161
164
  </head>
162
165
  <body>
163
166
  <div id="root">
167
+ <App routerProps={routerProps}>
164
- <Page />
168
+ <Page />
169
+ </App>
165
170
  </div>
166
171
  </body>
167
172
  </html >
168
173
  );
174
+ // injectToStream('<script type="module" src="/main.js"></script>', { flush: true });
169
175
  return new Response(stream, {
170
176
  headers: { 'Content-Type': 'text/html' },
171
177
  status: 200,
@@ -193,11 +199,23 @@ const renderCss = async (url) => {
193
199
  }
194
200
  }
195
201
 
202
+ const transpiler = new Bun.Transpiler({
203
+ loader: "jsx",
204
+ autoImportJSX: true,
205
+ jsxOptimizationInline: true,
206
+
207
+ // TODO
208
+ // autoImportJSX: false,
209
+ // jsxOptimizationInline: false,
210
+ });
211
+
196
- const renderJs = async (url) => {
212
+ const renderJs = async (src) => {
197
213
  try {
198
- const jsText = await Bun.file(path.join(process.cwd(), url.pathname)).text();
214
+ const jsText = await Bun.file(src).text();
199
215
  const result = await transpiler.transform(jsText);
200
216
  const js = result.replaceAll(`import"./page.css";`, "");
217
+ // TODO
218
+ //.replaceAll("$jsx", "React.createElement");
201
219
  return new Response(js, {
202
220
  headers: { 'Content-Type': 'application/javascript' },
203
221
  status: 200,
@@ -234,8 +252,12 @@ export default {
234
252
  return renderCss(url);
235
253
  }
236
254
  if (url.pathname.endsWith(".js") || url.pathname.endsWith(".jsx")) {
237
- return renderJs(url);
255
+ return renderJs(path.join(process.cwd(), url.pathname));
238
256
  }
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
+ // }
239
261
  const match = radixRouter.lookup(url.pathname);
240
262
  if (match) {
241
263
  if (match.file) {